From 3802463a385f52379c3664ceb03806515f32f963 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 6 Aug 2026 16:00:27 +1000 Subject: [PATCH 1/2] Rework aliased rasterization and path lowering Replace threshold-based aliased coverage with exact center-sampled rasterization in both the CPU and WebGPU backends, including profile-driven thin-feature recovery for fills and updated hard-clip behavior. Rename the WebGPU flatten stage to path lowering, add the new profile/tile scene data plumbing, and update related scheduling, docs, and shader bindings. The change also removes antialias-threshold plumbing, adds shared linear-geometry profile storage, refreshes text and aliasing tests/reference images, and bumps SixLabors.Fonts. --- ImageSharp.Drawing.All.sln | 1 - .../GpuSceneDrawTag.cs | 2 +- .../ImageSharp.Drawing.WebGPU.csproj | 2 - .../Shaders/BboxClearComputeShader.cs | 2 +- .../Shaders/FineAreaComputeShader.cs | 8 +- .../Shaders/PathCountComputeShader.cs | 6 +- .../Shaders/PathCountSetupComputeShader.cs | 2 +- ...Shader.cs => PathLoweringComputeShader.cs} | 18 +- .../Shaders/PathRowAllocComputeShader.cs | 6 +- .../Shaders/PathRowSpanComputeShader.cs | 4 +- .../Shaders/PathTilingComputeShader.cs | 2 +- .../Shaders/PathtagScanComputeShader.cs | 2 +- .../Shaders/WgslSource/Shared/bbox.wgsl | 14 +- .../Shaders/WgslSource/Shared/bump.wgsl | 6 +- .../Shaders/WgslSource/Shared/config.wgsl | 13 +- .../Shaders/WgslSource/Shared/drawtag.wgsl | 13 +- .../Shaders/WgslSource/Shared/pathtag.wgsl | 6 +- .../Shaders/WgslSource/Shared/ptcl.wgsl | 7 +- .../Shaders/WgslSource/Shared/segment.wgsl | 25 +- .../Shaders/WgslSource/Shared/tile.wgsl | 15 + .../Shaders/WgslSource/bbox_clear.wgsl | 2 +- .../Shaders/WgslSource/binning.wgsl | 9 +- .../Shaders/WgslSource/chunk_reset.wgsl | 6 +- .../Shaders/WgslSource/coarse.wgsl | 90 +- .../Shaders/WgslSource/draw_leaf.wgsl | 8 +- .../Shaders/WgslSource/fine.wgsl | 565 ++++++++- .../Shaders/WgslSource/path_count.wgsl | 8 +- .../Shaders/WgslSource/path_count_setup.wgsl | 2 +- .../{flatten.wgsl => path_lowering.wgsl} | 600 +-------- .../Shaders/WgslSource/path_row_alloc.wgsl | 17 +- .../Shaders/WgslSource/path_row_span.wgsl | 6 +- .../Shaders/WgslSource/path_tiling.wgsl | 12 +- .../Shaders/WgslSource/pathtag_scan.wgsl | 2 +- .../WEBGPU_BACKEND_PROCESS.md | 2 +- .../WEBGPU_RASTERIZER.md | 49 +- .../WebGPUSceneConfig.cs | 26 +- .../WebGPUSceneDispatch.cs | 123 +- .../WebGPUSceneEncoder.cs | 1086 ++++++++++++----- .../WebGPUSceneResources.cs | 147 ++- .../CubicBezierLineSegment.cs | 47 +- .../ImageSharp.Drawing.csproj | 6 +- .../LinearGeometryProfiles.cs | 502 ++++++++ .../Processing/Backends/CompositionCommand.cs | 3 +- .../Processing/Backends/DEFAULT_RASTERIZER.md | 8 +- .../Backends/DefaultDrawingBackend.Helpers.cs | 38 +- .../Backends/DefaultDrawingBackend.cs | 4 +- .../DefaultRasterizer.Linearizer.Outputs.cs | 33 +- .../Backends/DefaultRasterizer.Linearizer.cs | 214 +++- .../DefaultRasterizer.RasterizableGeometry.cs | 27 +- .../DefaultRasterizer.RetainedTypes.cs | 262 +++- .../Backends/DefaultRasterizer.Stroke.cs | 73 +- .../DefaultRasterizer.StrokeLinearizer.cs | 58 +- .../Processing/Backends/DefaultRasterizer.cs | 942 +++++++++++++- .../Processing/Backends/FlushScene.cs | 278 +++-- .../Backends/PreparedPathClipState.cs | 8 +- .../Processing/Backends/RasterizerOptions.cs | 11 +- .../Processing/DrawingCanvas{TPixel}.cs | 30 +- .../Processing/DrawingClipDescriptor.cs | 72 +- .../Processing/DrawingClipState.cs | 11 +- .../Processing/DrawingOptions.cs | 4 +- .../Processing/RichTextGlyphRenderer.cs | 26 +- .../GraphicsOptionsTests.cs | 10 - .../Issues/Issue_134.cs | 33 +- .../Backends/WebGPUDrawingBackendTests.cs | 55 +- .../Processing/DrawingCanvasBatcherTests.cs | 11 +- .../DrawingCanvasTests.Primitives.cs | 2 +- .../Processing/DrawingClipStateTests.cs | 19 +- .../ProcessWithDrawingCanvasTests.Aliased.cs | 114 ++ ...thDrawingCanvasTests.AntialiasThreshold.cs | 133 -- ...rocessWithDrawingCanvasTests.Primitives.cs | 2 - .../Processing/RichTextGlyphRendererTests.cs | 41 + .../Shapes/TextBuilderTests.cs | 20 +- .../TestUtilities/GraphicsOptionsComparer.cs | 1 - ..._RegionAndPath_MatchesReference_Rgba32.png | 4 +- ...r_WithClipPath_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 +- ...DrawPieHelpers_MatchesReference_Rgba32.png | 4 +- ...imitiveHelpers_MatchesReference_Rgba32.png | 4 +- ...PathWithOrigin_MatchesReference_Rgba32.png | 4 +- ..._FillAndStroke_MatchesReference_Rgba32.png | 4 +- ...DifferentPaths_MatchesReference_Rgba32.png | 4 +- ...eMetricsGuides_MatchesReference_Rgba32.png | 4 +- ...awText_PenOnly_MatchesReference_Rgba32.png | 4 +- ...BlockAlongPath_MatchesReference_Rgba32.png | 4 +- ...AndLineSpacing_MatchesReference_Rgba32.png | 4 +- ...ndGradientPens_MatchesReference_Rgba32.png | 4 +- ...imitiveHelpers_MatchesReference_Rgba32.png | 4 +- ...PatternBrushes_MatchesReference_Rgba32.png | 4 +- ...ss_PathBuilder_MatchesReference_Rgba32.png | 4 +- .../Process_Path_MatchesReference_Rgba32.png | 4 +- ...ctanglePolygon_MatchesReference_Rgba32.png | 4 +- ...enderedMetrics_MatchesReference_Rgba32.png | 4 +- ...100_(0,0,0,255)_RichText-Path-(spiral).png | 4 +- ...0_(0,0,0,255)_RichText-Path-(triangle).png | 2 +- ...350_(0,0,0,255)_RichText-Path-(circle).png | 4 +- ...zontal_Rgba32_Blank100x100_type-spiral.png | 4 +- ...ntal_Rgba32_Blank120x120_type-triangle.png | 4 +- ...zontal_Rgba32_Blank350x350_type-circle.png | 4 +- ...ical_Rgba32_Blank250x250_type-triangle.png | 4 +- ...rtical_Rgba32_Blank350x350_type-circle.png | 4 +- ...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 +- .../CanRenderTextOutOfBoundsIssue301.png | 4 +- .../DrawBeziers_HotPink_A150_T5.png | 4 +- .../DrawBeziers_HotPink_A255_T5.png | 4 +- .../DrawBeziers_Red_A255_T3.png | 4 +- .../DrawBeziers_White_A255_T1.5.png | 4 +- .../DrawBeziers_White_A255_T15.png | 4 +- ...sInvalidPoints_Rgba32_T(1)_NoAntialias.png | 3 - ...sInvalidPoints_Rgba32_T(5)_NoAntialias.png | 3 - ...Dot_Rgba32_Black_A(1)_T(5)_NoAntialias.png | 4 +- ...ot_Rgba32_Yellow_A(1)_T(5)_NoAntialias.png | 4 +- ...ash_Rgba32_White_A(1)_T(5)_NoAntialias.png | 4 +- ...gba32_LightGreen_A(1)_T(5)_NoAntialias.png | 4 +- ...ple_Rgba32_White_A(1)_T(5)_NoAntialias.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 +- ...x200_(0,0,0,255)_RichText-Arabic-F(32).png | 4 +- ...x300_(0,0,0,255)_RichText-Arabic-F(40).png | 4 +- ...300_(0,0,0,255)_RichText-Rainbow-F(40).png | 4 +- ...TextStrikethroughIgnoresSkipInk_Rgba32.png | 4 +- .../DrawTextUnderlineSkipsInk_Rgba32_auto.png | 4 +- .../DrawTextUnderlineSkipsInk_Rgba32_none.png | 4 +- .../DrawText_EmojiGrid_NotoColorEmoji.png | 4 +- ...ExistingBackground_Rgba32_Blank200x200.png | 4 +- .../FillPathArcToAlternates.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 +- ...ntShapesAreRenderedCorrectly_LargeText.png | 4 +- ..._OpenSans-Regular.ttf-50-Sphi-(150,50).png | 4 +- ...5,255)_OpenSans-Regular.ttf-50-i-(0,0).png | 2 +- ...55)_OpenSans-Regular.ttf-20-Sphi-(0,0).png | 4 +- ...55)_OpenSans-Regular.ttf-50-Sphi-(0,0).png | 4 +- ...linespacing_1.5_linecount_3_wrap_False.png | 4 +- ..._linespacing_1.5_linecount_3_wrap_True.png | 4 +- ...g_linespacing_1_linecount_5_wrap_False.png | 4 +- ...ng_linespacing_1_linecount_5_wrap_True.png | 4 +- ...g_linespacing_2_linecount_2_wrap_False.png | 4 +- ...ng_linespacing_2_linecount_2_wrap_True.png | 4 +- ...egular.ttf)-S(20)-A(45)-Sphi-(200,200).png | 4 +- ...ans-Regular.ttf)-S(50)-A(45)-i-(25,25).png | 4 +- ...lar.ttf)-S(20)-A(0,-10)-Sphi-(200,200).png | 4 +- ...-Regular.ttf)-S(50)-A(-12,0)-i-(25,25).png | 4 +- ...BezierFilledBezier_Rgba32_Blank500x500.png | 4 +- ...lledPolygonOpacity_Rgba32_Blank500x500.png | 4 +- ...SvgPath_Rgba32_Blank500x400_type-bumpy.png | 4 +- ...ath_Rgba32_Blank500x400_type-pie_small.png | 4 +- ...renceClip_MatchesDefaultOutput_Default.png | 4 +- ...chesDefaultOutput_WebGPU_NativeSurface.png | 4 +- ...pectiveTransform_StarWarsCrawl_Default.png | 4 +- ...orm_StarWarsCrawl_WebGPU_NativeSurface.png | 4 +- ...s_MatchesDefaultOutput_acrylic_Default.png | 4 +- ...ultOutput_acrylic_WebGPU_NativeSurface.png | 4 +- ...atchesDefaultOutput_brightness_Default.png | 4 +- ...Output_brightness_WebGPU_NativeSurface.png | 4 +- ..._MatchesDefaultOutput_contrast_Default.png | 4 +- ...ltOutput_contrast_WebGPU_NativeSurface.png | 4 +- ...tchesDefaultOutput_drop-shadow_Default.png | 4 +- ...utput_drop-shadow_WebGPU_NativeSurface.png | 4 +- ...MatchesDefaultOutput_grayscale_Default.png | 4 +- ...tOutput_grayscale_WebGPU_NativeSurface.png | 4 +- ...atchesDefaultOutput_hue-rotate_Default.png | 4 +- ...Output_hue-rotate_WebGPU_NativeSurface.png | 4 +- ...ts_MatchesDefaultOutput_invert_Default.png | 4 +- ...aultOutput_invert_WebGPU_NativeSurface.png | 4 +- ...s_MatchesDefaultOutput_opacity_Default.png | 4 +- ...ultOutput_opacity_WebGPU_NativeSurface.png | 4 +- ..._MatchesDefaultOutput_saturate_Default.png | 4 +- ...ltOutput_saturate_WebGPU_NativeSurface.png | 4 +- ...cts_MatchesDefaultOutput_sepia_Default.png | 4 +- ...faultOutput_sepia_WebGPU_NativeSurface.png | 4 +- ...yerEffect_MatchesDefaultOutput_Default.png | 4 +- ...chesDefaultOutput_WebGPU_NativeSurface.png | 4 +- ...yerEffect_MatchesDefaultOutput_Default.png | 4 +- ...chesDefaultOutput_WebGPU_NativeSurface.png | 4 +- ...WriteBack_MatchesDefaultOutput_Default.png | 4 +- ...chesDefaultOutput_WebGPU_NativeSurface.png | 4 +- ...yerEffect_MatchesDefaultOutput_Default.png | 4 +- ...chesDefaultOutput_WebGPU_NativeSurface.png | 4 +- ...yerEffect_MatchesDefaultOutput_Default.png | 4 +- ...chesDefaultOutput_WebGPU_NativeSurface.png | 4 +- ...ectRegion_MatchesDefaultOutput_Default.png | 4 +- ...chesDefaultOutput_WebGPU_NativeSurface.png | 4 +- ...s_AfterClear_UsesBlendFastPath_Default.png | 4 +- ...UsesBlendFastPath_WebGPU_NativeSurface.png | 4 +- ...peatedGlyphs_UsesCoverageCache_Default.png | 4 +- ...UsesCoverageCache_WebGPU_NativeSurface.png | 4 +- ...ersAndReleasesPreparedCoverage_Default.png | 4 +- ...sPreparedCoverage_WebGPU_NativeSurface.png | 4 +- ...Threshold_MatchesDefaultOutput_Default.png | 4 +- ...chesDefaultOutput_WebGPU_NativeSurface.png | 4 +- ...TileCount_MatchesDefaultOutput_Default.png | 4 +- ...chesDefaultOutput_WebGPU_NativeSurface.png | 4 +- ...ientBrush_MatchesDefaultOutput_Default.png | 4 +- ...chesDefaultOutput_WebGPU_NativeSurface.png | 4 +- ..._Diagonal_MatchesDefaultOutput_Default.png | 4 +- ...chesDefaultOutput_WebGPU_NativeSurface.png | 4 +- ...ientBrush_MatchesDefaultOutput_Default.png | 4 +- ...chesDefaultOutput_WebGPU_NativeSurface.png | 4 +- ...PUBackend_MatchesDefaultOutput_Default.png | 4 +- ...chesDefaultOutput_WebGPU_NativeSurface.png | 4 +- ...LowFontSizeRenderOK_Rgb24_Full_Aliased.png | 3 + ...ontSizeRenderOK_Rgb24_Full_Antialiased.png | 3 + ...ontSizeRenderOK_Rgb24_None_Antialiased.png | 3 + .../LowFontSizeRenderOK_Rgba32_False.png | 3 - .../LowFontSizeRenderOK_Rgba32_True.png | 3 - ...Rgba32_Solid2084x2084_(138,43,226,255).png | 4 +- .../Issue_367/BrushAndTextAlign_Rgba32.png | 4 +- ...ectingClip_Difference_IntersectingClip.png | 4 +- ...tingClip_Intersection_IntersectingClip.png | 4 +- ...ingClip_Difference_NonIntersectingClip.png | 4 +- ...gClip_Intersection_NonIntersectingClip.png | 4 +- ..._Rgba32_Solid800x200_(255,255,255,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 +- 231 files changed, 4556 insertions(+), 2077 deletions(-) rename src/ImageSharp.Drawing.WebGPU/Shaders/{FlattenComputeShader.cs => PathLoweringComputeShader.cs} (84%) rename src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/{flatten.wgsl => path_lowering.wgsl} (54%) create mode 100644 src/ImageSharp.Drawing/LinearGeometryProfiles.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Aliased.cs delete mode 100644 tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.AntialiasThreshold.cs delete mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1)_NoAntialias.png delete mode 100644 tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5)_NoAntialias.png create mode 100644 tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgb24_Full_Aliased.png create mode 100644 tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgb24_Full_Antialiased.png create mode 100644 tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgb24_None_Antialiased.png delete mode 100644 tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgba32_False.png delete mode 100644 tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgba32_True.png diff --git a/ImageSharp.Drawing.All.sln b/ImageSharp.Drawing.All.sln index f82b7951c..7b317194c 100644 --- a/ImageSharp.Drawing.All.sln +++ b/ImageSharp.Drawing.All.sln @@ -513,7 +513,6 @@ Global SolutionGuid = {5F8B9D1F-CD8B-4CC5-8216-D531E25BD795} EndGlobalSection GlobalSection(SharedMSBuildProjectFiles) = preSolution - shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{061582c2-658f-40ae-a978-7d74a4eb2c0a}*SharedItemsImports = 5 shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{2e33181e-6e28-4662-a801-e2e7dc206029}*SharedItemsImports = 5 shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{68a8cc40-6aed-4e96-b524-31b1158fdeea}*SharedItemsImports = 13 EndGlobalSection diff --git a/src/ImageSharp.Drawing.WebGPU/GpuSceneDrawTag.cs b/src/ImageSharp.Drawing.WebGPU/GpuSceneDrawTag.cs index 53f3073fd..1777be776 100644 --- a/src/ImageSharp.Drawing.WebGPU/GpuSceneDrawTag.cs +++ b/src/ImageSharp.Drawing.WebGPU/GpuSceneDrawTag.cs @@ -12,7 +12,7 @@ internal static class GpuSceneDrawTag { // These values are not a plain enum because each word also encodes the path-count, clip-count, // scene-word-count, and info-word-count increments consumed by the scan/reduction stages. - // Visible-fill tags carry five extra info words: coverage threshold plus raster interest. Must match drawtag.wgsl. + // Visible-fill tags carry five extra info words: coverage data plus raster interest. Must match drawtag.wgsl. public const uint Nop = 0U; public const uint FillColor = 0x188U; public const uint FillRecolor = 0x184U; diff --git a/src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj b/src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj index e6fada2a3..1d76c2052 100644 --- a/src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj +++ b/src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj @@ -79,8 +79,6 @@ - - diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/BboxClearComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/BboxClearComputeShader.cs index 40360b7a7..e37cf2c76 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/BboxClearComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/BboxClearComputeShader.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// GPU stage that initializes every per-path bounding box to an empty (inverted) box so -/// that flatten can accumulate extents with atomic min/max. Wraps bbox_clear.wgsl. +/// that path lowering can accumulate extents with atomic min/max. Wraps bbox_clear.wgsl. /// internal static unsafe class BboxClearComputeShader { diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/FineAreaComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/FineAreaComputeShader.cs index cc35ff0ff..1f5a9239f 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/FineAreaComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/FineAreaComputeShader.cs @@ -103,7 +103,9 @@ public static unsafe bool TryCreateBindGroupLayout( // 6 gradients (sampled gradient ramp texture) // 7 image_atlas (sampled image atlas texture) // 8 backdrop_texture (sampled existing target contents) - WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[9]; + // 9 scene_data (read-only profile ranges and contour links in the packed scene stream) + // 10 path_tiles (read-only neighboring segment-slice indices for aliased row halos) + WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[11]; entries[0] = CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig)); entries[1] = CreateStorageEntry(1, WGPUBufferBindingType.ReadOnlyStorage, 0); entries[2] = CreateStorageEntry(2, WGPUBufferBindingType.ReadOnlyStorage, 0); @@ -113,10 +115,12 @@ public static unsafe bool TryCreateBindGroupLayout( entries[6] = CreateSampledTextureEntry(6); entries[7] = CreateSampledTextureEntry(7); entries[8] = CreateSampledTextureEntry(8); + entries[9] = CreateStorageEntry(9, WGPUBufferBindingType.ReadOnlyStorage, 0); + entries[10] = CreateStorageEntry(10, WGPUBufferBindingType.ReadOnlyStorage, 0); WGPUBindGroupLayoutDescriptor descriptor = new() { - entryCount = 9, + entryCount = 11, entries = entries }; diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountComputeShader.cs index 7ec01b250..4e3dba4d5 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountComputeShader.cs @@ -7,7 +7,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// GPU stage that counts, per tile, how many flattened line segments cross it: one thread per +/// GPU stage that counts, per tile, how many final line segments cross it: one thread per /// line walks the line's tile crossings, bumps tile segment counts and backdrops, and emits one /// SegmentCount record per crossing for path-tiling. Wraps path_count.wgsl. /// @@ -29,7 +29,7 @@ internal static unsafe class PathCountComputeShader /// ceil( / 256). At runtime the stage is normally driven by /// the indirect count written by path-count-setup using the same divisor. /// - /// The number of flattened lines. + /// The number of final lines. /// The X dispatch dimension in workgroups. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint GetDispatchX(uint lineCount) @@ -52,7 +52,7 @@ public static bool TryCreateBindGroupLayout( // Bindings match path_count.wgsl: // 0 config uniform // 1 bump allocators (read-write; lines read, seg_counts bump-allocated) - // 2 lines (read-only LineSoup from flatten) + // 2 lines (read-only LineSoup from path lowering) // 3 paths (read-only Path records from path_row_alloc) // 4 rows (read-only PathRow spans finalized by tile_alloc) // 5 tile (read-write; backdrop and segment counts updated atomically) diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountSetupComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountSetupComputeShader.cs index ec96b3f08..98d201c3a 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountSetupComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountSetupComputeShader.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// GPU stage that computes the indirect dispatch size for the per-line stages -/// (path-row-span and path-count) by dividing the flattened line count by the workgroup +/// (path-row-span and path-count) by dividing the final line count by the workgroup /// size, or zero workgroups after an upstream allocation failure. Wraps /// path_count_setup.wgsl. /// diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/FlattenComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathLoweringComputeShader.cs similarity index 84% rename from src/ImageSharp.Drawing.WebGPU/Shaders/FlattenComputeShader.cs rename to src/ImageSharp.Drawing.WebGPU/Shaders/PathLoweringComputeShader.cs index b59b192ae..d9ec5598c 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/FlattenComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PathLoweringComputeShader.cs @@ -7,16 +7,16 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// GPU stage that flattens encoded path segments into a device-space line soup, expands -/// strokes via the CPU PolygonStroker port, and accumulates per-path bounding boxes for the -/// downstream stages. Wraps flatten.wgsl. +/// GPU stage that lowers encoded path segments into device-space lines and expands strokes +/// with the CPU PolygonStroker port. The stage also accumulates per-path bounding boxes for +/// downstream stages. It wraps path_lowering.wgsl. /// -internal static unsafe class FlattenComputeShader +internal static unsafe class PathLoweringComputeShader { /// - /// Gets the generated WGSL source bytes for the flatten stage. + /// Gets the generated WGSL source bytes for the path-lowering stage. /// - public static ReadOnlySpan ShaderCode => GeneratedWgslShaderSources.FlattenCode; + public static ReadOnlySpan ShaderCode => GeneratedWgslShaderSources.PathLoweringCode; /// /// Gets the WGSL entry point used by this shader. @@ -35,7 +35,7 @@ public static uint GetDispatchX(uint pathTagCount) => (pathTagCount + 255U) / 256U; /// - /// Creates the bind-group layout required by the flatten stage. + /// Creates the bind-group layout required by the path-lowering stage. /// /// The WebGPU API facade. /// The device that owns the staged-scene pipelines. @@ -48,7 +48,7 @@ public static bool TryCreateBindGroupLayout( out WGPUBindGroupLayoutImpl* layout, out string? error) { - // Bindings match flatten.wgsl: + // Bindings match path_lowering.wgsl: // 0 config uniform // 1 scene (read-only path tags, points, styles and transforms) // 2 tag_monoids (read-only pathtag prefix sums from the scan stages) @@ -72,7 +72,7 @@ public static bool TryCreateBindGroupLayout( layout = api.DeviceCreateBindGroupLayout(device, in descriptor); if (layout is null) { - error = "Failed to create the flatten bind-group layout."; + error = "Failed to create the path-lowering bind-group layout."; return false; } diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathRowAllocComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathRowAllocComputeShader.cs index aba603e9e..2b894dfc1 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PathRowAllocComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PathRowAllocComputeShader.cs @@ -56,17 +56,19 @@ public static bool TryCreateBindGroupLayout( // 3 bump allocators (read-write; path_rows bump-allocated, failure bit set on overflow) // 4 paths (read-write; Path record written per draw object) // 5 rows (read-write; PathRow records initialized to empty spans) - WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[6]; + // 6 path_bboxes (read-only path bounds before clipping to the drawing bounds) + WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[7]; entries[0] = SceneShaderBindingLayoutHelper.CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig)); entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, WGPUBufferBindingType.ReadOnlyStorage); entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, WGPUBufferBindingType.ReadOnlyStorage); entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, WGPUBufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); entries[4] = SceneShaderBindingLayoutHelper.CreateStorageEntry(4, WGPUBufferBindingType.Storage); entries[5] = SceneShaderBindingLayoutHelper.CreateStorageEntry(5, WGPUBufferBindingType.Storage); + entries[6] = SceneShaderBindingLayoutHelper.CreateStorageEntry(6, WGPUBufferBindingType.ReadOnlyStorage); WGPUBindGroupLayoutDescriptor descriptor = new() { - entryCount = 6, + entryCount = 7, entries = entries }; diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathRowSpanComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathRowSpanComputeShader.cs index 40766e26e..5888c38d4 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PathRowSpanComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PathRowSpanComputeShader.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// GPU stage that derives sparse per-row tile column spans from the flattened line stream: +/// GPU stage that derives sparse per-row tile column spans from the final line stream: /// one thread per line replays the tile-crossing traversal, atomically growing each touched /// row's span and accumulating per-row winding backdrops for tile-alloc. Wraps /// path_row_span.wgsl. @@ -40,7 +40,7 @@ public static bool TryCreateBindGroupLayout( // Bindings match path_row_span.wgsl: // 0 config uniform // 1 bump allocators (read-write because the buffer is atomic; this stage only reads the lines counter) - // 2 lines (read-only LineSoup from flatten) + // 2 lines (read-only LineSoup from path lowering) // 3 paths (read-only Path records from path_row_alloc) // 4 rows (read-write; spans, backdrops and flags accumulated atomically) WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[5]; diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingComputeShader.cs index feb3683f3..b9594985a 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingComputeShader.cs @@ -40,7 +40,7 @@ public static bool TryCreateBindGroupLayout( // Bindings match path_tiling.wgsl: // 0 bump allocators (read-write because the buffer is atomic; this stage only reads the seg_counts total) // 1 seg_counts (read-only SegmentCount records from path_count) - // 2 lines (read-only LineSoup from flatten) + // 2 lines (read-only LineSoup from path lowering) // 3 paths (read-only Path records) // 4 rows (read-only sparse PathRow records) // 5 tiles (read-only; segment_count_or_ix holds the inverted segment base index from coarse) diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathtagScanComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathtagScanComputeShader.cs index b019a9b50..68eb4e01a 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PathtagScanComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PathtagScanComputeShader.cs @@ -7,7 +7,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// GPU stage that finishes the pathtag scan, writing an exclusive prefix TagMonoid per 4-byte -/// tag word for flatten to locate path data in the scene stream. Two generated variants share +/// tag word for path lowering to locate path data in the scene stream. Two generated variants share /// this wrapper: the small variant scans the pathtag-reduce partials directly, while the large /// variant reads per-workgroup prefixes precomputed by pathtag-scan1. Wraps /// pathtag_scan.wgsl. diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/bbox.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/bbox.wgsl index 4859d45eb..f9426a178 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/bbox.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/bbox.wgsl @@ -3,18 +3,18 @@ // Per-path device-space bounding box record. // -// Cleared by bbox_clear.wgsl, atomically widened by flatten.wgsl (via its +// Cleared by bbox_clear.wgsl, atomically widened by path_lowering.wgsl (via its // field-compatible AtomicPathBbox view of the same buffer), then consumed by // binning.wgsl, clip_reduce.wgsl, clip_leaf.wgsl, and draw_leaf.wgsl. // Ported from Vello's shader/shared/bbox.wgsl (linebender/vello) with -// ImageSharp additions (coverage_threshold, interest). +// ImageSharp additions (coverage_data, interest). // The annotated bounding box for a transformed path. // Coordinates are integer pixels (for the convenience of atomic update) // but will probably become fixed-point fractions for rectangles. // // `draw_flags` is propagated to the draw info stream and carries fill-rule, aliasing, and blend state. -// Field layout must mirror AtomicPathBbox in flatten.wgsl. +// Field layout must mirror AtomicPathBbox in path_lowering.wgsl. struct PathBbox { x0: i32, // min bounds; i32 max while empty (see bbox_clear) y0: i32, @@ -22,11 +22,9 @@ struct PathBbox { y1: i32, draw_flags: u32, trans_ix: u32, // index of the path's transform in the scene stream - // Per-fill coverage parameter, propagated from the style record to the fine pass. - // For aliased fills this is the quantization cutoff in [0,1]; for antialiased fills it - // carries the perceptual coverage boost for text (zero when disabled). The draw-flags - // aliased bit selects the interpretation; the two uses are mutually exclusive. - coverage_threshold: f32, + // Per-fill antialiased coverage adjustment propagated from the style record to the fine pass. + // Aliased fills use exact centre sampling and leave this value at zero. + coverage_data: f32, _padding: u32, interest: vec4, // raster interest rect (x0, y0, x1, y1) in pixels } diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/bump.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/bump.wgsl index 9d40f8b73..8238d0fca 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/bump.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/bump.wgsl @@ -7,7 +7,7 @@ // compare the result against the capacity in Config; on overflow they set // their STAGE_* bit in `failed` and the C# side reads the block back, grows // the buffers, and retries the render. Imported by most compute stages -// (binning, flatten, tile/row allocation, counting, coarse, chunk_reset, +// (binning, path lowering, tile/row allocation, counting, coarse, chunk_reset, // prepare, and the indirect-dispatch setup shaders). // // Ported from Vello's shader/shared/bump.wgsl (linebender/vello), where the @@ -16,7 +16,7 @@ // Bitflags for each stage that can fail allocation. const STAGE_BINNING: u32 = 0x1u; const STAGE_TILE_ALLOC: u32 = 0x2u; -const STAGE_FLATTEN: u32 = 0x4u; +const STAGE_PATH_LOWERING: u32 = 0x4u; const STAGE_PATH_COUNT: u32 = 0x8u; const STAGE_COARSE: u32 = 0x10u; @@ -33,7 +33,7 @@ struct BumpAllocators { seg_counts: atomic, // SegmentCount records segments: atomic, // Segment records blend_spill: atomic, // blend stack slots spilled past BLEND_STACK_SPLIT - lines: atomic, // LineSoup records from flattening + lines: atomic, // final LineSoup records from path lowering } // Workgroup counts for dispatchWorkgroupsIndirect, written by the setup diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/config.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/config.wgsl index 2510c8004..9d6725ddc 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/config.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/config.wgsl @@ -55,9 +55,16 @@ struct Config { segments_size: u32, blend_size: u32, ptcl_size: u32, - // Scene-wide coverage cutoff consumed by the aliased fine pass; per-fill - // thresholds (CmdFill.coverage_threshold) override it where set. - fine_coverage_threshold: f32, + + // Word offset of the segment-profile tag table, or zero when the table is absent. Each encoded + // point pair has one slot, which lets a fill segment find its X and Y profile identifiers. + profile_slots_base: u32, + // Word offset of the profile record table, or zero when the table is absent. The table starts + // with the X and Y record counts. Each record then stores two axis limits and one contour link. + profile_records_base: u32, + profile_pad0: u32, + profile_pad1: u32, + profile_pad2: u32, } // Geometry of tiles and bins diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/drawtag.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/drawtag.wgsl index f6e1d3a0d..c3f3e28e8 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/drawtag.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/drawtag.wgsl @@ -4,7 +4,7 @@ // Draw tag stream decoding: the draw monoid, tag values, and draw-flag bits. // // Imported by binning.wgsl, clip_leaf.wgsl, coarse.wgsl, draw_leaf.wgsl, -// draw_reduce.wgsl, fine.wgsl, flatten.wgsl, and path_row_alloc.wgsl. +// draw_reduce.wgsl, fine.wgsl, path_lowering.wgsl, and path_row_alloc.wgsl. // Ported from Vello's shader/shared/drawtag.wgsl (linebender/vello) with // ImageSharp additions (recolor, elliptic and path gradient tags, the // aliased-coverage bit, and the blend mode/alpha draw-flag fields). @@ -28,7 +28,7 @@ struct DrawMonoid { // Each draw object has a 32-bit draw tag, which is a bit-packed // version of the draw monoid: bit 0 = clip count, bits 2..4 = scene words, // bits 6..9 = info words (see map_draw_tag). -// Visible-fill draw tags carry five extra info words: coverage threshold plus raster interest. +// Visible-fill draw tags carry five extra info words: coverage data plus raster interest. const DRAWTAG_NOP = 0u; const DRAWTAG_FILL_COLOR = 0x188u; const DRAWTAG_FILL_RECOLOR = 0x184u; @@ -45,9 +45,8 @@ const DRAWTAG_END_CLIP = 0x21u; // draw object stream but is used after the draw objects have been reduced on the GPU. // 0 represents a non-zero fill. 1 represents an even-odd fill. const DRAW_INFO_FLAGS_FILL_RULE_BIT = 1u; -// Per-fill coverage rule. When set, the fill is rasterized aliased (coverage quantized against -// config.fine_coverage_threshold) instead of using analytic area coverage. Carried in a free -// high bit of the draw-flags word alongside the fill-rule bit. +// Per-fill coverage rule. When set, the fill is rasterized using exact centre sampling instead +// of analytic area coverage. Carried in a free high bit alongside the fill-rule bit. const DRAW_INFO_FLAGS_ALIASED_BIT = 0x40000000u; // Blend state packed into the draw-flags word by the C# encoder // (WebGPUSceneEncoder.PackStyleDrawFlags): bits 1..13 hold the (mix << 8) | compose @@ -59,10 +58,8 @@ const DRAW_FLAGS_BLEND_ALPHA_MASK = 0x3fffc000u; // Flag bits carried in the high bits of the clip blend word, set by the C# encoder // (WebGPUSceneEncoder.AppendClipBeginData). DIFFERENCE marks an ImageSharp Difference -// clip so fine inverts the mask; HARD marks a hard-edge (aliased) clip mask. Declared -// here because every consumer (draw_leaf, coarse, fine) imports this module. +// clip so fine inverts the mask. Declared here because every consumer imports this module. const CLIP_DIFFERENCE_MASK_BIT = 0x80000000u; -const CLIP_HARD_MASK_BIT = 0x40000000u; // Marks an ISOLATED group (a layer). Isolated groups seed with transparent content and // composite back with blend_mix_compose, matching the CPU backend's clean layer targets. // Non-isolated groups (canvas clips) seed with a copy of the current tile content and pop diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/pathtag.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/pathtag.wgsl index 89f5a448d..dba8b40fa 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/pathtag.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/pathtag.wgsl @@ -5,9 +5,9 @@ // // The scene encodes each path as a byte stream of tags (one per segment, // plus transform/style/path markers). The pathtag_reduce/scan stages compute -// a prefix sum of TagMonoid over that stream so flatten.wgsl can locate each +// a prefix sum of TagMonoid over that stream so path_lowering.wgsl can locate each // segment's point data, transform, and style in O(1). Imported by -// flatten.wgsl, pathtag_reduce.wgsl, pathtag_reduce2.wgsl, pathtag_scan.wgsl, +// path_lowering.wgsl, pathtag_reduce.wgsl, pathtag_reduce2.wgsl, pathtag_scan.wgsl, // and pathtag_scan1.wgsl. // // Ported from Vello's shader/shared/pathtag.wgsl (linebender/vello). Tag @@ -43,7 +43,7 @@ const STYLE_SIZE_IN_WORDS: u32 = 10u; // Flag bits of the first word of a Style record, packed by the C# encoder. // STYLE marks a stroke (vs fill) and FILL marks the even-odd fill rule // (clear = non-zero); the cap and join fields drive the GPU stroker in -// flatten.wgsl. +// path_lowering.wgsl. const STYLE_FLAGS_STYLE: u32 = 0x80000000u; const STYLE_FLAGS_FILL: u32 = 0x40000000u; diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/ptcl.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/ptcl.wgsl index 56d9942d3..00ee41929 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/ptcl.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/ptcl.wgsl @@ -47,10 +47,9 @@ struct CmdFill { size_and_rule: u32, // bit 0 = even-odd, bit 1 = aliased coverage, bits 2.. = segment count seg_data: u32, // index of the tile's first Segment in segment storage backdrop: i32, // winding number at the tile's left edge - // Per-fill coverage parameter, mode-dependent because the two uses are mutually - // exclusive: the quantization threshold when the aliased bit in size_and_rule is set, - // otherwise the perceptual coverage boost for antialiased text (zero when disabled). - coverage_threshold: f32, + // Mode-dependent payload. Aliased fills pack the path-tile index and horizontal-neighbor + // flags. Antialiased fills store the bit pattern of their coverage boost, or zero. + coverage_data: u32, interest: vec4, // pixel rect (x0, y0, x1, y1); coverage outside it is zeroed } diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/segment.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/segment.wgsl index 7c9abbd8d..ebd5be9f7 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/segment.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/segment.wgsl @@ -1,9 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -// Line segment records flowing between flattening and fine rasterization. +// Line segment records used after path lowering and before fine rasterization. // -// Imported by fine.wgsl, flatten.wgsl, path_count.wgsl, path_row_span.wgsl, +// Imported by fine.wgsl, path_lowering.wgsl, path_count.wgsl, path_row_span.wgsl, // and path_tiling.wgsl. Ported from Vello's shader/shared/segment.wgsl // (linebender/vello). @@ -18,9 +18,23 @@ struct Segment { // 1e9 if it does not; fine accumulates the implied vertical edge there // to keep winding consistent after clipping. y_edge: f32, + // Profile identifiers used by aliased thin-feature recovery. Bits 0..15 select the profile + // based on left and right movement. Bits 16..31 select the profile based on up and down + // movement. An all-ones field means that no profile is available for that axis. + tag: u32, } -// A line segment produced by flattening and ready for rasterization. +// Marks a segment retained only so the aliased row walk in the tile on its left can read the +// exact crossing. The segment's owning tile skips it, preserving the original edge ownership. +const HALO_ONLY_Y_EDGE = -1e9; + +// Profile identifiers occupy 16 bits in a segment tag. The largest value means that the segment +// has no profile for that axis. A tag with both fields set to the sentinel has no profile data. +const PROFILE_ID_MASK = 0xffffu; +const PROFILE_ID_SENTINEL = PROFILE_ID_MASK; +const PROFILE_TAG_SENTINEL = 0xffffffffu; + +// A final line segment produced by the path-lowering stage. // // The name is perhaps too playful, but reflects the fact that these // lines are completely unordered. They will flow through coarse path @@ -28,9 +42,10 @@ struct Segment { // segment storage so that each (tile, path) tuple gets a contiguous // slice of segments. struct LineSoup { + // Index of the path that owns this line. path_ix: u32, - // Note: this creates an alignment gap. Don't worry about - // this now, but maybe move to scalars. + // Profile tag copied from the encoded fill segment to every per-tile Segment record. + tag: u32, p0: vec2, p1: vec2, } diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/tile.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/tile.wgsl index ec256f0cc..efbb443b4 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/tile.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/tile.wgsl @@ -17,8 +17,23 @@ struct Path { bbox: vec4, // offset to the first sparse row record owned by this path rows: u32, + // Facts about the original path that remain necessary after clipping its drawing bounds. + flags: u32, } +// The original path begins left of its drawing bounds. The aliased row walk uses this flag when an +// interval enters through the clipped left edge and closes inside the first half pixel. +const PATH_FLAGS_CLIPPED_LEFT = 0x1u; + +// Coarse packs one path-tile index and three aliased row-walk flags into CmdFill.coverage_data. +// The index occupies bits 0..28. The remaining bits describe the original left clip and the two +// neighboring tiles that contain segments for the same path row. +const ALIASED_TILE_INDEX_MASK = 0x1fffffffu; +const ALIASED_CLIPPED_LEFT_BIT = 0x20000000u; +const ALIASED_LEFT_NEIGHBOR_BIT = 0x40000000u; +const ALIASED_RIGHT_NEIGHBOR_BIT = 0x80000000u; +const INVALID_TILE_INDEX = 0xffffffffu; + // One tile row of a path's bounding box. path_row_span narrows x0/x1 to the // columns actually touched, then tile_alloc allocates that span and rewrites // `tiles` from flags to the base tile index. diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/bbox_clear.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/bbox_clear.wgsl index 5dc3e92bf..4c3da5c60 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/bbox_clear.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/bbox_clear.wgsl @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. // Initializes the per-path bounding boxes to an empty (inverted) box so -// that later stages (flatten) can accumulate extents with atomic min/max. +// that path_lowering can accumulate extents with atomic min/max. // // Inputs: config uniform (n_path). // Outputs: path_bboxes, one PathBbox per path with min fields set to the diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/binning.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/binning.wgsl index 0f2a285e1..e876077f4 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/binning.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/binning.wgsl @@ -101,7 +101,7 @@ fn main( let failed = workgroupUniformLoad(&sh_previous_failed); if failed != 0u { if global_id.x == 0u { - atomicOr(&bump.failed, STAGE_FLATTEN); + atomicOr(&bump.failed, STAGE_PATH_LOWERING); } return; } @@ -139,7 +139,12 @@ fn main( // TODO check this is true let path_bbox = path_bbox_buf[draw_monoid.path_ix]; - let pb = vec4(vec4(path_bbox.x0, path_bbox.y0, path_bbox.x1, path_bbox.y1)); + // The CPU retained rasterizer extends a fill's maximum X bound by one pixel because + // its clipper otherwise discards a closing vertical edge on that boundary. Aliased + // fine needs the same edge in the next tile to complete its full scanline walk; keep + // antialiased bin coverage unchanged. + let aliased_right_pad = select(0.0, 1.0, (path_bbox.draw_flags & DRAW_INFO_FLAGS_ALIASED_BIT) != 0u); + let pb = vec4(f32(path_bbox.x0), f32(path_bbox.y0), f32(path_bbox.x1) + aliased_right_pad, f32(path_bbox.y1)); let bbox = bbox_intersect(clip_bbox, bbox_intersect(pb, path_bbox.interest)); // Only the first bin-chunk workgroup writes the intersected bbox, since diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/chunk_reset.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/chunk_reset.wgsl index 829132a9b..48849cd30 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/chunk_reset.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/chunk_reset.wgsl @@ -4,12 +4,12 @@ // Resets the chunk-local bump allocator counters between chunk dispatches // while preserving the full-scene state produced by the shared scheduling // stages. The scene is rendered in vertical tile-row chunks; the shared -// stages (flatten, binning, ...) run once, but the chunk-local stages +// stages (path_lowering, binning, ...) run once, but the chunk-local stages // (path_row_alloc through path_tiling) rerun per chunk and must start // from zeroed counters. // // Inputs/outputs: bump. Retained across chunks: the binning and lines -// counters plus the STAGE_BINNING and STAGE_FLATTEN failure bits, since +// counters plus the STAGE_BINNING and STAGE_PATH_LOWERING failure bits, since // those belong to the shared stages that are not rerun. Everything else // (ptcl, path_rows, tile, seg_counts, segments, blend_spill and the // chunk-local failure bits) is cleared. @@ -25,7 +25,7 @@ var bump: BumpAllocators; // whole struct so chunk-local counters start at zero. @compute @workgroup_size(1) fn main() { - let retained_failed = atomicLoad(&bump.failed) & (STAGE_BINNING | STAGE_FLATTEN); + let retained_failed = atomicLoad(&bump.failed) & (STAGE_BINNING | STAGE_PATH_LOWERING); let retained_binning = atomicLoad(&bump.binning); let retained_lines = atomicLoad(&bump.lines); diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/coarse.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/coarse.wgsl index 94ed25b6d..d81e84b2b 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/coarse.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/coarse.wgsl @@ -18,9 +18,9 @@ // allocated segment index), bump (ptcl, segments, blend_spill, failed). // // Derived from Vello's coarse.wgsl. Local divergences: sparse row-based tile -// lookup instead of dense per-path tile grids, per-draw coverage thresholds -// and interest rectangles, aliased-coverage fills, difference/hard clip -// bits, extra draw tags (recolor, elliptic/sweep/path gradients), and +// lookup instead of dense per-path tile grids, per-draw coverage data and +// interest rectangles, centre-sampled aliased fills, difference clip bits, +// extra draw tags (recolor, elliptic/sweep/path gradients), and // chunked rendering via config.chunk_tile_y_start. #import config @@ -121,13 +121,12 @@ fn alloc_cmd(size: u32) { } // Determines whether a tile with no crossing segments produces any visible -// coverage. The backdrop winding is resolved through the fill rule (even-odd -// folds the winding, non-zero saturates it) and, for aliased fills, -// quantized against the coverage threshold. Returns true when the resulting -// coverage is non-zero. -fn solid_tile_has_coverage(draw_flags: u32, backdrop: i32, coverage_threshold: f32) -> bool { +// coverage. The backdrop winding is resolved through the fill rule: even-odd +// folds the winding and non-zero saturates it. The same rule serves aliased +// fills, which sample the shape at pixel centres: a segment-free tile has one +// winding everywhere, so its pixels are all inside or all outside. +fn solid_tile_has_coverage(draw_flags: u32, backdrop: i32) -> bool { let even_odd = (draw_flags & DRAW_INFO_FLAGS_FILL_RULE_BIT) != 0u; - let aliased = (draw_flags & DRAW_INFO_FLAGS_ALIASED_BIT) != 0u; var coverage = f32(backdrop); if even_odd { @@ -136,10 +135,6 @@ fn solid_tile_has_coverage(draw_flags: u32, backdrop: i32, coverage_threshold: f coverage = min(abs(coverage), 1.0); } - if aliased { - coverage = select(0.0, 1.0, coverage >= coverage_threshold); - } - return coverage != 0.0; } @@ -149,7 +144,7 @@ fn solid_tile_has_coverage(draw_flags: u32, backdrop: i32, coverage_threshold: f // When emit_empty_solid is false, solid tiles whose backdrop resolves to // zero coverage are skipped entirely. Returns true when a command was // written, meaning the caller should emit the matching paint command. -fn write_path(tile: Tile, tile_ix: u32, draw_flags: u32, coverage_threshold: f32, interest: vec4, emit_empty_solid: bool) -> bool { +fn write_path(tile: Tile, tile_ix: u32, path: Path, global_x: u32, global_y: u32, draw_flags: u32, coverage_data: f32, interest: vec4, emit_empty_solid: bool) -> bool { // We overload the "segments" field to store both count (written by // path_count stage) and segment allocation (used by path_tiling and // fine). @@ -161,21 +156,52 @@ fn write_path(tile: Tile, tile_ix: u32, draw_flags: u32, coverage_threshold: f32 ptcl[cmd_offset] = CMD_FILL; let even_odd = (draw_flags & DRAW_INFO_FLAGS_FILL_RULE_BIT) != 0u; let aliased = (draw_flags & DRAW_INFO_FLAGS_ALIASED_BIT) != 0u; + var packed_coverage_data = bitcast(coverage_data); + if aliased { + // Pack the data needed by the aliased half-pixel halo into one word: + // bits 0..28 contain this path-tile index; + // bit 29 means the original path started left of its clipped drawing bounds; + // bit 30 means the tile on the left has segments for the same path row; + // bit 31 means the tile on the right has segments for the same path row. + // A path-tile record is eight bytes, so its storage-buffer index fits in 29 bits. + var tile_and_neighbors = tile_ix; + if (path.flags & PATH_FLAGS_CLIPPED_LEFT) != 0u { + tile_and_neighbors |= ALIASED_CLIPPED_LEFT_BIT; + } + + if global_x > 0u { + let left = lookup_tile(path, global_x - 1u, global_y); + if left.valid != 0u && tiles[left.tile_ix].segment_count_or_ix != 0u { + tile_and_neighbors |= ALIASED_LEFT_NEIGHBOR_BIT; + } + } + + let right = lookup_tile(path, global_x + 1u, global_y); + if right.valid != 0u && tiles[right.tile_ix].segment_count_or_ix != 0u { + tile_and_neighbors |= ALIASED_RIGHT_NEIGHBOR_BIT; + } + + packed_coverage_data = tile_and_neighbors; + } + // size_and_rule: bit 0 = even-odd, bit 1 = aliased coverage, bits 2.. = segment count. let size_and_rule = (n_segs << 2u) | (u32(aliased) << 1u) | u32(even_odd); - let fill = CmdFill(size_and_rule, seg_ix, tile.backdrop, coverage_threshold, interest); + let fill = CmdFill(size_and_rule, seg_ix, tile.backdrop, packed_coverage_data, interest); ptcl[cmd_offset + 1u] = fill.size_and_rule; ptcl[cmd_offset + 2u] = fill.seg_data; ptcl[cmd_offset + 3u] = u32(fill.backdrop); - ptcl[cmd_offset + 4u] = bitcast(fill.coverage_threshold); + ptcl[cmd_offset + 4u] = fill.coverage_data; ptcl[cmd_offset + 5u] = bitcast(fill.interest.x); ptcl[cmd_offset + 6u] = bitcast(fill.interest.y); ptcl[cmd_offset + 7u] = bitcast(fill.interest.z); ptcl[cmd_offset + 8u] = bitcast(fill.interest.w); + // The winding backdrop is now in PTCL. Reuse its tile field for the original segment + // count so fine can read adjacent slices after segment_count_or_ix becomes the allocation. + tiles[tile_ix].backdrop = i32(n_segs); cmd_offset += 9u; return true; } else { - if !emit_empty_solid && !solid_tile_has_coverage(draw_flags, tile.backdrop, coverage_threshold) { + if !emit_empty_solid && !solid_tile_has_coverage(draw_flags, tile.backdrop) { return false; } @@ -349,7 +375,7 @@ fn main( // another workgroup, we still want to know this workgroup's memory // requirement. if local_id.x == 0u { - var failed = atomicLoad(&bump.failed) & (STAGE_BINNING | STAGE_TILE_ALLOC | STAGE_FLATTEN); + var failed = atomicLoad(&bump.failed) & (STAGE_BINNING | STAGE_TILE_ALLOC | STAGE_PATH_LOWERING); if atomicLoad(&bump.seg_counts) > config.seg_counts_size { failed |= STAGE_PATH_COUNT; } @@ -515,11 +541,7 @@ fn main( // blend layer, so mask the operation bit before comparing with BLEND_CLIP. let raw_blend = scene[dd]; is_difference_clip = (raw_blend & CLIP_DIFFERENCE_MASK_BIT) != 0u; - let is_hard_clip = (raw_blend & CLIP_HARD_MASK_BIT) != 0u; var blend = raw_blend & ~(CLIP_DIFFERENCE_MASK_BIT | CLIP_ISOLATED_MASK_BIT); - if is_hard_clip { - blend &= ~CLIP_HARD_MASK_BIT; - } // Isolated groups (layers) must never take the solid-tile skip below: their // contents composite against a transparent seed, so skipping the group would @@ -574,14 +596,14 @@ fn main( let dd = config.drawdata_base + dm.scene_offset; let di = dm.info_offset; let draw_flags = info_bin_data[di]; - var coverage_threshold = -1.0; + var coverage_data = 0.0; var interest = vec4(0.0, 0.0, f32(config.target_width), f32(config.target_height)); // Draw tags whose info block spans at least five words append a - // coverage threshold plus interest rectangle at the end of it. + // coverage data plus interest rectangle at the end of it. let drawtag_info_size = (drawtag >> 6u) & 0xfu; if drawtag_info_size >= 5u { let interest_offset = di + drawtag_info_size - 5u; - coverage_threshold = bitcast(info_bin_data[interest_offset]); + coverage_data = bitcast(info_bin_data[interest_offset]); interest = vec4( bitcast(info_bin_data[interest_offset + 1u]), bitcast(info_bin_data[interest_offset + 2u]), @@ -600,50 +622,50 @@ fn main( let tile = tiles[tile_ix]; switch drawtag { case DRAWTAG_FILL_COLOR: { - if write_path(tile, tile_ix, draw_flags, coverage_threshold, interest, false) { + if write_path(tile, tile_ix, path, bin_tile_x + tile_x, bin_tile_y + tile_y, draw_flags, coverage_data, interest, false) { write_color(CmdColor(scene[dd], scene[dd + 1u], draw_flags)); } } case DRAWTAG_FILL_RECOLOR: { - if write_path(tile, tile_ix, draw_flags, coverage_threshold, interest, false) { + if write_path(tile, tile_ix, path, bin_tile_x + tile_x, bin_tile_y + tile_y, draw_flags, coverage_data, interest, false) { write_recolor(config.brush_data_base + scene[dd], draw_flags); } } case DRAWTAG_FILL_LIN_GRADIENT: { - if write_path(tile, tile_ix, draw_flags, coverage_threshold, interest, false) { + if write_path(tile, tile_ix, path, bin_tile_x + tile_x, bin_tile_y + tile_y, draw_flags, coverage_data, interest, false) { let index = scene[dd]; let info_offset = di + 1u; write_grad(CMD_LIN_GRAD, index, info_offset); } } case DRAWTAG_FILL_RAD_GRADIENT: { - if write_path(tile, tile_ix, draw_flags, coverage_threshold, interest, false) { + if write_path(tile, tile_ix, path, bin_tile_x + tile_x, bin_tile_y + tile_y, draw_flags, coverage_data, interest, false) { let index = scene[dd]; let info_offset = di + 1u; write_grad(CMD_RAD_GRAD, index, info_offset); } } case DRAWTAG_FILL_ELLIPTIC_GRADIENT: { - if write_path(tile, tile_ix, draw_flags, coverage_threshold, interest, false) { + if write_path(tile, tile_ix, path, bin_tile_x + tile_x, bin_tile_y + tile_y, draw_flags, coverage_data, interest, false) { let index = scene[dd]; let info_offset = di + 1u; write_grad(CMD_ELLIPTIC_GRAD, index, info_offset); } } case DRAWTAG_FILL_SWEEP_GRADIENT: { - if write_path(tile, tile_ix, draw_flags, coverage_threshold, interest, false) { + if write_path(tile, tile_ix, path, bin_tile_x + tile_x, bin_tile_y + tile_y, draw_flags, coverage_data, interest, false) { let index = scene[dd]; let info_offset = di + 1u; write_grad(CMD_SWEEP_GRAD, index, info_offset); } } case DRAWTAG_FILL_PATH_GRADIENT: { - if write_path(tile, tile_ix, draw_flags, coverage_threshold, interest, false) { + if write_path(tile, tile_ix, path, bin_tile_x + tile_x, bin_tile_y + tile_y, draw_flags, coverage_data, interest, false) { write_path_grad(config.brush_data_base + scene[dd], scene[dd + 1u], scene[dd + 2u], draw_flags); } } case DRAWTAG_FILL_IMAGE: { - if write_path(tile, tile_ix, draw_flags, coverage_threshold, interest, false) { + if write_path(tile, tile_ix, path, bin_tile_x + tile_x, bin_tile_y + tile_y, draw_flags, coverage_data, interest, false) { write_image(di + 1u); } } @@ -667,7 +689,7 @@ fn main( case DRAWTAG_END_CLIP: { clip_depth -= 1u; let blend = scene[dd]; - write_path(tile, tile_ix, draw_flags, coverage_threshold, interest, true); + write_path(tile, tile_ix, path, bin_tile_x + tile_x, bin_tile_y + tile_y, draw_flags, coverage_data, interest, true); let alpha = bitcast(scene[dd + 1u]); write_end_clip(CmdEndClip(blend, alpha)); render_blend_depth -= 1u; diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/draw_leaf.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/draw_leaf.wgsl index e5e148d86..94ee250b7 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/draw_leaf.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/draw_leaf.wgsl @@ -9,7 +9,7 @@ // // Inputs: config uniform; scene stream; reduced (per-workgroup DrawMonoid // aggregates from draw_reduce); path_bbox (per-path bounds, draw flags, -// coverage threshold and raster interest from the path bbox stages). +// coverage data and raster interest from the path bbox stages). // Outputs: draw_monoid (exclusive prefix per draw object); info (per-draw // brush info words consumed by coarse and fine); clip_inp (ClipInp records // consumed by clip_reduce and clip_leaf). @@ -17,7 +17,7 @@ // Ported from Vello's draw_leaf.wgsl (linebender/vello, // vello_shaders/shader). Local divergences: extra draw tags (recolor, // elliptic and path gradients), 9-word transforms carrying a perspective -// row, coverage-threshold plus raster-interest words appended to +// row, coverage-data plus raster-interest words appended to // visible-fill info entries, and a clip operation (intersection or // difference) carried in each ClipInp record. @@ -345,12 +345,12 @@ fn main( // Visible fills and begin clips carry raster interest in the info stream so coarse can // read it without another storage-buffer binding. Bits 6..9 of the tag give the info - // word count (matching map_draw_tag); the threshold plus interest block occupies the + // word count (matching map_draw_tag); the coverage data plus interest block occupies the // final five words of the entry. let tag_info_size = (tag_word >> 6u) & 0xfu; if tag_info_size >= 5u { let interest_offset = di + tag_info_size - 5u; - info[interest_offset] = bitcast(bbox.coverage_threshold); + info[interest_offset] = bitcast(bbox.coverage_data); info[interest_offset + 1u] = bitcast(bbox.interest.x); info[interest_offset + 2u] = bitcast(bbox.interest.y); info[interest_offset + 3u] = bitcast(bbox.interest.z); diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/fine.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/fine.wgsl index d25027956..081ddeb86 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/fine.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/fine.wgsl @@ -3,10 +3,10 @@ // Fine rasterizer: the final stage of the pipeline. Each workgroup shades one // 16x16 tile by interpreting the tile's command list (PTCL) written by -// coarse.wgsl. Coverage is computed analytically from the tile's line segments -// plus backdrop winding, then brushes (solid color, recolor, linear/radial/ -// elliptic/sweep/path gradients, images) are evaluated and composited, -// including clip mask begin/end handling. +// coarse.wgsl. Antialiased fills use analytic area coverage. Aliased fills use +// exact row-centre and column-centre crossings. Brushes (solid color, recolor, +// linear/radial/elliptic/sweep/path gradients, images) are then evaluated and +// composited, including clip mask begin/end handling. // // Inputs: config uniform, segment buffer, ptcl and info streams, gradient ramp // texture, image atlas, and a backdrop texture holding the existing target @@ -15,15 +15,16 @@ // stacks deeper than BLEND_STACK_SPLIT. // // Ported from Vello's fine.wgsl (vello_shaders/shader/fine.wgsl). Local -// divergences from upstream: per-fill aliased coverage thresholds, extra PTCL -// commands (CMD_RECOLOR, CMD_ELLIPTIC_GRAD, CMD_PATH_GRAD), clip difference and -// hard mask bits, per-command raster interest rectangles, backdrop texture +// divergences from upstream: centre-sampled aliased fills, extra PTCL commands +// (CMD_RECOLOR, CMD_ELLIPTIC_GRAD, CMD_PATH_GRAD), clip difference and isolation +// bits, per-command raster interest rectangles, backdrop texture // seeding, tile-row chunking via config.chunk_tile_y_start, and gradient extend // behavior deliberately matched to the CPU brushes. #import segment #import config #import drawtag +#import tile // Scene configuration: target dimensions, tile counts, and the chunk window. @group(0) @binding(0) @@ -71,21 +72,35 @@ var image_atlas: texture_2d; @group(0) @binding(8) var backdrop_texture: texture_2d; +// The packed scene stream is bound here so aliased fills can read profile records. The table at +// profile_records_base starts with the X and Y record counts. X records follow, then Y records. +// Each record contains a minimum coordinate, a maximum coordinate, and a contour link. +@group(0) @binding(9) +var scene_data: array; + +// Coarse rewrites each non-empty path tile's segment field to an inverted slice index and then +// preserves its original segment count in backdrop. Aliased row walks use adjacent slices for +// the exact half-pixel crossing halo. +@group(0) @binding(10) +var path_tiles: array; + // Decodes a CMD_FILL payload: packed segment-count/fill-rule word, segment -// base index, backdrop winding, per-fill aliased coverage threshold, and the -// raster interest rectangle. As for all read_* decoders, cmd_ix addresses the +// base index, backdrop winding, coverage data, and the raster interest +// rectangle. Antialiased text may use the coverage word for a perceptual boost; +// coarse repacks aliased fills with the path-tile index and neighbor flags used +// by the exact crossing halo. As for all read_* decoders, cmd_ix addresses the // command tag and the payload words follow it. fn read_fill(cmd_ix: u32) -> CmdFill { let size_and_rule = ptcl[cmd_ix + 1u]; let seg_data = ptcl[cmd_ix + 2u]; let backdrop = i32(ptcl[cmd_ix + 3u]); - let coverage_threshold = bitcast(ptcl[cmd_ix + 4u]); + let coverage_data = ptcl[cmd_ix + 4u]; let interest = vec4( bitcast(ptcl[cmd_ix + 5u]), bitcast(ptcl[cmd_ix + 6u]), bitcast(ptcl[cmd_ix + 7u]), bitcast(ptcl[cmd_ix + 8u])); - return CmdFill(size_and_rule, seg_data, backdrop, coverage_threshold, interest); + return CmdFill(size_and_rule, seg_data, backdrop, coverage_data, interest); } // Expands one RGBA color stored as two binary16 pairs. Brush payloads use @@ -220,9 +235,8 @@ fn read_image(cmd_ix: u32) -> CmdImage { return CmdImage(matrx, xlat, vec2(x, y), vec2(width, height), format, x_extend, y_extend, alpha, alpha_type, signed_unit); } -// Decodes a CMD_END_CLIP payload: the packed blend mode (which may carry the -// clip difference/hard mask bits) and the layer alpha, which doubles as the -// coverage threshold for hard mask clips. +// Decodes a CMD_END_CLIP payload: the packed blend mode, including the local +// difference and isolation bits, and the layer alpha. fn read_end_clip(cmd_ix: u32) -> CmdEndClip { let blend = ptcl[cmd_ix + 1u]; let alpha = bitcast(ptcl[cmd_ix + 2u]); @@ -713,14 +727,480 @@ fn evaluate_path_gradient(path_grad: CmdPathGrad, point: vec2) -> vec4 // Number of horizontally adjacent pixels shaded by each thread. const PIXELS_PER_THREAD = 4u; +// Tests whether a centre-free interval ends at a contour tip and must remain unlit. +// +// A centre-free interval is the closed span between two crossings when that span contains no pixel +// centre. It normally lights the pixel containing its midpoint. Do not light that pixel when the +// two boundary profiles meet in the contour and both end in the same gap between centre lines. +// That shape is a terminating tip, not a continuing thin feature. Keep the tip when it reaches at +// least halfway into the pixel and the interval is also at least half a pixel long. +// +// `x_axis` selects X profiles for a vertical interval and Y profiles for a horizontal interval. +// `a` and `b` identify the profiles on the two edges that bound the interval. +fn profile_is_stub(x_axis: bool, a: u32, b: u32, centre_px: f32, span_px: f32) -> bool { + // A sentinel identifier or an absent record table means the interval cannot be classified as + // a terminating tip. Keep its midpoint pixel. + if a == PROFILE_ID_SENTINEL || b == PROFILE_ID_SENTINEL || config.profile_records_base == 0u { + return false; + } + + // The record region starts with the X and Y counts. Each following record has three words: + // minimum, maximum, and adjacency link. + let records = config.profile_records_base; + let x_count = scene_data[records]; + let y_count = scene_data[records + 1u]; + var base = records + 2u; + var count = x_count; + if !x_axis { + base = records + 2u + x_count * 3u; + count = y_count; + } + if a >= count || b >= count { + return false; + } + + let link_a = bitcast(scene_data[base + a * 3u + 2u]); + let link_b = bitcast(scene_data[base + b * 3u + 2u]); + + // Bit 0 records a connection to the previous identifier. Bits 1..31 contain the one-based + // identifier joined across a closed contour's final point. + let adjacent = (b == a + 1u && (link_b & 1) != 0) + || (a == b + 1u && (link_a & 1) != 0) + || (link_a >> 1u) == i32(b + 1u) + || (link_b >> 1u) == i32(a + 1u); + if !adjacent { + return false; + } + + let min_a = bitcast(scene_data[base + a * 3u]); + let max_a = bitcast(scene_data[base + a * 3u + 1u]); + let min_b = bitcast(scene_data[base + b * 3u]); + let max_b = bitcast(scene_data[base + b * 3u + 1u]); + let centre = i32(round(centre_px * 256.0)); + let span = i32(round(span_px * 256.0)); + + // A tip on the positive side has both profile ends before the next centre. A tip on the + // negative side has both profile starts after the previous centre. Keep either tip when it + // reaches halfway across its pixel and the interval is at least half a pixel wide. + if max_a < centre + 256 && max_b < centre + 256 { + let tip = max(max_a, max_b); + return !((tip & 255) >= 128 && span >= 128); + } + if min_a > centre - 256 && min_b > centre - 256 { + let tip = min(min_a, min_b); + let fraction = tip & 255; + return !(fraction != 0 && fraction <= 128 && span >= 128); + } + return false; +} + +// Fixed shared-memory capacities for one row or column crossing list. On overflow, the exact +// centre-winding result remains valid. Only midpoint recovery for a centre-free interval is skipped. +const ROW_CROSSING_CAPACITY = 16u; +const COLUMN_CROSSING_CAPACITY = 8u; + +// Four invocations shade each row, but all four need the same crossing list. The invocation whose +// first pixel has X = 0 scans the geometry once and calculates winding for all sixteen row centres. +// This replaces four identical segment scans, four halo scans, and four sorts per row. +var aliased_row_crossings: array, TILE_HEIGHT>; +var aliased_row_counts: array; +var aliased_row_overflow: array; +var aliased_row_seed: array; +var aliased_row_winding: array, TILE_HEIGHT>; + +// The four invocations in row zero own four columns each. They build all sixteen column lists once. +var aliased_column_crossings: array, TILE_WIDTH>; +var aliased_column_counts: array; +var aliased_column_overflow: array; + +// Packs one crossing into a sortable word: +// bits 0..15: profile identifier +// bit 16: positive winding direction +// bits 17..31: tile-relative position, biased by 16 pixels, with 9 fractional bits +// +// The full 15-bit position field is required for the right halo, which extends to X = 16.5. +fn pack_crossing(position: f32, direction_positive: bool, profile_id: u32) -> u32 { + let fixed = min(u32(max(position + 16.0, 0.0) * 512.0), 0x7fffu); + return (fixed << 17u) | (u32(direction_positive) << 16u) | profile_id; +} + +// Recovers the position a crossing was packed with. +fn unpack_crossing_position(packed: u32) -> f32 { + return (f32(packed >> 17u) / 512.0) - 16.0; +} + +// Inserts one crossing into its row's shared sorted list. One invocation owns each row, so the +// insertion needs no atomics. Keeping the list sorted here also removes four per-thread sorts. +fn insert_aliased_row_crossing(row: u32, packed: u32) { + let count = aliased_row_counts[row]; + if count < ROW_CROSSING_CAPACITY { + var insert = count; + while insert > 0u && aliased_row_crossings[row][insert - 1u] > packed { + aliased_row_crossings[row][insert] = aliased_row_crossings[row][insert - 1u]; + insert -= 1u; + } + + aliased_row_crossings[row][insert] = packed; + aliased_row_counts[row] = count + 1u; + } else { + aliased_row_overflow[row] = 1u; + } +} + +// Computes binary coverage for one 16x16 tile. +// +// The workgroup has 64 invocations. Each invocation returns four adjacent pixels in one row. +// Processing has four phases: +// 1. Sixteen row owners and four column owners clear their shared lists. +// 2. Row owners scan current and neighboring tile segments. Column owners scan current segments. +// 3. A barrier publishes the sorted crossings and exact centre windings. +// 4. Every invocation consumes its row list and four column lists. +// +// Exact winding decides normal centre coverage. A closed interval that contains no centre lights +// its midpoint pixel unless its profiles identify a terminating tip. The column walk applies the +// same rule vertically to recover horizontal features that lie between two row centres. +fn fill_path_aliased(fill: CmdFill, xy: vec2, global_xy: vec2, result: ptr>) { + let n_segs = fill.size_and_rule >> 2u; + let even_odd = (fill.size_and_rule & 1u) != 0u; + let sample_y = xy.y + 0.5; + let row = u32(xy.y); + let owns_row = xy.x == 0.0; + let owns_columns = xy.y == 0.0; + + // A tile can contain consecutive aliased fill commands. All invocations must finish reading + // the previous command's shared row and column state before its owners clear it. + workgroupBarrier(); + + if owns_row { + aliased_row_counts[row] = 0u; + aliased_row_overflow[row] = 0u; + aliased_row_seed[row] = fill.backdrop; + for (var i = 0u; i < TILE_WIDTH; i += 1u) { + aliased_row_winding[row][i] = fill.backdrop; + } + } + + if owns_columns { + for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + let column = u32(xy.x) + i; + aliased_column_counts[column] = 0u; + aliased_column_overflow[column] = 0u; + } + } + + // The sixteen row owners and four column owners are the only invocations that need segment + // data. The remaining 45 invocations wait at the collection barrier and then consume their + // row and columns. Row zero, X zero owns both one row and four columns. + if owns_row || owns_columns { + for (var s = 0u; s < n_segs; s++) { + let segment = segments[fill.seg_data + s]; + if segment.y_edge == HALO_ONLY_Y_EDGE { + continue; + } + + let delta = segment.point1 - segment.point0; + + // The CPU captures crossings after rounding endpoints to 24.8. Use the same integer + // interpolation so hinted edges and exact half-pixel positions select the same pixels. + let p0_fixed = vec2(round(segment.point0 * 256.0)); + let p1_fixed = vec2(round(segment.point1 * 256.0)); + + if owns_row { + let sample_y_fixed = i32(sample_y * 256.0); + + // Half-open span ownership counts a vertex on the centre line on exactly one edge. + let crosses_row = (p0_fixed.y <= sample_y_fixed) != (p1_fixed.y <= sample_y_fixed); + if crosses_row { + let x_cross_fixed = p0_fixed.x + + (((p1_fixed.x - p0_fixed.x) * (sample_y_fixed - p0_fixed.y)) / (p1_fixed.y - p0_fixed.y)); + + // Increasing local X walks from the tile's left edge. Match the sign used by + // the backdrop and y_edge values so all three contributions form one winding. + let direction = -i32(sign(delta.y)); + for (var k = 0u; k < TILE_WIDTH; k += 1u) { + if x_cross_fixed <= (i32(k) << 8) + 128 { + aliased_row_winding[row][k] += direction; + } + } + + // A horizontal interval uses the Y profiles of its two boundary edges. Their + // identifiers occupy bits 16..31 of the segment tag. + insert_aliased_row_crossing( + row, + pack_crossing(f32(x_cross_fixed) / 256.0, direction > 0, segment.tag >> 16u)); + } + + // y_edge represents the part of this segment clipped to the left of the tile. Add + // its winding step to every centre below that crossing. It is not a column event; + // the corresponding geometry lies outside this tile. + if segment.y_edge < 1.0e9 { + // NearestCrossingCount rounds an exact half-row carry away from zero, so a + // left-edge crossing on the row centre is included. + let step = select(0, i32(sign(delta.x)), sample_y >= segment.y_edge); + for (var k = 0u; k < TILE_WIDTH; k += 1u) { + aliased_row_winding[row][k] += step; + } + + aliased_row_seed[row] += step; + } + } + + if owns_columns { + for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + let column = u32(xy.x) + i; + let sample_x_fixed = (i32(column) << 8) + 128; + if (p0_fixed.x <= sample_x_fixed) != (p1_fixed.x <= sample_x_fixed) { + let column_count = aliased_column_counts[column]; + if column_count < COLUMN_CROSSING_CAPACITY { + let y_cross_fixed = p0_fixed.y + + (((p1_fixed.y - p0_fixed.y) * (sample_x_fixed - p0_fixed.x)) / (p1_fixed.x - p0_fixed.x)); + + // A vertical interval uses the X profiles of its two boundary edges. + // Their identifiers occupy bits 0..15 of the segment tag. + let packed = pack_crossing( + f32(y_cross_fixed) / 256.0, + segment.point1.x > segment.point0.x, + segment.tag & PROFILE_ID_MASK); + + // Keep the shared list sorted as it is built. Sorting once here avoids + // copying and sorting the same column again in every row. + var insert = column_count; + while insert > 0u && aliased_column_crossings[column][insert - 1u] > packed { + aliased_column_crossings[column][insert] = aliased_column_crossings[column][insert - 1u]; + insert -= 1u; + } + + aliased_column_crossings[column][insert] = packed; + aliased_column_counts[column] = column_count + 1u; + } else { + aliased_column_overflow[column] = 1u; + } + } + } + } + } + } + + // A CPU row list is not split at tile boundaries. A crossing no more than half a pixel outside + // this tile can pair with a local crossing to form a centre-free interval whose midpoint belongs + // to this tile. Read that narrow halo from the two real neighboring tile slices. Do not add halo + // crossings to centre winding; backdrop and y_edge already include their winding effect. + let tile_and_neighbors = fill.coverage_data; + let current_tile = tile_and_neighbors & ALIASED_TILE_INDEX_MASK; + let halo_tiles = array( + select(INVALID_TILE_INDEX, current_tile - 1u, (tile_and_neighbors & ALIASED_LEFT_NEIGHBOR_BIT) != 0u), + select(INVALID_TILE_INDEX, current_tile + 1u, (tile_and_neighbors & ALIASED_RIGHT_NEIGHBOR_BIT) != 0u)); + if owns_row { + for (var h = 0u; h < 2u; h += 1u) { + let halo_tile_ix = halo_tiles[h]; + if halo_tile_ix == INVALID_TILE_INDEX { + continue; + } + + let halo_data = ~path_tiles[halo_tile_ix].segment_count_or_ix; + let halo_count = u32(path_tiles[halo_tile_ix].backdrop); + let fixed_offset = select(-4096, 4096, h != 0u); + for (var s = 0u; s < halo_count; s += 1u) { + let segment = segments[halo_data + s]; + let p0_fixed = vec2(round(segment.point0 * 256.0)); + let p1_fixed = vec2(round(segment.point1 * 256.0)); + let sample_y_fixed = i32(sample_y * 256.0); + if (p0_fixed.y <= sample_y_fixed) == (p1_fixed.y <= sample_y_fixed) { + continue; + } + + let local_x_fixed = p0_fixed.x + + (((p1_fixed.x - p0_fixed.x) * (sample_y_fixed - p0_fixed.y)) / (p1_fixed.y - p0_fixed.y)) + + fixed_offset; + let in_halo = select( + local_x_fixed >= -128 && local_x_fixed <= 0, + local_x_fixed >= 4096 && local_x_fixed <= 4224, + h != 0u); + if !in_halo { + continue; + } + + let direction = -i32(sign(segment.point1.y - segment.point0.y)); + insert_aliased_row_crossing( + row, + pack_crossing(f32(local_x_fixed) / 256.0, direction > 0, segment.tag >> 16u)); + + if h == 0u { + // row_seed is the winding at X = 0. The sorted list begins in the left halo, so + // remove each halo crossing to obtain the winding immediately before that list. + aliased_row_seed[row] -= direction; + } + } + } + } + + // Publish the row and column collections before any invocation reads its four-pixel slice. + workgroupBarrier(); + + let row_count = aliased_row_counts[row]; + let row_overflow = aliased_row_overflow[row] != 0u; + let row_seed = aliased_row_seed[row]; + + var lit: array; + for (var k = 0u; k < PIXELS_PER_THREAD; k += 1u) { + let centre_winding = aliased_row_winding[row][u32(xy.x) + k]; + if even_odd { + lit[k] = (centre_winding & 1) != 0; + } else { + lit[k] = centre_winding != 0; + } + } + + // Walk the sorted row crossings from left to right. row_seed is the winding immediately before + // the first local or halo crossing. + if !row_overflow && row_count > 0u { + var w = row_seed; + if even_odd { + w &= 1; + } + + var interval_start = 0.0; + var enter_id = PROFILE_ID_SENTINEL; + var has_local_start = false; + for (var c = 0u; c < row_count; c += 1u) { + let packed = aliased_row_crossings[row][c]; + let position = unpack_crossing_position(packed) - xy.x; + let previous = w; + if even_odd { + w ^= 1; + } else { + w += select(-1, 1, ((packed >> 16u) & 1u) != 0u); + } + + // Interval coverage is closed at both ends. The direct winding comparison already + // includes an opening edge on a centre, but it excludes a closing edge on a centre. + // Restore that closing endpoint explicitly. + let centre_index = i32(floor(position)); + if centre_index >= 0 && centre_index < i32(PIXELS_PER_THREAD) + && position == f32(centre_index) + 0.5 && (previous != 0 || w != 0) { + lit[u32(centre_index)] = true; + } + + if previous == 0 && w != 0 { + interval_start = position; + enter_id = packed & PROFILE_ID_MASK; + has_local_start = true; + continue; + } + if previous == 0 || w != 0 { + continue; + } + + // An interval already open before the halo is not a local centre-free interval; it may + // have covered centres farther left. The clipped-left case is different: the CPU keeps + // the closing edge's one-pixel extension, so its first destination pixel remains covered. + if !has_local_start { + let clipped_left = (tile_and_neighbors & ALIASED_CLIPPED_LEFT_BIT) != 0u; + if clipped_left && position == 0.0 && global_xy.x + 1.0 == fill.interest.z { + lit[0] = true; + } + + continue; + } + has_local_start = false; + + // For closed endpoint coverage, the first contained centre is ceil(start - 0.5) and + // the last is floor(end - 0.5). last < first means the interval contains no centre. + let first = i32(ceil(interval_start - 0.5)); + let last = i32(floor(position - 0.5)); + if last >= first { + continue; + } + + let interval_start_fixed = i32(round((interval_start + xy.x) * 256.0)); + let position_fixed = i32(round((position + xy.x) * 256.0)); + let midpoint_pixel = ((interval_start_fixed >> 1) + (position_fixed >> 1)) >> 8; + let local = midpoint_pixel - i32(xy.x); + if local >= 0 && local < i32(PIXELS_PER_THREAD) && !lit[u32(local)] + && !profile_is_stub(false, enter_id, packed & PROFILE_ID_MASK, global_xy.y + 0.5, position - interval_start) { + lit[u32(local)] = true; + } + } + } + + // The CPU column pass starts each 16-row band at zero and accepts only intervals with both + // crossings inside that band. Do not derive a top-edge winding seed: that would turn geometry + // clipped above the band into a new open interval and could add a pixel absent from the CPU. + for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + let column = u32(xy.x) + i; + let column_count = aliased_column_counts[column]; + + // Match the CPU's merge order. It consults column results only when this row has at least + // one horizontal-centre crossing. Overflow disables only this secondary recovery. + if lit[i] || row_overflow || row_count == 0u || aliased_column_overflow[column] != 0u || column_count < 2u { + continue; + } + + var w = 0; + + var interval_start = 0.0; + var enter_id = PROFILE_ID_SENTINEL; + for (var c = 0u; c < column_count; c += 1u) { + let packed = aliased_column_crossings[column][c]; + let position = unpack_crossing_position(packed); + let previous = w; + if even_odd { + w ^= 1; + } else { + w += select(-1, 1, ((packed >> 16u) & 1u) != 0u); + } + + if previous == 0 && w != 0 { + interval_start = position; + enter_id = packed & PROFILE_ID_MASK; + continue; + } + if previous == 0 || w != 0 { + continue; + } + + // Closed at both ends, as in the row walk: an interval reaching a row centre + // exactly is owned by that row's walk and is not a collapsed interval. + if i32(floor(position - 0.5)) >= i32(ceil(interval_start - 0.5)) { + continue; + } + + // Match the CPU's overflow-safe fixed-point midpoint exactly: it + // halves each endpoint before adding, so two odd coordinates land + // one 24.8 unit below their mathematical average. + let interval_start_fixed = i32(round(interval_start * 256.0)); + let position_fixed = i32(round(position * 256.0)); + let midpoint_row = ((interval_start_fixed >> 1) + (position_fixed >> 1)) >> 8; + if midpoint_row == i32(xy.y) + && !profile_is_stub(true, enter_id, packed & PROFILE_ID_MASK, global_xy.x + f32(i) + 0.5, position - interval_start) { + lit[i] = true; + } + } + } + + var area: array; + for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + area[i] = select(0.0, 1.0, lit[i]); + } + + // Discard coverage outside the fill's raster interest rectangle. + for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + let pixel = global_xy + vec2(f32(i), 0.0); + if pixel.x < fill.interest.x || pixel.y < fill.interest.y || pixel.x >= fill.interest.z || pixel.y >= fill.interest.w { + area[i] = 0.0; + } + } + + *result = area; +} + // Computes per-pixel coverage for a CMD_FILL using analytic area // anti-aliasing. Signed winding is accumulated from the backdrop and every -// segment in the tile, the fill rule is applied, aliased fills are quantized -// against their per-fill threshold, and coverage outside the fill's raster -// interest rectangle is zeroed. xy is the thread's first pixel in tile-local -// space (segments are tile-relative); global_xy is the same pixel in -// full-target space. result receives coverage for PIXELS_PER_THREAD adjacent -// pixels. +// segment in the tile, the fill rule is applied, and coverage outside the +// fill's raster interest rectangle is zeroed. xy is the thread's first pixel +// in tile-local space (segments are tile-relative); global_xy is the same +// pixel in full-target space. result receives coverage for PIXELS_PER_THREAD +// adjacent pixels. // // FIXME: This should return an array when https://github.com/gfx-rs/naga/issues/1930 is fixed. fn fill_path(fill: CmdFill, xy: vec2, global_xy: vec2, result: ptr>) { @@ -728,6 +1208,11 @@ fn fill_path(fill: CmdFill, xy: vec2, global_xy: vec2, result: ptr> 2u; let even_odd = (fill.size_and_rule & 1u) != 0u; let aliased = (fill.size_and_rule & 2u) != 0u; + if aliased { + fill_path_aliased(fill, xy, global_xy, result); + return; + } + var area: array; let backdrop_f = f32(fill.backdrop); for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { @@ -736,6 +1221,10 @@ fn fill_path(fill: CmdFill, xy: vec2, global_xy: vec2, result: ptr, global_xy: vec2, result: ptr= threshold); - } - } else if fill.coverage_threshold > 0.0 { - // For antialiased fills the coverage_threshold word carries the perceptual coverage - // boost instead (the two uses are mutually exclusive). An S-curve darkens coverage + if bitcast(fill.coverage_data) > 0.0 { + // For antialiased fills the coverage_data word carries the perceptual coverage boost. + // An S-curve darkens coverage // above one half and lightens it below, so text stems solidify while nearly-open // counters stay bright; at full strength the remap is exactly smoothstep. Matches // the CPU rasterizer's AreaToCoverage boost. - let boost = fill.coverage_threshold; + let boost = bitcast(fill.coverage_data); for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { let a = area[i]; area[i] = a + boost * a * (1.0 - a) * (2.0 * a - 1.0); @@ -966,21 +1445,10 @@ fn main( // Difference clips reuse the same clip path but invert the mask at the // point where the saved backdrop is restored. This keeps clip operation // semantics in the clip stack instead of fabricating inverse path geometry. - // Hard mask clips binarize coverage against the stored alpha - // threshold instead of scaling the layer by it. - let is_hard_clip = (end_clip.blend & CLIP_HARD_MASK_BIT) != 0u; - var source_clip_area = area[i]; - if is_hard_clip { - source_clip_area = select(0.0, 1.0, source_clip_area > end_clip.alpha); - } - // Strip the local mask bits so only the packed blend mode remains. - let clip_area = select(source_clip_area, 1.0 - source_clip_area, (end_clip.blend & CLIP_DIFFERENCE_MASK_BIT) != 0u); + let clip_area = select(area[i], 1.0 - area[i], (end_clip.blend & CLIP_DIFFERENCE_MASK_BIT) != 0u); let isolated = (end_clip.blend & CLIP_ISOLATED_MASK_BIT) != 0u; - var clip_blend = end_clip.blend & ~(CLIP_DIFFERENCE_MASK_BIT | CLIP_ISOLATED_MASK_BIT); - if is_hard_clip { - clip_blend &= ~CLIP_HARD_MASK_BIT; - } + let clip_blend = end_clip.blend & ~(CLIP_DIFFERENCE_MASK_BIT | CLIP_ISOLATED_MASK_BIT); let bg = unpack_clip_color(bg_rgba); @@ -994,8 +1462,7 @@ fn main( continue; } - let clip_alpha = select(end_clip.alpha, 1.0, is_hard_clip); - let fg = rgba[i] * clip_alpha; + let fg = rgba[i] * end_clip.alpha; if clip_blend == LUMINANCE_MASK_LAYER { // TODO: Does this case apply more generally? diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_count.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_count.wgsl index c7ef13caf..48b1c1322 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_count.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_count.wgsl @@ -1,14 +1,14 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -// Counts, per tile, how many line segments from the flattened line soup +// Counts, per tile, how many segments from the final line stream // cross it. One thread per line: walks the line's tile boundary crossings, // atomically increments the crossed tile's segment count, accumulates // top-edge winding deltas into tile backdrops, and emits one SegmentCount // record per crossing so path_tiling can later write the clipped segments. // // Inputs: config uniform, bump (lines counter, seg_counts allocator), -// lines (LineSoup from flatten), paths (Path records from path_row_alloc), +// lines (LineSoup from path lowering), paths (Path records from path_row_alloc), // rows (PathRow spans finalized by tile_alloc). // Outputs: tile (backdrop and segment_count_or_ix, atomically updated), // seg_counts (one record per line/tile crossing), bump.seg_counts. @@ -92,8 +92,8 @@ fn main( let dx = abs(s1.x - s0.x); let dy = s1.y - s0.y; - // Zero-length segment, drop it. This could be culled in the flattening - // stage, but letting one slip through here would be much worse. + // Path lowering rejects zero length before grid snapping. Snapping can still collapse a short + // line, so reject it again before the divisions below. if dx + dy == 0.0 { return; } diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_count_setup.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_count_setup.wgsl index 0a51863f5..8faa193fd 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_count_setup.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_count_setup.wgsl @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. // Computes the indirect dispatch size for the per-line stages. Divides the -// flattened line count (bump.lines) by the workgroup size, or dispatches +// final line count (bump.lines) by the workgroup size, or dispatches // zero workgroups when an earlier stage recorded an allocation failure. // // Inputs: bump (lines counter, failed mask). diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/flatten.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_lowering.wgsl similarity index 54% rename from src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/flatten.wgsl rename to src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_lowering.wgsl index 849c36651..42f0d86ba 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/flatten.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_lowering.wgsl @@ -1,17 +1,13 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -// Flatten stage: converts encoded path segments into a device-space "line soup" and accumulates -// per-path bounding boxes for the downstream draw_leaf/binning/coarse/fine stages. Inputs are the -// scene stream (path tags, points, styles, transforms) at binding 1, the config uniform at -// binding 0, and the pathtag prefix sums at binding 2; outputs are the atomic path bboxes -// (binding 3), the bump allocators (binding 4), and the LineSoup buffer (binding 5). Ported from -// Vello's flatten.wgsl. Fills of every segment type route through flatten_euler exactly as in -// upstream Vello: read_path_segment lowers lines to degenerate cubics and the Euler-spiral math -// resolves them through its low-curvature path. The C# encoder currently pre-flattens fill curves -// on the CPU, so in practice only line segments arrive for fills. Stroke expansion diverges from -// upstream: it is a port of the CPU PolygonStroker (chained joins, miters, arcs, and caps) with -// quadto tags reused as stroke tangent markers rather than curves. +// Path-lowering stage. Fill paths already contain final line segments, which this stage transforms +// and writes to LineSoup. Stroke paths contain centre lines; this stage expands them into +// the same joins, miters, arcs, and caps as the CPU stroker. It also calculates each path's bounds. +// No curve subdivision occurs here. +// +// Inputs: configuration, encoded scene data, and path-tag prefix sums. +// Outputs: path bounds, line allocation counts, and final device-space lines. #import config #import drawtag @@ -29,7 +25,7 @@ var scene: array; var tag_monoids: array; // Per-path device-space bounding box plus draw metadata copied from the style stream. The extents -// are atomic so the many invocations flattening segments of the same path can merge their results. +// are atomic so the many invocations lowering segments of the same path can merge their results. struct AtomicPathBbox { x0: atomic, y0: atomic, @@ -38,7 +34,7 @@ struct AtomicPathBbox { draw_flags: u32, trans_ix: u32, // Must mirror PathBbox in bbox.wgsl: both views alias the same buffer, so the strides must match. - coverage_threshold: f32, + coverage_data: f32, _padding: u32, interest: vec4, } @@ -52,452 +48,10 @@ var bump: BumpAllocators; @group(0) @binding(5) var lines: array; -// Functions for Euler spirals - -// Geometric Hermite parameters of a curve segment: tangent angles at each endpoint measured -// relative to the chord, the chord length, and an estimate of the Euler-spiral fit error. -struct CubicParams { - th0: f32, - th1: f32, - chord_len: f32, - err: f32, -} - -// Normalized Euler spiral parameters: th0 is the start tangent angle relative to the chord, -// k0 and k1 the curvature and its derivative, ch the chord length of the unit-arclength spiral. -struct EulerParams { - th0: f32, - // th1 need not be explicitly stored, as it can be derived from k0 - th0 - k0: f32, - k1: f32, - ch: f32, -} - -// An Euler spiral segment between two endpoints with its normalized spiral parameters. -struct EulerSeg { - p0: vec2f, - p1: vec2f, - params: EulerParams, -} - -// Threshold below which a derivative is considered too small. -const DERIV_THRESH: f32 = 1e-6; -const DERIV_THRESH_SQUARED: f32 = DERIV_THRESH * DERIV_THRESH; -// Amount to nudge t when derivative is near-zero. -const DERIV_EPS: f32 = 1e-6; -// Limit for subdivision of cubic Beziers. -const SUBDIV_LIMIT: f32 = 1.0 / 65536.0; -// Robust ESPC computation: below this value, treat curve as circular arc -const K1_THRESH: f32 = 1e-3; -// Robust ESPC: below this value, evaluate ES rather than parallel curve -const DIST_THRESH: f32 = 1e-3; -// Threshold for tangents to be considered near zero length +// A tangent shorter than this value has no stable direction for a join or cap. The encoder removes +// normal micro-segments; this value remains as the stroker's division-by-zero limit. const TANGENT_THRESH: f32 = 1e-6; -// Computes geometric Hermite parameters for a curve piece from its endpoints (p0, p1) and -// endpoint derivatives (q0, q1) over parameter width dt, including an error estimate for -// approximating the piece with a single Euler spiral segment. -fn cubic_from_points_derivs(p0: vec2f, p1: vec2f, q0: vec2f, q1: vec2f, dt: f32) -> CubicParams { - let chord = p1 - p0; - let chord_squared = dot(chord, chord); - let chord_len = sqrt(chord_squared); - if chord_squared < DERIV_THRESH_SQUARED { - let chord_err = sqrt((9. / 32.0) * (dot(q0, q0) + dot(q1, q1))) * dt; - return CubicParams(0.0, 0.0, DERIV_THRESH, chord_err); - } - let scale = dt / chord_squared; - let h0 = vec2(q0.x * chord.x + q0.y * chord.y, q0.y * chord.x - q0.x * chord.y); - let th0 = atan2(h0.y, h0.x); - let d0 = length(h0) * scale; - let h1 = vec2(q1.x * chord.x + q1.y * chord.y, q1.x * chord.y - q1.y * chord.x); - let th1 = atan2(h1.y, h1.x); - let d1 = length(h1) * scale; - - // Estimate error of geometric Hermite interpolation to Euler spiral. - let cth0 = cos(th0); - let cth1 = cos(th1); - var err = 2.0; - if cth0 * cth1 >= 0.0 { - let e0 = (2. / 3.) / max(1.0 + cth0, 1e-9); - let e1 = (2. / 3.) / max(1.0 + cth1, 1e-9); - let s0 = sin(th0); - let s1 = sin(th1); - let s01 = cth0 * s1 + cth1 * s0; - let amin = 0.15 * (2. * e0 * s0 + 2. * e1 * s1 - e0 * e1 * s01); - let a = 0.15 * (2. * d0 * s0 + 2. * d1 * s1 - d0 * d1 * s01); - let aerr = abs(a - amin); - let symm = abs(th0 + th1); - let asymm = abs(th0 - th1); - let dist = length(vec2(d0 - e0, d1 - e1)); - let symm2 = symm * symm; - let ctr = (4.625e-6 * symm * symm2 + 7.5e-3 * asymm) * symm2; - let halo = (5e-3 * symm + 7e-2 * asymm) * dist; - err = ctr + 1.55 * aerr + halo; - } - err *= chord_len; - return CubicParams(th0, th1, chord_len, err); -} - -// Solves for the Euler spiral whose endpoint tangent angles (relative to the chord) are th0 and -// th1, using polynomial approximations for the curvature terms and the normalized chord length. -fn es_params_from_angles(th0: f32, th1: f32) -> EulerParams { - let k0 = th0 + th1; - let dth = th1 - th0; - let d2 = dth * dth; - let k2 = k0 * k0; - var a = 6.0; - a -= d2 * (1. / 70.); - a -= (d2 * d2) * (1. / 10780.); - a += (d2 * d2 * d2) * 2.769178184818219e-07; - let b = -0.1 + d2 * (1. / 4200.) + d2 * d2 * 1.6959677820260655e-05; - let c = -1. / 1400. + d2 * 6.84915970574303e-05 - k2 * 7.936475029053326e-06; - a += (b + c * k2) * k2; - let k1 = dth * a; - - // calculation of chord - var ch = 1.0; - ch -= d2 * (1. / 40.); - ch += (d2 * d2) * 0.00034226190482569864; - ch -= (d2 * d2 * d2) * 1.9349474568904524e-06; - let b_ = -1. / 24. + d2 * 0.0024702380951963226 - d2 * d2 * 3.7297408997537985e-05; - let c_ = 1. / 1920. - d2 * 4.87350869747975e-05 - k2 * 3.1001936068463107e-06; - ch += (b_ + c_ * k2) * k2; - return EulerParams(th0, k0, k1, ch); -} - -// Evaluates the tangent angle of the Euler spiral at parameter t, relative to the chord. -fn es_params_eval_th(params: EulerParams, t: f32) -> f32 { - return (params.k0 + 0.5 * params.k1 * (t - 1.0)) * t - params.th0; -} - -// Integrates the Euler spiral with curvature k0 and curvature derivative k1 over unit arclength, -// using a degree-10 polynomial expansion. Returns the endpoint displacement as (u, v). -fn integ_euler_10(k0: f32, k1: f32) -> vec2f { - let t1_1 = k0; - let t1_2 = 0.5 * k1; - let t2_2 = t1_1 * t1_1; - let t2_3 = 2. * (t1_1 * t1_2); - let t2_4 = t1_2 * t1_2; - let t3_4 = t2_2 * t1_2 + t2_3 * t1_1; - let t3_6 = t2_4 * t1_2; - let t4_4 = t2_2 * t2_2; - let t4_5 = 2. * (t2_2 * t2_3); - let t4_6 = 2. * (t2_2 * t2_4) + t2_3 * t2_3; - let t4_7 = 2. * (t2_3 * t2_4); - let t4_8 = t2_4 * t2_4; - let t5_6 = t4_4 * t1_2 + t4_5 * t1_1; - let t5_8 = t4_6 * t1_2 + t4_7 * t1_1; - let t6_6 = t4_4 * t2_2; - let t6_7 = t4_4 * t2_3 + t4_5 * t2_2; - let t6_8 = t4_4 * t2_4 + t4_5 * t2_3 + t4_6 * t2_2; - let t7_8 = t6_6 * t1_2 + t6_7 * t1_1; - let t8_8 = t6_6 * t2_2; - var u = 1.; - u -= (1. / 24.) * t2_2 + (1. / 160.) * t2_4; - u += (1. / 1920.) * t4_4 + (1. / 10752.) * t4_6 + (1. / 55296.) * t4_8; - u -= (1. / 322560.) * t6_6 + (1. / 1658880.) * t6_8; - u += (1. / 92897280.) * t8_8; - var v = (1. / 12.) * t1_2; - v -= (1. / 480.) * t3_4 + (1. / 2688.) * t3_6; - v += (1. / 53760.) * t5_6 + (1. / 276480.) * t5_8; - v -= (1. / 11612160.) * t7_8; - return vec2(u, v); -} - -// Evaluates the Euler spiral point at parameter t in chord-relative coordinates, where the -// segment endpoints map to (0, 0) at t = 0 and (1, 0) at t = 1. -fn es_params_eval(params: EulerParams, t: f32) -> vec2f { - let thm = es_params_eval_th(params, t * 0.5); - let k0 = params.k0; - let k1 = params.k1; - let uv = integ_euler_10((k0 + k1 * (0.5 * t - 0.5)) * t, k1 * t * t); - let scale = t / params.ch; - let s = scale * sin(thm); - let c = scale * cos(thm); - let x = uv.x * c - uv.y * s; - let y = -uv.y * c - uv.x * s; - return vec2(x, y); -} - -// Evaluates the parallel curve of the Euler spiral at parameter t, displaced along the spiral -// normal by `offset` (in chord-relative units). -fn es_params_eval_with_offset(params: EulerParams, t: f32, offset: f32) -> vec2f { - let th = es_params_eval_th(params, t); - let v = offset * vec2f(sin(th), cos(th)); - return es_params_eval(params, t) + v; -} - -// Constructs an Euler spiral segment from its endpoints and normalized spiral parameters. -fn es_seg_from_params(p0: vec2f, p1: vec2f, params: EulerParams) -> EulerSeg { - return EulerSeg(p0, p1, params); -} - -// Evaluates the parallel curve of an Euler spiral segment at parameter t, mapped back into the -// coordinate space of the segment endpoints. Note: the offset is normalized so 1 = chord length. -fn es_seg_eval_with_offset(es: EulerSeg, t: f32, normalized_offset: f32) -> vec2f { - let chord = es.p1 - es.p0; - let xy = es_params_eval_with_offset(es.params, t, normalized_offset); - return es.p0 + vec2f(chord.x * xy.x - chord.y * xy.y, chord.x * xy.y + chord.y * xy.x); -} - -// Computes sign(x) * |x|^1.5, used by the ESPC subdivision integrals. -fn pow_1_5_signed(x: f32) -> f32 { - return x * sqrt(abs(x)); -} - -// Breakpoints and fitted coefficients for the piecewise ESPC integral approximation below. -const BREAK1: f32 = 0.8; -const BREAK2: f32 = 1.25; -const BREAK3: f32 = 2.1; -const SIN_SCALE: f32 = 1.0976991822760038; -const QUAD_A1: f32 = 0.6406; -const QUAD_B1: f32 = -0.81; -const QUAD_C1: f32 = 0.9148117935952064; -const QUAD_A2: f32 = 0.5; -const QUAD_B2: f32 = -0.156; -const QUAD_C2: f32 = 0.16145779359520596; -const QUAD_W1: f32 = 0.5 * QUAD_B1 / QUAD_A1; -const QUAD_V1: f32 = 1.0 / QUAD_A1; -const QUAD_U1: f32 = QUAD_W1 * QUAD_W1 - QUAD_C1 / QUAD_A1; -const QUAD_W2: f32 = 0.5 * QUAD_B2 / QUAD_A2; -const QUAD_V2: f32 = 1.0 / QUAD_A2; -const QUAD_U2: f32 = QUAD_W2 * QUAD_W2 - QUAD_C2 / QUAD_A2; -const FRAC_PI_4: f32 = 0.7853981633974483; -const CBRT_9_8: f32 = 1.040041911525952; - -// Piecewise approximation of the ESPC (Euler spiral parallel curve) subdivision density -// integral. Odd in x: the sign of the input is preserved. -fn espc_int_approx(x: f32) -> f32 { - let y = abs(x); - var a: f32; - if y < BREAK1 { - a = sin(SIN_SCALE * y) * (1.0 / SIN_SCALE); - } else if y < BREAK2 { - a = (sqrt(8.0) / 3.0) * pow_1_5_signed(y - 1.0) + FRAC_PI_4; - } else { - let abc = select(vec3(QUAD_A2, QUAD_B2, QUAD_C2), vec3(QUAD_A1, QUAD_B1, QUAD_C1), y < BREAK3); - a = (abc.x * y + abc.y) * y + abc.z; - }; - return a * sign(x); -} - -// Piecewise approximation of the inverse of espc_int_approx, used to map evenly spaced -// subdivision fractions back to spiral parameters. Odd in x. -fn espc_int_inv_approx(x: f32) -> f32 { - let y = abs(x); - var a: f32; - if y < 0.7010707591262915 { - a = asin(y * SIN_SCALE) * (1.0 / SIN_SCALE); - } else if y < 0.903249293595206 { - let b = y - FRAC_PI_4; - let u = pow(abs(b), 2. / 3.) * sign(b); - a = u * CBRT_9_8 + 1.0; - } else { - let uvw = select(vec3(QUAD_U2, QUAD_V2, QUAD_W2), vec3(QUAD_U1, QUAD_V1, QUAD_W1), y < 2.038857793595206); - a = sqrt(uvw.x + uvw.y * y) - uvw.z; - } - return a * sign(x); -} - -// A point on a curve together with its first derivative at the same parameter. -struct PointDeriv { - point: vec2f, - deriv: vec2f, -} - -// Evaluates a cubic Bezier and its first derivative at parameter t. The returned derivative is -// scaled by 1/3 relative to the true derivative. -fn eval_cubic_and_deriv(p0: vec2f, p1: vec2f, p2: vec2f, p3: vec2f, t: f32) -> PointDeriv { - let m = 1.0 - t; - let mm = m * m; - let mt = m * t; - let tt = t * t; - let p = p0 * (mm * m) + (p1 * (3.0 * mm) + p2 * (3.0 * mt) + p3 * tt) * t; - let q = (p1 - p0) * mm + (p2 - p1) * (2.0 * mt) + (p3 - p2) * tt; - return PointDeriv(p, q); -} - -// Returns the start tangent of a cubic Bezier, falling back to later control points when the -// leading ones are coincident so degenerate segments still yield a usable direction. -fn cubic_start_tangent(p0: vec2f, p1: vec2f, p2: vec2f, p3: vec2f) -> vec2f { - let EPS = 1e-12; - let d01 = p1 - p0; - let d02 = p2 - p0; - let d03 = p3 - p0; - return select(select(d03, d02, dot(d02, d02) > EPS), d01, dot(d01, d01) > EPS); -} - -// Robustness regimes for the ESPC subdivision integral in flatten_euler: near-zero curvature -// derivative and near-zero offset each get a numerically stable specialization. -const ESPC_ROBUST_NORMAL = 0; -const ESPC_ROBUST_LOW_K1 = 1; -const ESPC_ROBUST_LOW_DIST = 2; - -// Flattens a cubic Bezier by first converting it into Euler spiral segments, then computing a -// near-optimal flattening of the parallel curves of the Euler spiral segments. With offset == 0 -// the curve itself is flattened in device space; a non-zero offset flattens the parallel curve in -// local space and transforms each emitted line. `start_p`/`end_p` pin the exact endpoints so -// adjacent segments stay watertight. Retained from Vello: the C# encoder currently pre-flattens -// fill curves to lines (degree-raised to cubics), so this path sees no true curves today, but the -// capability is kept intact. -fn flatten_euler( - cubic: CubicPoints, - path_ix: u32, - local_to_device: Transform, - offset: f32, - start_p: vec2f, - end_p: vec2f, -) { - var p0: vec2f; - var p1: vec2f; - var p2: vec2f; - var p3: vec2f; - var scale: f32; - var transform: Transform; - var t_start = start_p; - var t_end = end_p; - if offset == 0. { - let t = local_to_device; - p0 = transform_apply(t, cubic.p0); - p1 = transform_apply(t, cubic.p1); - p2 = transform_apply(t, cubic.p2); - p3 = transform_apply(t, cubic.p3); - scale = 1.; - transform = transform_identity(); - t_start = p0; - t_end = p3; - } else { - p0 = cubic.p0; - p1 = cubic.p1; - p2 = cubic.p2; - p3 = cubic.p3; - - transform = local_to_device; - let mat = transform.mat; - // The scale is the semi-major axis of the ellipse given by the unit - // circle transformed by `transform`. This is the greater of the two - // singular values of the 2x2 `transform` matrix (ignoring - // translation). - scale = 0.5 * (length(vec2(mat.x + mat.w, mat.y - mat.z)) + - length(vec2(mat.x - mat.w, mat.y + mat.z))); - } - - // Drop zero length lines. This is an exact equality test because dropping very short - // line segments may result in loss of watertightness. - if all(p0 == p1) && all(p0 == p2) && all(p0 == p3) { - return; - } - - let tol = 0.25; - var t0_u = 0u; - var dt = 1.0; - var last_p = p0; - var last_q = p1 - p0; - if dot(last_q, last_q) < DERIV_THRESH_SQUARED { - last_q = eval_cubic_and_deriv(p0, p1, p2, p3, DERIV_EPS).deriv; - } - var last_t = 0.0; - var lp0 = t_start; - loop { - let t0 = f32(t0_u) * dt; - if t0 == 1.0 { - break; - } - var t1 = t0 + dt; - let this_p0 = last_p; - let this_q0 = last_q; - var this_pq1 = eval_cubic_and_deriv(p0, p1, p2, p3, t1); - if dot(this_pq1.deriv, this_pq1.deriv) < DERIV_THRESH_SQUARED { - let new_pq1 = eval_cubic_and_deriv(p0, p1, p2, p3, t1 - DERIV_EPS); - this_pq1.deriv = new_pq1.deriv; - if t1 < 1.0 { - this_pq1.point = new_pq1.point; - t1 = t1 - DERIV_EPS; - } - } - let actual_dt = t1 - last_t; - let cubic_params = cubic_from_points_derivs(this_p0, this_pq1.point, this_q0, this_pq1.deriv, actual_dt); - if cubic_params.err * scale <= tol || dt <= SUBDIV_LIMIT { - let euler_params = es_params_from_angles(cubic_params.th0, cubic_params.th1); - let es = es_seg_from_params(this_p0, this_pq1.point, euler_params); - let k0 = es.params.k0 - 0.5 * es.params.k1; - let k1 = es.params.k1; - let normalized_offset = offset / cubic_params.chord_len; - let dist_scaled = normalized_offset * es.params.ch; - let scale_multiplier = sqrt(0.125 * scale * cubic_params.chord_len / (es.params.ch * tol)); - var a = 0.0; - var b = 0.0; - var integral = 0.0; - var int0 = 0.0; - var n_frac: f32; - var robust = ESPC_ROBUST_NORMAL; - if abs(k1) < K1_THRESH { - let k = es.params.k0; - n_frac = sqrt(abs(k * (k * dist_scaled + 1.0))); - robust = ESPC_ROBUST_LOW_K1; - } else if abs(dist_scaled) < DIST_THRESH { - a = k1; - b = k0; - int0 = pow_1_5_signed(b); - let int1 = pow_1_5_signed(a + b); - integral = int1 - int0; - n_frac = (2. / 3.) * integral / a; - robust = ESPC_ROBUST_LOW_DIST; - } else { - a = -2.0 * dist_scaled * k1; - b = -1.0 - 2.0 * dist_scaled * k0; - int0 = espc_int_approx(b); - let int1 = espc_int_approx(a + b); - integral = int1 - int0; - let k_peak = k0 - k1 * b / a; - let integrand_peak = sqrt(abs(k_peak * (k_peak * dist_scaled + 1.0))); - n_frac = integral * integrand_peak / a; - } - // Bound number of subdivisions to a reasonable number when the scale is huge. - // This may give slightly incorrect rendering but avoids hangs. - // TODO: aggressively cull to viewport - let n = clamp(ceil(n_frac * scale_multiplier), 1.0, 100.0); - for (var i = 0u; i < u32(n); i++) { - var lp1: vec2f; - if i + 1u == u32(n) && t1 == 1.0 { - lp1 = t_end; - } else { - let t = f32(i + 1u) / n; - var s = t; - if robust != ESPC_ROBUST_LOW_K1 { - let u = integral * t + int0; - var inv: f32; - if robust == ESPC_ROBUST_LOW_DIST { - inv = pow(abs(u), 2. / 3.) * sign(u); - } else { - inv = espc_int_inv_approx(u); - } - s = (inv - b) / a; - } - lp1 = es_seg_eval_with_offset(es, s, normalized_offset); - } - let l0 = select(lp1, lp0, offset >= 0.); - let l1 = select(lp0, lp1, offset >= 0.); - output_line_with_transform(path_ix, l0, l1, transform); - lp0 = lp1; - } - last_p = this_pq1.point; - last_q = this_pq1.deriv; - last_t = t1; - // Dyadic bookkeeping: advance to the next interval, then merge fully consumed - // sibling intervals back into a coarser dt so the walk stays O(log) deep. - t0_u += 1u; - let shift = countTrailingZeros(t0_u); - t0_u >>= shift; - dt *= f32(1u << shift); - } else { - // Fit error too large for one Euler segment: halve the interval and retry. - t0_u = t0_u * 2u; - dt *= 0.5; - } - } -} - // ---- CPU stroker port ---- // // The stroke geometry below is a direct port of the CPU backend's stroker @@ -826,11 +380,6 @@ struct Transform { perspective: vec3f, } -// Returns the identity transform: unit matrix, zero translation, trivial perspective row. -fn transform_identity() -> Transform { - return Transform(vec4(1., 0., 0., 1.), vec2(0.), vec3(0., 0., 1.)); -} - // Reads transform `ix` from the scene stream: 9 words covering the 2x2 matrix, translation, and // perspective row. Local divergence: Vello stores 6-word affine transforms. fn read_transform(transform_base: u32, ix: u32) -> Transform { @@ -892,87 +441,57 @@ fn compute_tag_monoid(ix: u32) -> PathTagData { return PathTagData(tag_byte, tm); } -// Control points of a cubic Bezier; lines and quads are degree-raised to this common form. -struct CubicPoints { +// Endpoints of one final line or one stroke tangent marker. +struct LinePoints { p0: vec2f, p1: vec2f, - p2: vec2f, - p3: vec2f, } -// Decodes the control points of the segment described by `tag`, reading f32 or packed i16 points -// as indicated by the tag, translating stroke cap markers (quadto-encoded tangents) into lines, -// and degree-raising lines and quads so downstream code handles a single cubic form. -fn read_path_segment(tag: PathTagData, is_stroke: bool) -> CubicPoints { +// Reads one final line. An open-stroke cap marker uses a QuadTo tag only to select its four-word +// data layout. For that marker, the second and third encoded points define the starting tangent. +fn read_path_segment(tag: PathTagData, is_stroke: bool) -> LinePoints { var p0: vec2f; var p1: vec2f; - var p2: vec2f; - var p3: vec2f; - var seg_type = tag.tag_byte & PATH_TAG_SEG_TYPE; + let seg_type = tag.tag_byte & PATH_TAG_SEG_TYPE; let pathseg_offset = tag.monoid.pathseg_offset; let is_stroke_cap_marker = is_stroke && (tag.tag_byte & PATH_TAG_SUBPATH_END) != 0u; let is_open = seg_type == PATH_TAG_QUADTO; + let carries_start_tangent = is_stroke_cap_marker && is_open; if (tag.tag_byte & PATH_TAG_F32) != 0u { p0 = read_f32_point(pathseg_offset); p1 = read_f32_point(pathseg_offset + 2u); - if seg_type >= PATH_TAG_QUADTO { - p2 = read_f32_point(pathseg_offset + 4u); - if seg_type == PATH_TAG_CUBICTO { - p3 = read_f32_point(pathseg_offset + 6u); - } + + if carries_start_tangent { + p0 = p1; + p1 = read_f32_point(pathseg_offset + 4u); } } else { p0 = read_i16_point(pathseg_offset); p1 = read_i16_point(pathseg_offset + 1u); - if seg_type >= PATH_TAG_QUADTO { - p2 = read_i16_point(pathseg_offset + 2u); - if seg_type == PATH_TAG_CUBICTO { - p3 = read_i16_point(pathseg_offset + 3u); - } - } - } - - if is_stroke_cap_marker && is_open { - // The stroke cap marker for an open path is encoded as a quadto where the p1 and p2 store - // the start control point of the subpath and together with p2 forms the start tangent. p0 - // is ignored. - // - // This is encoded this way because encoding this as a lineto would require adding a moveto, - // which would terminate the subpath too early (by setting the SUBPATH_END on the - // segment preceding the cap marker). This scheme is only used for strokes. - p0 = p1; - p1 = p2; - seg_type = PATH_TAG_LINETO; - } - // Degree-raise - if seg_type == PATH_TAG_LINETO { - p3 = p1; - p2 = p3 + (1.0 / 3.0) * (p0 - p3); - p1 = p0 + (1.0 / 3.0) * (p3 - p0); - } else if seg_type == PATH_TAG_QUADTO { - p3 = p2; - p2 = p1 + (1.0 / 3.0) * (p2 - p1); - p1 = p1 + (1.0 / 3.0) * (p0 - p1); + if carries_start_tangent { + p0 = p1; + p1 = read_i16_point(pathseg_offset + 2u); + } } - return CubicPoints(p0, p1, p2, p3); + return LinePoints(p0, p1); } -// Half-width of the band around integer device coordinates that is snapped onto the grid by -// snap_to_pixel_grid. Sub-milli-pixel, so the coverage change is imperceptible, while safely -// exceeding the rounding spread of the flattening and stroking arithmetic. +// Half-width of the band that snap_to_pixel_grid treats as an exact integer coordinate. The value +// is below visible pixel precision but above the floating-point differences created when separate +// stroke operations calculate a shared endpoint. const PIXEL_GRID_SNAP_EPSILON: f32 = 1e-3; // Snaps coordinates lying within PIXEL_GRID_SNAP_EPSILON of an integer device coordinate onto // that integer. The counting stages resolve grid ties with exact comparisons (the boundary // horizontal cull and the start-on-boundary backdrop bump in path_count pair up, as do the // left-edge y_edge assignments in path_tiling); those pairings only hold when every endpoint -// meeting a boundary agrees on whether it lies exactly on it. Flattening and stroking round -// independently per point, so a contour can otherwise reach the boundary with mixed exactness -// and flip the winding of a whole tile row. +// meeting a boundary agrees on whether it lies exactly on it. Separate stroke calculations can +// otherwise produce slightly different values for one shared endpoint and change a tile row's +// winding. fn snap_to_pixel_grid(p: vec2f) -> vec2f { let r = round(p); return select(p, r, abs(p - r) < vec2(PIXEL_GRID_SNAP_EPSILON)); @@ -985,8 +504,9 @@ fn write_line(line_ix: u32, path_ix: u32, p0: vec2f, p1: vec2f) { let s0 = snap_to_pixel_grid(p0); let s1 = snap_to_pixel_grid(p1); bbox = vec4(min(bbox.xy, min(s0, s1)), max(bbox.zw, max(s0, s1))); + if line_ix < config.lines_size { - lines[line_ix] = LineSoup(path_ix, s0, s1); + lines[line_ix] = LineSoup(path_ix, profile_tag, s0, s1); } } @@ -1035,24 +555,23 @@ fn read_neighboring_segment(ix: u32) -> NeighboringSegment { let is_closed = (tag.tag_byte & PATH_TAG_SEG_TYPE) == PATH_TAG_LINETO; let is_stroke_cap_marker = (tag.tag_byte & PATH_TAG_SUBPATH_END) != 0u; let do_join = !is_stroke_cap_marker || is_closed; - var tangent = pts.p3 - pts.p0; - if !is_stroke_cap_marker { - tangent = cubic_start_tangent(pts.p0, pts.p1, pts.p2, pts.p3); - } - return NeighboringSegment(do_join, length(pts.p3 - pts.p0), tangent); + let tangent = pts.p1 - pts.p0; + return NeighboringSegment(do_join, length(tangent), tangent); } // `pathdata_base` is decoded once and reused by helpers above. var pathdata_base: u32; -// This is the bounding box of the shape flattened by a single shader invocation. It gets modified -// during LineSoup generation. +// X and Y profile identifiers for the current fill segment. Stroke segments and fills without +// profile data use the all-ones sentinel. +var profile_tag: u32 = PROFILE_TAG_SENTINEL; + +// Device-space bounds of the lines written by this invocation. var bbox: vec4f; -// Entry point: one invocation per path tag. Decodes the tag's style, transform, and control -// points; records per-path draw metadata on PATH tags; emits fill geometry via flatten_euler or -// stroke outline geometry via the CPU stroker port; then merges this invocation's device-space -// extents into the path's atomic bbox. +// One invocation processes one path tag. A segment tag emits one fill line or one part of a stroke +// outline. A Path tag writes metadata for the path. At the end, each segment invocation merges its +// line bounds into the path's shared bounds. @compute @workgroup_size(256) fn main( @builtin(global_invocation_id) global_id: vec3, @@ -1069,11 +588,10 @@ fn main( let out = &path_bboxes[path_ix]; // Style stream layout (words): 0 flags, 1 line width, 2 draw flags, 3 miter limit, - // 4 arc detail scale, 5 coverage parameter (aliased threshold or antialiased text - // coverage boost; the uses are mutually exclusive), 6..9 interest rect. + // 4 arc detail scale, 5 antialiased coverage adjustment, 6..9 interest rect. let style_flags = scene[config.style_base + style_ix]; let style_draw_flags = scene[config.style_base + style_ix + 2u]; - let coverage_threshold = bitcast(scene[config.style_base + style_ix + 5u]); + let coverage_data = bitcast(scene[config.style_base + style_ix + 5u]); let style_interest = vec4( bitcast(scene[config.style_base + style_ix + 6u]), bitcast(scene[config.style_base + style_ix + 7u]), @@ -1085,7 +603,7 @@ fn main( if (tag.tag_byte & PATH_TAG_PATH) != 0u { (*out).draw_flags = draw_flags; (*out).trans_ix = trans_ix; - (*out).coverage_threshold = coverage_threshold; + (*out).coverage_data = coverage_data; (*out).interest = style_interest; } // Decode path data @@ -1107,7 +625,7 @@ fn main( if is_open { // Start cap. The marker carries the first segment's tangent; the CPU stroker's // caps chain from cap0 to cap1 so they splice into the offset edges. - var tangent = pts.p3 - pts.p0; + var tangent = pts.p1 - pts.p0; if dot(tangent, tangent) < TANGENT_THRESH * TANGENT_THRESH { tangent = vec2(TANGENT_THRESH, 0.); } @@ -1123,7 +641,7 @@ fn main( // offset edges are straight offset lines; joins and caps carry all the curvature. // The encoder collapses micro-segments (1/64 px) exactly like the CPU stroker. let p0 = pts.p0; - let p1 = pts.p3; + let p1 = pts.p1; let seg = p1 - p0; let len1 = length(seg); if len1 >= TANGENT_THRESH { @@ -1154,8 +672,22 @@ fn main( } } } else { - let offset = 0.; - flatten_euler(pts, path_ix, transform, offset, pts.p0, pts.p3); + // Each final fill segment starts at one pair of path-data words. The same pair index + // selects its profile tag. Add pathdata_base because a large scene can dispatch only a + // range of its path tags while all ranges still share one profile-tag table. + if config.profile_slots_base != 0u { + let slot = config.profile_slots_base + ((config.pathdata_base + tag.monoid.pathseg_offset) >> 1u); + profile_tag = scene[slot]; + } + + // Transform the final endpoints directly. Reject a zero-length result before it uses a + // line slot or changes the path bounds. + let p0 = transform_apply(transform, pts.p0); + let p1 = transform_apply(transform, pts.p1); + if any(p0 != p1) { + let line_ix = atomicAdd(&bump.lines, 1u); + write_line(line_ix, path_ix, p0, p1); + } } // Update bounding box using atomics only. Computing a monoid is a // potential future optimization. diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_row_alloc.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_row_alloc.wgsl index 6a3b8266a..7298047ed 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_row_alloc.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_row_alloc.wgsl @@ -20,6 +20,7 @@ #import config #import bump #import drawtag +#import bbox #import tile @group(0) @binding(0) @@ -40,6 +41,11 @@ var paths: array; @group(0) @binding(5) var rows: array; +@group(0) @binding(6) +// Original path bounds and drawing bounds. The reduced draw_bboxes buffer no longer contains the +// unclipped left edge needed to set PATH_FLAGS_CLIPPED_LEFT. +var path_bboxes: array; + // Allocates and initializes the row records for one draw object. NOP and // end-clip objects, and objects with an empty bbox, keep an all-zero tile // bbox and therefore allocate no rows. @@ -53,6 +59,13 @@ fn main( } let drawtag = scene[config.drawtag_base + drawobj_ix]; + let path_bbox = path_bboxes[drawobj_ix]; + // Only aliased fills use the original left edge. Fine needs this distinction for the one-pixel + // extension of an interval that started before the drawing bounds. + let path_flags = select( + 0u, + PATH_FLAGS_CLIPPED_LEFT, + (path_bbox.draw_flags & DRAW_INFO_FLAGS_ALIASED_BIT) != 0u && f32(path_bbox.x0) < path_bbox.interest.x); var ux0 = 0u; var uy0 = 0u; @@ -85,11 +98,11 @@ fn main( // setup stages dispatch zero work, and the CPU resizes and retries. if row_limit_exceeded { atomicOr(&bump.failed, STAGE_TILE_ALLOC); - paths[drawobj_ix] = Path(bbox, 0u); + paths[drawobj_ix] = Path(bbox, 0u, path_flags); return; } - paths[drawobj_ix] = Path(bbox, row_base); + paths[drawobj_ix] = Path(bbox, row_base, path_flags); // Empty-span sentinel: x0 at u32 max and x1 at 0 so the atomicMin/Max // updates in path_row_span establish the true span; a row with diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_row_span.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_row_span.wgsl index 9e427f8e9..8ce99bacf 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_row_span.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_row_span.wgsl @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -// Derives sparse per-row tile spans from the flattened line stream. One +// Derives sparse per-row tile spans from the final line stream. One // thread per line: replays the same tile-crossing traversal as path_count // and, for every tile row the line touches, atomically grows that row's // column span (rows[].x0/x1), accumulates a per-row winding backdrop for @@ -9,8 +9,8 @@ // rows where activity extends past the right bbox edge so tile_alloc can // widen the span to cover the fill interior. // -// Inputs: config uniform, bump (lines counter), lines (LineSoup from -// flatten), paths (Path records from path_row_alloc). +// Inputs: config uniform, bump (lines counter), lines (LineSoup from path +// lowering), paths (Path records from path_row_alloc). // Outputs: rows (AtomicPathRow spans, backdrops and flags), consumed by // tile_alloc. // diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_tiling.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_tiling.wgsl index 65d3ba785..38c3c0d87 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_tiling.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_tiling.wgsl @@ -117,7 +117,6 @@ fn main( let tile_xy = vec2(f32(x) * f32(TILE_WIDTH), f32(y) * f32(TILE_HEIGHT)); let tile_xy1 = tile_xy + vec2(f32(TILE_WIDTH), f32(TILE_HEIGHT)); - // Clip the segment's start to the tile edge it entered through, unless // this is the line's first crossing (the true endpoint is inside). if seg_within_line > 0u { @@ -169,9 +168,12 @@ fn main( p1.x = EPSILON; p1.y = f32(TILE_HEIGHT); } else { - // Make the segment disappear (zero vertical extent). - p1.x = 2.0 * EPSILON; - p1.y = p0.y; + // The owning tile must ignore this boundary edge, but the aliased row walk in + // the tile on the left still needs its exact crossing to match the CPU's single + // crossing list for the full row. Preserve the vertical extent under an explicit + // halo-only marker; ordinary fine integration continues to treat it as absent. + p1.x = EPSILON; + y_edge = HALO_ONLY_Y_EDGE; } } else if p0.y == 0.0 { p0.x = EPSILON; @@ -201,6 +203,6 @@ fn main( p0 = p1; p1 = tmp; } - let segment = Segment(p0, p1, y_edge); + let segment = Segment(p0, p1, y_edge, line.tag); segments[seg_start + seg_within_slice] = segment; } diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/pathtag_scan.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/pathtag_scan.wgsl index f71780bd2..b232f83b7 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/pathtag_scan.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/pathtag_scan.wgsl @@ -3,7 +3,7 @@ // Final stage of the path tag monoid scan: writes, for every 4-byte tag // word, the exclusive prefix TagMonoid (counts of transforms, segments, -// segment data words, styles and paths preceding that word). Flatten uses +// segment data words, styles and paths preceding that word). Path lowering uses // these prefixes to locate each path element's data in the scene buffer. // // Two compile-time variants exist, selected by the "small" define: diff --git a/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md b/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md index f6918d339..de69439c5 100644 --- a/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md +++ b/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md @@ -6,7 +6,7 @@ The WebGPU documentation is split into two newcomer-first documents: Explains how `WebGPUEnvironment`, the public target types, and `WebGPUDrawingBackend` fit together, how retained scene creation reaches the GPU path (including the parallel and ordered encoders), where explicit support probing fits, how explicit layers and ordered Apply/scoped-layer scenes execute, how the scratch-growth retry loop and typed readback work, and how runtime/device-scoped state relates to flush-scoped work. - [`WEBGPU_RASTERIZER.md`](d:/GitHub/SixLabors/ImageSharp.Drawing/src/ImageSharp.Drawing.WebGPU/WEBGPU_RASTERIZER.md) - Explains the staged scene pipeline itself: scene encoding (including parallel partitioned encoding), packed scene format types, planning, resource creation, scheduling passes, GPU stroking in the flatten stage, fine rasterization, the scratch overflow protocol, chunked oversized-scene execution, and submission. + Explains the staged scene pipeline. Topics include parallel scene encoding, packed formats, planning, resources, path lowering, rasterization, retries, chunking, and submission. If you are new to the GPU path, read them in this order: diff --git a/src/ImageSharp.Drawing.WebGPU/WEBGPU_RASTERIZER.md b/src/ImageSharp.Drawing.WebGPU/WEBGPU_RASTERIZER.md index c30d3c55c..a52d7300b 100644 --- a/src/ImageSharp.Drawing.WebGPU/WEBGPU_RASTERIZER.md +++ b/src/ImageSharp.Drawing.WebGPU/WEBGPU_RASTERIZER.md @@ -149,7 +149,7 @@ The encoder first builds several logical streams such as: Those streams are then packed into the final scene word buffer plus separate gradient and image payloads. -The draw-tag words and draw-info flag bits are defined by `GpuSceneDrawTag` and must match `Shaders/WgslSource/Shared/drawtag.wgsl`. The encoder chooses which tag or flag to write; the format type owns the numeric shader contract. The clip mask bits (`CLIP_DIFFERENCE_MASK_BIT`, `CLIP_HARD_MASK_BIT`) are declared once in `drawtag.wgsl` and set by the encoder in the high bits of the clip blend word. +The draw-tag words and draw-info flag bits are defined by `GpuSceneDrawTag` and must match `Shaders/WgslSource/Shared/drawtag.wgsl`. The encoder chooses which tag or flag to write; the format type owns the numeric shader contract. The local clip bits (`CLIP_DIFFERENCE_MASK_BIT`, `CLIP_ISOLATED_MASK_BIT`) are declared once in `drawtag.wgsl` and set by the encoder in the high bits of the clip blend word. Explicit layers are part of this encoding step too. `BeginLayer` and `EndLayer` stay in the prepared command stream until `WebGPUSceneEncoder` lowers them into `BeginClip` and `EndClip` draw records inside the encoded scene. @@ -157,11 +157,11 @@ The stream split matters because the shaders consume offsets into one shared pac Three geometry rules the encoder applies matter downstream: -- fill geometry is pre-flattened on the CPU, so only line segments reach the flatten stage for fills; curve handling on the GPU exists but is not exercised by fills in practice -- stroke geometry is not expanded on the CPU. The encoder emits the stroke's segment list after collapsing micro-segments shorter than 1/64 px (mirroring the CPU stroker's preprocessing), and reuses quad-to path tags as stroke tangent markers rather than curves; the GPU stroker in `flatten.wgsl` does the expansion -- clip paths carry a full-target raster interest rectangle; a clipped interest would make binning intersect the clip coverage away +- the CPU pre-flattens fill geometry. The path-lowering stage transforms and emits only final fill lines. It does not subdivide fill curves +- the CPU does not expand stroke geometry. The encoder collapses micro-segments shorter than 1/64 px, matching the CPU stroker. Quad-to path tags carry stroke tangents, not curves. The GPU stroker in `path_lowering.wgsl` expands the stroke geometry +- clip paths carry a full-target raster interest rectangle. A clipped interest rectangle makes binning remove the clip coverage -Per-draw rasterization state is also encoded here: each visible fill carries its own coverage threshold and raster interest rectangle, and an aliased fill sets `DRAW_INFO_FLAGS_ALIASED_BIT` in its draw flags. Antialiased and aliased fills therefore coexist in one scene. +Per-draw rasterization state is also encoded here: each visible fill carries its raster interest rectangle, and an aliased fill sets `DRAW_INFO_FLAGS_ALIASED_BIT` in its draw flags. Antialiased coverage data contains only the optional text coverage boost. Antialiased and aliased fills therefore coexist in one scene. ### Parallel Encoding @@ -236,7 +236,7 @@ Their purpose is structural. They: - reset the GPU bump allocators (prepare) - scan the packed path and draw streams -- flatten path segments into a device-space line soup, expanding strokes on the GPU +- lower path segments into a device-space line soup, expanding strokes on the GPU - build path and clip metadata - bin work into tiles - allocate sparse per-path row metadata from clipped draw bounds @@ -254,7 +254,7 @@ flowchart TD B --> C[PathtagScan1 if needed] C --> D[PathtagScan] D --> E[BboxClear] - E --> F[Flatten] + E --> F[Path Lowering] F --> G[DrawReduce] G --> H[DrawLeaf] H --> I[ClipReduce if needed] @@ -274,15 +274,15 @@ flowchart TD A few stage details worth knowing: - `prepare.wgsl` binds only the bump buffer. It zeroes every bump-allocator counter and the failure mask on the GPU, and the pipeline never cancels mid-flush: all stages run so the counters report the true demand for every scratch buffer in a single pass -- the pathtag scan computes a prefix sum of a four-word `TagMonoid` (`trans_ix`, `pathseg_offset`, `style_ix`, `path_ix`) so flatten can locate each segment's points, transform, and style in O(1) -- `bbox_clear` is dispatched over the scene's path count and resets each atomic path bbox to an inverted empty state before flatten accumulates into it +- the pathtag scan computes a prefix sum of a four-word `TagMonoid` (`trans_ix`, `pathseg_offset`, `style_ix`, `path_ix`) so path lowering can locate each segment's points, transform, and style in O(1) +- `bbox_clear` is dispatched over the scene's path count and resets each atomic path bbox to an inverted empty state before path lowering expands it - `clip_reduce`/`clip_leaf` are skipped entirely when the scene has no clip records, and `clip_reduce` is skipped when the clip stream fits one 256-element partition -### Flatten: Fills And GPU Stroking +### Path Lowering: Fill Lines And GPU Stroking -`flatten.wgsl` converts encoded path segments into the device-space line soup consumed by the later stages, and it is where strokes become geometry. +`path_lowering.wgsl` converts encoded path segments into the device-space line soup consumed by the later stages, and it is where strokes become geometry. -For fills, every segment type routes through `flatten_euler` exactly as in upstream Vello: `read_path_segment` lowers lines to degenerate cubics and the Euler-spiral math resolves them through its low-curvature path. Because the C# encoder pre-flattens fill curves on the CPU, only line segments arrive for fills in practice. +For fills, the C# encoder supplies only final line segments. The shader reads each endpoint pair, applies the device transform, rejects zero-length results, and writes the line directly. This stage contains no curve subdivision or degree raising. Stroke expansion diverges from upstream Vello. It is a direct port of the CPU `PolygonStroker`: `stroke_chain_point` walks the offset chain, `stroke_side_join` dispatches per-join handling, and `stroke_calc_miter` and `stroke_calc_arc` produce miters and round joins/caps. Caps, joins, and arcs are all generated on the GPU. The encoder supports this with two conventions: micro-segments shorter than 1/64 px are collapsed CPU-side before encoding (matching the CPU stroker's preprocessing), and quad-to path tags in a stroke are tangent markers for the stroker, not curve segments. Stroking never falls back to the CPU. @@ -291,17 +291,16 @@ Stroke expansion diverges from upstream Vello. It is a direct port of the CPU `P The fine pass is where the scheduled scene becomes final pixel writes. A single fine shader (`FineAreaComputeShader`) handles every flush; its pipeline is cached per -output texture format. It computes analytic area coverage and, per fill, quantizes that -coverage against the fill's own threshold when the fill carries the aliased bit in -`CmdFill.size_and_rule`. The per-fill threshold travels in the `CmdFill` command itself and -overrides the scene-wide `config.fine_coverage_threshold` fallback. Antialiased and aliased -fills therefore coexist within one flush, matching the CPU rasterizer's per-fill -`RasterizationMode`. Each fill also carries a raster interest rectangle; coverage outside it -is zeroed. - -For antialiased fills the `CmdFill.coverage_threshold` word is reused to carry the perceptual -coverage boost for text (the two uses are mutually exclusive, so no extra command word is -needed). When non-zero, fine remaps partial coverage with the S-curve +output texture format. Antialiased fills use analytic area coverage. Aliased fills walk sorted +crossings at pixel row and column centres. If a closed interval contains no centre, they light its +midpoint pixel unless the two boundary profiles show that the interval ends at a contour tip. A +second, vertical walk finds horizontal features between row centres. The aliased bit in +`CmdFill.size_and_rule` selects the coverage path. Each fill also carries a raster interest +rectangle; coverage outside it is zeroed. + +For antialiased fills the `CmdFill.coverage_data` word carries the perceptual coverage boost +for text. Aliased fills leave that style value at zero because their command word is used for +tile-neighbour data. When non-zero, fine remaps partial coverage with the S-curve `f(a) = a + boost * a * (1 - a) * (2a - 1)`, equivalently a blend `(1 - boost) * a + boost * smoothstep(a)`: coverage above one half darkens and coverage below it lightens, so stems solidify while counters stay bright. The remap is monotone and range @@ -330,7 +329,7 @@ gradient extend behavior; the recolor threshold is pre-transformed by the encode (`Threshold * 4`) into the shader's squared-color-distance domain so the shader compares distances directly. -That is also where explicit layers are composited. The fine shader handles `BeginClip` and `EndClip` records inline by saving the current tile color, rendering the isolated layer contents, and then blending that isolated result back into the saved backdrop with the layer's stored blend mode and alpha. The clip blend word's high bits select clip variants: `CLIP_DIFFERENCE_MASK_BIT` inverts the mask coverage for Difference clips and `CLIP_HARD_MASK_BIT` marks a hard-edge (aliased) clip mask. +That is also where explicit layers are composited. The fine shader handles `BeginClip` and `EndClip` records inline by saving the current tile color, rendering the isolated layer contents, and then blending that isolated result back into the saved backdrop with the layer's stored blend mode and alpha. `CLIP_DIFFERENCE_MASK_BIT` inverts the clip coverage. Hard clips use the same aliased centre-sampling path as other aliased fills, so the clip pop consumes their binary mask directly. ## Stage 7: Readback, Copy, And Submit @@ -362,7 +361,7 @@ The important design point is that chunking does not re-clip or re-encode the sc The dispatch layer executes the staged pipeline in chunk-local tile-row windows so each chunk stays within device limits while still using the same encoded scene. The mechanics are: - the output texture is seeded with the current target contents first, so pixels no chunk writes keep their original values when the full rectangle is copied back -- the chunk-invariant stages (prepare through binning) run exactly once per flush; each chunk then replays only the chunk-local stages, with `chunk_reset` clearing the chunk-local bump counters while preserving the shared flatten/binning state +- the chunk-invariant stages (prepare through binning) run exactly once per flush; each chunk then replays only the chunk-local stages, with `chunk_reset` clearing the chunk-local bump counters while preserving the shared path-lowering/binning state - each chunk window is validated against the binding limits before dispatch; if a chunk's buffers still exceed the limit the window shrinks and validation repeats - a chunk attempt that fails after passing binding validation fails the flush, because shrinking cannot cure it and retrying would re-record the identical chunk forever - every chunk copies its bump-allocator status into a distinct offset of one readback buffer; all chunks are checked in a single batch readback at the end, and any overflow feeds the same grow-and-retry loop as the monolithic path diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneConfig.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneConfig.cs index 863509921..8a448a187 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneConfig.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneConfig.cs @@ -170,7 +170,7 @@ internal readonly struct WebGPUSceneBumpSizes /// /// Initializes a new instance of the struct. /// - /// The flattened line buffer capacity. + /// The final line buffer capacity. /// The bin-data scratch capacity. /// The sparse path-row buffer capacity. /// The path-tile buffer capacity. @@ -199,7 +199,7 @@ public WebGPUSceneBumpSizes( } /// - /// Gets the flattened line buffer capacity. + /// Gets the final line buffer capacity. /// public uint Lines { get; } @@ -275,7 +275,7 @@ internal readonly struct WebGPUSceneWorkgroupCounts /// The workgroup count for the large pathtag scan setup pass. /// The workgroup count for the final pathtag scan pass. /// The workgroup count for the bbox clear pass. - /// The workgroup count for the flatten pass. + /// The workgroup count for the path-lowering pass. /// The workgroup count for the draw reduction pass. /// The workgroup count for the draw leaf pass. /// The workgroup count for the clip reduction pass. @@ -300,7 +300,7 @@ public WebGPUSceneWorkgroupCounts( uint pathScan1X, uint pathScanX, uint bboxClearX, - uint flattenX, + uint pathLoweringX, uint drawReduceX, uint drawLeafX, uint clipReduceX, @@ -325,7 +325,7 @@ public WebGPUSceneWorkgroupCounts( this.PathScan1X = pathScan1X; this.PathScanX = pathScanX; this.BboxClearX = bboxClearX; - this.FlattenX = flattenX; + this.PathLoweringX = pathLoweringX; this.DrawReduceX = drawReduceX; this.DrawLeafX = drawLeafX; this.ClipReduceX = clipReduceX; @@ -376,9 +376,9 @@ public WebGPUSceneWorkgroupCounts( public uint BboxClearX { get; } /// - /// Gets the workgroup count for the flatten pass. + /// Gets the workgroup count for the path-lowering pass. /// - public uint FlattenX { get; } + public uint PathLoweringX { get; } /// /// Gets the workgroup count for the draw reduction pass. @@ -493,7 +493,7 @@ public static WebGPUSceneWorkgroupCounts Create(WebGPUEncodedScene scene, WebGPU // draw_leaf.wgsl distributes all draw blocks over at most 256 workgroups // (each loops over its share of blocks), so reduce/leaf dispatches cap at 256. uint drawMonoidWgs = Math.Min(drawObjectWgs, 256U); - uint flattenWgs = DivideRoundUp(pathTagCount, 256U); + uint pathLoweringWgs = DivideRoundUp(pathTagCount, 256U); // Vello's sizing: clip_leaf processes the final (possibly partial) partition // itself, so clip_reduce only covers the full partitions preceding it. @@ -518,7 +518,7 @@ public static WebGPUSceneWorkgroupCounts Create(WebGPUEncodedScene scene, WebGPU reducedSize / 256U, pathTagWgs, pathWgs, - flattenWgs, + pathLoweringWgs, drawMonoidWgs, drawMonoidWgs, clipReduceWgs, @@ -560,7 +560,7 @@ public static WebGPUSceneWorkgroupCounts Create(WebGPUSceneRange range, WebGPUSc uint reducedSize = useLargePathScan ? AlignUp(pathTagWgs, 256U) : pathTagWgs; uint drawObjectWgs = DivideRoundUp(drawObjectCount, 256U); uint drawMonoidWgs = Math.Min(drawObjectWgs, 256U); - uint flattenWgs = DivideRoundUp(pathTagCount, 256U); + uint pathLoweringWgs = DivideRoundUp(pathTagCount, 256U); uint clipReduceWgs = clipCount == 0U ? 0U : (clipCount - 1U) / 256U; uint clipWgs = DivideRoundUp(clipCount, 256U); uint pathWgs = DivideRoundUp(pathCount, 256U); @@ -579,7 +579,7 @@ public static WebGPUSceneWorkgroupCounts Create(WebGPUSceneRange range, WebGPUSc reducedSize / 256U, pathTagWgs, pathWgs, - flattenWgs, + pathLoweringWgs, drawMonoidWgs, drawMonoidWgs, clipReduceWgs, @@ -643,7 +643,7 @@ internal readonly struct WebGPUSceneBufferSizes /// The draw bounding-box buffer size. /// The bump allocator block size. /// The per-path scheduling buffer size. - /// The flattened line buffer size. + /// The final line buffer size. /// The bin-header buffer size. /// The bin-data scratch buffer size. /// The indirect dispatch-count buffer size. @@ -783,7 +783,7 @@ public WebGPUSceneBufferSizes( public WebGPUSceneBufferSize Paths { get; } /// - /// Gets the size of the flattened line buffer. + /// Gets the size of the final line buffer. /// public WebGPUSceneBufferSize Lines { get; } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneDispatch.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneDispatch.cs index 55a2aa98d..5b9047500 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneDispatch.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneDispatch.cs @@ -18,7 +18,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// /// The stage order is fixed by data dependencies: prepare (bump reset), pathtag reduce/scan, -/// bbox_clear, flatten, draw reduce/leaf, clip reduce/leaf, binning, then the tile stages +/// bbox_clear, path lowering, draw reduce/leaf, clip reduce/leaf, binning, then the tile stages /// (path_row_alloc, path_row_span, tile_alloc, path_count, backdrop, coarse, path_tiling) and /// finally the fine shading pass. Scratch buffers use GPU bump allocators sized from cached /// estimates; after each attempt the allocator counters are read back and, on overflow, the caller @@ -45,7 +45,7 @@ internal static class WebGPUSceneDispatch private const string PathtagScanPipelineKey = "scene/pathtag-scan"; private const string PathtagScanSmallPipelineKey = "scene/pathtag-scan-small"; private const string BboxClearPipelineKey = "scene/bbox-clear"; - private const string FlattenPipelineKey = "scene/flatten"; + private const string PathLoweringPipelineKey = "scene/path-lowering"; private const string DrawReducePipelineKey = "scene/draw-reduce"; private const string DrawLeafPipelineKey = "scene/draw-leaf"; private const string ClipReducePipelineKey = "scene/clip-reduce"; @@ -648,7 +648,6 @@ private static unsafe bool TryDispatchSharedSchedulingStages( nuint clipBboxBufferSize = bufferSizes.ClipBboxes.ByteLength; nuint drawBboxBufferSize = bufferSizes.DrawBboxes.ByteLength; nuint lineBufferSize = bufferSizes.Lines.ByteLength; - nuint binHeaderBufferSize = bufferSizes.BinHeaders.ByteLength; if (GetRenderableDrawCount(stagedScene) == 0) { error = null; @@ -661,12 +660,10 @@ private static unsafe bool TryDispatchSharedSchedulingStages( return false; } - WGPUBufferImpl* binHeaderBuffer = arena.BinHeaderBuffer; WGPUBufferImpl* bumpBuffer = arena.BumpBuffer; WebGPUSceneResourceRegistry resourceRegistry = WebGPUSceneResourceRegistry.Create(stagedScene.Resources); resourceRegistry.RegisterSchedulingBuffers( - binHeaderBuffer, arena.IndirectCountBuffer, arena.PathRowBuffer, arena.PathTileBuffer, @@ -758,7 +755,7 @@ private static unsafe bool TryDispatchSharedSchedulingStages( return false; } - // Flatten accumulates path bboxes with atomic min/max, so every box must be reset to an + // Path lowering accumulates path bboxes with atomic min/max, so every box must be reset to an // inverted empty state first. if (!TryDispatchBboxClear( recording, @@ -771,7 +768,7 @@ private static unsafe bool TryDispatchSharedSchedulingStages( return false; } - if (!TryDispatchFlatten( + if (!TryDispatchPathLowering( recording, flushContext, stagedScene.Resources, @@ -780,7 +777,7 @@ private static unsafe bool TryDispatchSharedSchedulingStages( pathBboxBufferSize, bumpBuffer, lineBufferSize, - workgroupCounts.FlattenX, + workgroupCounts.PathLoweringX, out error)) { return false; @@ -904,7 +901,6 @@ private static unsafe bool TryDispatchChunkLocalSchedulingStages( nuint drawBboxBufferSize = bufferSizes.DrawBboxes.ByteLength; nuint pathBufferSize = bufferSizes.Paths.ByteLength; nuint lineBufferSize = bufferSizes.Lines.ByteLength; - nuint binHeaderBufferSize = bufferSizes.BinHeaders.ByteLength; nuint pathRowBufferSize = bufferSizes.PathRows.ByteLength; nuint pathTileBufferSize = bufferSizes.PathTiles.ByteLength; nuint segCountBufferSize = bufferSizes.SegCounts.ByteLength; @@ -922,7 +918,6 @@ private static unsafe bool TryDispatchChunkLocalSchedulingStages( return false; } - WGPUBufferImpl* binHeaderBuffer = arena.BinHeaderBuffer; WGPUBufferImpl* indirectCountBuffer = arena.IndirectCountBuffer; WGPUBufferImpl* pathRowBuffer = arena.PathRowBuffer; WGPUBufferImpl* pathTileBuffer = arena.PathTileBuffer; @@ -934,7 +929,6 @@ private static unsafe bool TryDispatchChunkLocalSchedulingStages( WebGPUSceneResourceRegistry resourceRegistry = WebGPUSceneResourceRegistry.Create(stagedScene.Resources); resourceRegistry.RegisterSchedulingBuffers( - binHeaderBuffer, indirectCountBuffer, pathRowBuffer, pathTileBuffer, @@ -946,7 +940,7 @@ private static unsafe bool TryDispatchChunkLocalSchedulingStages( WebGPUSceneComputeRecording recording = new(resourceRegistry); - // chunk_reset clears only the chunk-local bump counters; the shared flatten/binning + // chunk_reset clears only the chunk-local bump counters; the shared path-lowering/binning // counters and failure bits must survive because those stages are not rerun per window. if (resetChunkLocalBumpAllocators && !TryDispatchChunkReset(recording, bumpBuffer, ChunkResetComputeShader.GetDispatchX(), out error)) @@ -960,6 +954,7 @@ private static unsafe bool TryDispatchChunkLocalSchedulingStages( stagedScene.Resources, sceneBufferSize, drawBboxBufferSize, + pathBboxBufferSize, pathBufferSize, pathRowBuffer, pathRowBufferSize, @@ -970,7 +965,7 @@ private static unsafe bool TryDispatchChunkLocalSchedulingStages( return false; } - // path_count_setup reads only bump.lines, which the shared flatten pass has already + // path_count_setup reads only bump.lines, which the shared path-lowering pass has already // produced, so it can run here even though path_count itself comes later. The same // indirect argument buffer drives both per-line dispatches (path_row_span and path_count) // because they use the same workgroup size. @@ -1120,7 +1115,6 @@ private static unsafe bool TryDispatchChunkLocalSchedulingStages( } scheduling = new WebGPUSceneSchedulingResources( - binHeaderBuffer, indirectCountBuffer, pathRowBuffer, pathTileBuffer, @@ -1241,6 +1235,7 @@ public static unsafe bool TrySubmitTransactionalStagedScene( flushContext, stagedScene.Resources, stagedScene.Config.BufferSizes, + GetBindingByteLength(stagedScene.EncodedScene.SceneWordCount), scheduling, outputTextureView, backdropTarget.TextureView, @@ -1439,7 +1434,6 @@ private static unsafe WebGPUSceneSchedulingArena CreateSchedulingArena( WebGPUSceneBufferSizes bufferSizes, nuint readbackByteLength) { - WGPUBufferImpl* binHeaderBuffer = null; WGPUBufferImpl* indirectCountBuffer = null; WGPUBufferImpl* pathRowBuffer = null; WGPUBufferImpl* pathTileBuffer = null; @@ -1451,7 +1445,6 @@ private static unsafe WebGPUSceneSchedulingArena CreateSchedulingArena( try { - binHeaderBuffer = CreateArenaStorageBuffer(flushContext, bufferSizes.BinHeaders.ByteLength); indirectCountBuffer = CreateArenaIndirectStorageBuffer(flushContext, bufferSizes.IndirectCount.ByteLength); pathRowBuffer = CreateArenaStorageBuffer(flushContext, bufferSizes.PathRows.ByteLength); pathTileBuffer = CreateArenaStorageBuffer(flushContext, bufferSizes.PathTiles.ByteLength); @@ -1481,7 +1474,6 @@ private static unsafe WebGPUSceneSchedulingArena CreateSchedulingArena( flushContext.DeviceHandle, bufferSizes, readbackByteLength, - binHeaderBuffer, indirectCountBuffer, pathRowBuffer, pathTileBuffer, @@ -1497,7 +1489,6 @@ private static unsafe WebGPUSceneSchedulingArena CreateSchedulingArena( // Nothing owns these buffers until the arena is constructed; a throw mid-sequence // (a closed device handle, checked-size overflow) must release the ones created. WebGPU api = flushContext.Api; - WebGPUSceneSchedulingArena.ReleaseArenaBuffer(api, binHeaderBuffer); WebGPUSceneSchedulingArena.ReleaseArenaBuffer(api, indirectCountBuffer); WebGPUSceneSchedulingArena.ReleaseArenaBuffer(api, pathRowBuffer); WebGPUSceneSchedulingArena.ReleaseArenaBuffer(api, pathTileBuffer); @@ -1611,7 +1602,6 @@ public static unsafe bool TryRenderStagedScene( pendingStatus = null; error = null; - WebGPUEncodedScene encodedScene = stagedScene.EncodedScene; if (GetRenderableDrawCount(stagedScene) == 0) { return true; @@ -1665,6 +1655,7 @@ public static unsafe bool TryRenderStagedScene( flushContext, stagedScene.Resources, stagedScene.Config.BufferSizes, + GetBindingByteLength(stagedScene.EncodedScene.SceneWordCount), scheduling, outputTextureView, backdropTarget.TextureView, @@ -2134,6 +2125,7 @@ private static unsafe bool TryRenderChunkAttempt( stagedScene.FlushContext, stagedScene.Resources, stagedScene.Config.BufferSizes, + GetBindingByteLength(stagedScene.EncodedScene.SceneWordCount), scheduling, outputTextureView, backdropTarget.TextureView, @@ -2196,6 +2188,7 @@ private static unsafe bool TryRecordChunkAttempt( stagedScene.FlushContext, stagedScene.Resources, stagedScene.Config.BufferSizes, + GetBindingByteLength(stagedScene.EncodedScene.SceneWordCount), scheduling, outputTextureView, backdropTarget.TextureView, @@ -2832,7 +2825,7 @@ private static unsafe bool TryDispatchPathtagScan( } /// - /// Records the pass that resets every per-path bbox to an inverted empty box so flatten can + /// Records the pass that resets every per-path bbox to an inverted empty box so path lowering can /// accumulate extents with atomic min/max. /// /// The compute recording that receives the staged dispatch. @@ -2858,7 +2851,7 @@ private static unsafe bool TryDispatchBboxClear( } /// - /// Records the flatten stage that converts encoded path segments into the device-space line + /// Records the path-lowering stage that converts encoded path segments into the device-space line /// soup and accumulates per-path bboxes, bump-allocating lines from the shared counters. /// /// The compute recording that receives the staged dispatch. @@ -2868,11 +2861,11 @@ private static unsafe bool TryDispatchBboxClear( /// The byte length of the pathtag monoid buffer binding. /// The byte length of the per-path bbox buffer binding. /// The scheduling bump-allocator buffer that tracks line usage. - /// The byte length of the flattened line buffer binding. + /// The byte length of the final line buffer binding. /// The X workgroup count for the stage. /// Receives the recording failure reason when the dispatch cannot be staged. /// when the dispatch was recorded successfully; otherwise, . - private static unsafe bool TryDispatchFlatten( + private static unsafe bool TryDispatchPathLowering( WebGPUSceneComputeRecording recording, WebGPUFlushContext flushContext, WebGPUSceneResourceSet resources, @@ -2892,7 +2885,7 @@ private static unsafe bool TryDispatchFlatten( entries[4] = CreateBufferBinding(4, bumpBuffer, (nuint)sizeof(GpuSceneBumpAllocators)); entries[5] = CreateBufferBinding(5, resources.LineBuffer, lineBufferSize); - return recording.TryRecord(WebGPUSceneShaderId.Flatten, entries, 6, dispatchX, 1, 1, out error); + return recording.TryRecord(WebGPUSceneShaderId.PathLowering, entries, 6, dispatchX, 1, 1, out error); } /// @@ -3530,6 +3523,7 @@ private static uint ScaleChunkRequirementToSceneBudget(uint chunkRequired, uint /// The staged-scene resource set that provides the scene, draw-bbox, and path buffers. /// The byte length of the packed scene buffer binding. /// The byte length of the draw-bbox buffer binding. + /// The byte length of the original path-bbox buffer binding. /// The byte length of the per-path buffer binding. /// The sparse path-row buffer to populate. /// The byte length of the sparse path-row buffer binding. @@ -3543,6 +3537,7 @@ private static unsafe bool TryDispatchPathRowAlloc( WebGPUSceneResourceSet resources, nuint sceneBufferSize, nuint drawBboxBufferSize, + nuint pathBboxBufferSize, nuint pathBufferSize, WGPUBufferImpl* pathRowBuffer, nuint pathRowBufferSize, @@ -3550,15 +3545,16 @@ private static unsafe bool TryDispatchPathRowAlloc( uint dispatchX, out string? error) { - WGPUBindGroupEntry* entries = stackalloc WGPUBindGroupEntry[6]; + WGPUBindGroupEntry* entries = stackalloc WGPUBindGroupEntry[7]; entries[0] = CreateBufferBinding(0, resources.HeaderBuffer, (nuint)sizeof(GpuSceneConfig)); entries[1] = CreateBufferBinding(1, resources.SceneBuffer, sceneBufferSize); entries[2] = CreateBufferBinding(2, resources.DrawBboxBuffer, drawBboxBufferSize); entries[3] = CreateBufferBinding(3, bumpBuffer, (nuint)sizeof(GpuSceneBumpAllocators)); entries[4] = CreateBufferBinding(4, resources.PathBuffer, pathBufferSize); entries[5] = CreateBufferBinding(5, pathRowBuffer, pathRowBufferSize); + entries[6] = CreateBufferBinding(6, resources.PathBboxBuffer, pathBboxBufferSize); - if (!recording.TryRecord(WebGPUSceneShaderId.PathRowAlloc, entries, 6, dispatchX, 1, 1, out error)) + if (!recording.TryRecord(WebGPUSceneShaderId.PathRowAlloc, entries, 7, dispatchX, 1, 1, out error)) { return false; } @@ -3577,7 +3573,7 @@ private static unsafe bool TryDispatchPathRowAlloc( /// The byte length of the per-path buffer binding. /// The sparse path-row buffer to update. /// The byte length of the sparse path-row buffer binding. - /// The byte length of the flattened line buffer binding. + /// The byte length of the final line buffer binding. /// The indirect-dispatch argument buffer seeded from the line count. /// Receives the recording failure reason when the dispatch cannot be staged. /// when the dispatch was recorded successfully; otherwise, . @@ -3700,7 +3696,7 @@ private static unsafe bool TryDispatchBackdrop( } /// - /// Records the one-workgroup pass that converts the flattened line count into indirect + /// Records the one-workgroup pass that converts the final line count into indirect /// dispatch arguments for the per-line stages, or zeroes the dispatch after an upstream failure. /// /// The compute recording that receives the staged dispatch. @@ -3804,7 +3800,7 @@ private static unsafe bool TryDispatchChunkReset( /// The byte length of the tile buffer binding. /// The segment-count buffer receiving one record per crossing. /// The byte length of the segment-count buffer binding. - /// The byte length of the flattened line buffer binding. + /// The byte length of the final line buffer binding. /// The indirect-dispatch argument buffer seeded by path_count_setup. /// Receives the recording failure reason when the dispatch cannot be staged. /// when the dispatch was recorded successfully; otherwise, . @@ -3945,7 +3941,7 @@ private static unsafe bool TryDispatchPathTilingSetup( } /// - /// Records the per-crossing stage that clips each flattened line to its tile and writes the + /// Records the per-crossing stage that clips each final line to its tile and writes the /// final tile-relative segments consumed by the fine pass. /// /// The compute recording that receives the staged dispatch. @@ -3954,7 +3950,7 @@ private static unsafe bool TryDispatchPathTilingSetup( /// The scheduling bump-allocator buffer providing the segment-count total. /// The segment-count records emitted by path_count. /// The byte length of the segment-count buffer binding. - /// The byte length of the flattened line buffer binding. + /// The byte length of the final line buffer binding. /// The byte length of the per-path buffer binding. /// The sparse path-row buffer used to resolve tile indices. /// The byte length of the sparse path-row buffer binding. @@ -4012,6 +4008,7 @@ private static unsafe bool TryDispatchPathTiling( /// The flush context that owns the device, encoder, and texture format. /// The staged-scene resource set that provides the header, info, and texture bindings. /// The buffer plan providing the byte length of each scheduling binding. + /// The byte length of the packed scene buffer binding backing the profile records. /// The transient scheduling buffers produced by the earlier stages. /// The storage texture view receiving the shaded output. /// The texture view holding the existing target contents sampled as the backdrop. @@ -4023,6 +4020,7 @@ private static unsafe bool TryDispatchFineArea( WebGPUFlushContext flushContext, WebGPUSceneResourceSet resources, WebGPUSceneBufferSizes bufferSizes, + nuint sceneBufferSize, WebGPUSceneSchedulingResources scheduling, WGPUTextureViewImpl* outputTextureView, WGPUTextureViewImpl* backdropTextureView, @@ -4056,7 +4054,7 @@ bool LayoutFactory(WebGPU api, WGPUDeviceImpl* device, out WGPUBindGroupLayoutIm return false; } - WGPUBindGroupEntry* entries = stackalloc WGPUBindGroupEntry[9]; + WGPUBindGroupEntry* entries = stackalloc WGPUBindGroupEntry[11]; entries[0] = CreateBufferBinding(0, resources.HeaderBuffer, (nuint)sizeof(GpuSceneConfig)); entries[1] = CreateBufferBinding(1, scheduling.SegmentBuffer, bufferSizes.Segments.ByteLength); entries[2] = CreateBufferBinding(2, scheduling.PtclBuffer, bufferSizes.Ptcl.ByteLength); @@ -4067,8 +4065,10 @@ bool LayoutFactory(WebGPU api, WGPUDeviceImpl* device, out WGPUBindGroupLayoutIm entries[7] = new WGPUBindGroupEntry { binding = 7, textureView = resources.ImageAtlasTextureView }; entries[8] = new WGPUBindGroupEntry { binding = 8, textureView = backdropTextureView }; + entries[9] = CreateBufferBinding(9, resources.SceneBuffer, sceneBufferSize); + entries[10] = CreateBufferBinding(10, scheduling.PathTileBuffer, bufferSizes.PathTiles.ByteLength); - if (!TryDispatchComputePass(flushContext, bindGroupLayout, pipeline, entries, 9, groupCountX, groupCountY, 1, out error)) + if (!TryDispatchComputePass(flushContext, bindGroupLayout, pipeline, entries, 11, groupCountX, groupCountY, 1, out error)) { return false; } @@ -4265,7 +4265,7 @@ private static string GetShaderDebugName(WebGPUSceneShaderId shaderId) WebGPUSceneShaderId.PathtagScan => "pathtag_scan", WebGPUSceneShaderId.PathtagScanSmall => "pathtag_scan_small", WebGPUSceneShaderId.BboxClear => "bbox_clear", - WebGPUSceneShaderId.Flatten => "flatten", + WebGPUSceneShaderId.PathLowering => "path_lowering", WebGPUSceneShaderId.DrawReduce => "draw_reduce", WebGPUSceneShaderId.DrawLeaf => "draw_leaf", WebGPUSceneShaderId.ClipReduce => "clip_reduce", @@ -4312,7 +4312,7 @@ bool LayoutFactory(WebGPU api, WGPUDeviceImpl* device, out WGPUBindGroupLayoutIm WebGPUSceneShaderId.PathtagScan1 => PathtagScan1ComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError), WebGPUSceneShaderId.PathtagScan or WebGPUSceneShaderId.PathtagScanSmall => PathtagScanComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError), WebGPUSceneShaderId.BboxClear => BboxClearComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError), - WebGPUSceneShaderId.Flatten => FlattenComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError), + WebGPUSceneShaderId.PathLowering => PathLoweringComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError), WebGPUSceneShaderId.DrawReduce => DrawReduceComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError), WebGPUSceneShaderId.DrawLeaf => DrawLeafComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError), WebGPUSceneShaderId.ClipReduce => ClipReduceComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError), @@ -4340,7 +4340,7 @@ bool LayoutFactory(WebGPU api, WGPUDeviceImpl* device, out WGPUBindGroupLayoutIm WebGPUSceneShaderId.PathtagScan => PathtagScanComputeShader.ShaderCode, WebGPUSceneShaderId.PathtagScanSmall => PathtagScanComputeShader.SmallShaderCode, WebGPUSceneShaderId.BboxClear => BboxClearComputeShader.ShaderCode, - WebGPUSceneShaderId.Flatten => FlattenComputeShader.ShaderCode, + WebGPUSceneShaderId.PathLowering => PathLoweringComputeShader.ShaderCode, WebGPUSceneShaderId.DrawReduce => DrawReduceComputeShader.ShaderCode, WebGPUSceneShaderId.DrawLeaf => DrawLeafComputeShader.ShaderCode, WebGPUSceneShaderId.ClipReduce => ClipReduceComputeShader.ShaderCode, @@ -4367,7 +4367,7 @@ bool LayoutFactory(WebGPU api, WGPUDeviceImpl* device, out WGPUBindGroupLayoutIm WebGPUSceneShaderId.PathtagScan1 => PathtagScan1ComputeShader.EntryPoint, WebGPUSceneShaderId.PathtagScan or WebGPUSceneShaderId.PathtagScanSmall => PathtagScanComputeShader.EntryPoint, WebGPUSceneShaderId.BboxClear => BboxClearComputeShader.EntryPoint, - WebGPUSceneShaderId.Flatten => FlattenComputeShader.EntryPoint, + WebGPUSceneShaderId.PathLowering => PathLoweringComputeShader.EntryPoint, WebGPUSceneShaderId.DrawReduce => DrawReduceComputeShader.EntryPoint, WebGPUSceneShaderId.DrawLeaf => DrawLeafComputeShader.EntryPoint, WebGPUSceneShaderId.ClipReduce => ClipReduceComputeShader.EntryPoint, @@ -4395,7 +4395,7 @@ bool LayoutFactory(WebGPU api, WGPUDeviceImpl* device, out WGPUBindGroupLayoutIm WebGPUSceneShaderId.PathtagScan => PathtagScanPipelineKey, WebGPUSceneShaderId.PathtagScanSmall => PathtagScanSmallPipelineKey, WebGPUSceneShaderId.BboxClear => BboxClearPipelineKey, - WebGPUSceneShaderId.Flatten => FlattenPipelineKey, + WebGPUSceneShaderId.PathLowering => PathLoweringPipelineKey, WebGPUSceneShaderId.DrawReduce => DrawReducePipelineKey, WebGPUSceneShaderId.DrawLeaf => DrawLeafPipelineKey, WebGPUSceneShaderId.ClipReduce => ClipReducePipelineKey, @@ -4471,7 +4471,7 @@ private static unsafe void WarmPipelines(WebGPURuntime.DeviceSharedState deviceS // Most expensive shaders first so their driver compilation starts as early as possible. ReadOnlySpan order = [ - WebGPUSceneShaderId.Flatten, + WebGPUSceneShaderId.PathLowering, WebGPUSceneShaderId.Coarse, WebGPUSceneShaderId.DrawLeaf, WebGPUSceneShaderId.PathCount, @@ -4602,12 +4602,10 @@ private static unsafe WGPUBindGroupEntry CreateBufferBinding(uint binding, WGPUB size = size }; - using (WebGPUHandle.HandleReference deviceReference = flushContext.DeviceHandle.AcquireReference()) - { - WGPUBufferImpl* buffer = flushContext.Api.DeviceCreateBuffer((WGPUDeviceImpl*)deviceReference.Handle, in descriptor); - flushContext.TrackBuffer(buffer); - return buffer; - } + using WebGPUHandle.HandleReference deviceReference = flushContext.DeviceHandle.AcquireReference(); + WGPUBufferImpl* buffer = flushContext.Api.DeviceCreateBuffer((WGPUDeviceImpl*)deviceReference.Handle, in descriptor); + flushContext.TrackBuffer(buffer); + return buffer; } /// @@ -4633,10 +4631,8 @@ private static unsafe WGPUBindGroupEntry CreateBufferBinding(uint binding, WGPUB size = size }; - using (WebGPUHandle.HandleReference deviceReference = flushContext.DeviceHandle.AcquireReference()) - { - return flushContext.Api.DeviceCreateBuffer((WGPUDeviceImpl*)deviceReference.Handle, in descriptor); - } + using WebGPUHandle.HandleReference deviceReference = flushContext.DeviceHandle.AcquireReference(); + return flushContext.Api.DeviceCreateBuffer((WGPUDeviceImpl*)deviceReference.Handle, in descriptor); } /// @@ -4756,7 +4752,7 @@ private static unsafe bool WaitForMapSignal( if (!signal.IsSet) { - Thread.Yield(); + _ = Thread.Yield(); } } @@ -5007,9 +5003,9 @@ internal enum WebGPUSceneShaderId BboxClear = 6, /// - /// flatten.wgsl: lowers encoded path segments into the device-space line soup. + /// path_lowering.wgsl: lowers encoded path segments into the device-space line soup. /// - Flatten = 7, + PathLowering = 7, /// /// draw_reduce.wgsl: first pass of the draw-tag prefix sum. @@ -5224,7 +5220,6 @@ public static WebGPUSceneResourceRegistry Create(WebGPUSceneResourceSet resource /// /// Registers the transient buffers produced by the scheduling passes. /// - /// The bin-header buffer. /// The indirect dispatch-count buffer. /// The sparse path-row buffer. /// The path-tile buffer. @@ -5234,7 +5229,6 @@ public static WebGPUSceneResourceRegistry Create(WebGPUSceneResourceSet resource /// The PTCL buffer. /// The bump-allocator buffer. public void RegisterSchedulingBuffers( - WGPUBufferImpl* binHeaderBuffer, WGPUBufferImpl* indirectCountBuffer, WGPUBufferImpl* pathRowBuffer, WGPUBufferImpl* pathTileBuffer, @@ -5244,7 +5238,6 @@ public void RegisterSchedulingBuffers( WGPUBufferImpl* ptclBuffer, WGPUBufferImpl* bumpBuffer) { - this.RegisterBuffer(binHeaderBuffer); this.RegisterBuffer(indirectCountBuffer); this.RegisterBuffer(pathRowBuffer); this.RegisterBuffer(pathTileBuffer); @@ -5460,7 +5453,6 @@ internal readonly unsafe struct WebGPUSceneSchedulingResources /// /// Initializes a new instance of the struct. /// - /// The bin-header buffer produced by the scheduling passes. /// The indirect dispatch-count buffer. /// The sparse path-row buffer. /// The path-tile buffer. @@ -5470,7 +5462,6 @@ internal readonly unsafe struct WebGPUSceneSchedulingResources /// The PTCL buffer. /// The bump-allocator buffer. public WebGPUSceneSchedulingResources( - WGPUBufferImpl* binHeaderBuffer, WGPUBufferImpl* indirectCountBuffer, WGPUBufferImpl* pathRowBuffer, WGPUBufferImpl* pathTileBuffer, @@ -5480,7 +5471,6 @@ public WebGPUSceneSchedulingResources( WGPUBufferImpl* ptclBuffer, WGPUBufferImpl* bumpBuffer) { - this.BinHeaderBuffer = binHeaderBuffer; this.IndirectCountBuffer = indirectCountBuffer; this.PathRowBuffer = pathRowBuffer; this.PathTileBuffer = pathTileBuffer; @@ -5491,11 +5481,6 @@ public WebGPUSceneSchedulingResources( this.BumpBuffer = bumpBuffer; } - /// - /// Gets the bin-header buffer produced by the scheduling passes. - /// - public WGPUBufferImpl* BinHeaderBuffer { get; } - /// /// Gets the indirect dispatch-count buffer produced by the scheduling passes. /// @@ -5555,7 +5540,6 @@ internal sealed unsafe class WebGPUSceneSchedulingArena /// The device that created the arena buffers. /// The buffer plan the arena buffers were sized from. /// The size of the map-readable status buffer in bytes. - /// The bin-header scratch buffer. /// The indirect dispatch-count buffer. /// The sparse path-row scratch buffer. /// The path-tile scratch buffer. @@ -5570,7 +5554,6 @@ public WebGPUSceneSchedulingArena( WebGPUDeviceHandle device, WebGPUSceneBufferSizes capacitySizes, nuint readbackByteLength, - WGPUBufferImpl* binHeaderBuffer, WGPUBufferImpl* indirectCountBuffer, WGPUBufferImpl* pathRowBuffer, WGPUBufferImpl* pathTileBuffer, @@ -5585,7 +5568,6 @@ public WebGPUSceneSchedulingArena( this.Device = device; this.CapacitySizes = capacitySizes; this.ReadbackByteLength = readbackByteLength; - this.BinHeaderBuffer = binHeaderBuffer; this.IndirectCountBuffer = indirectCountBuffer; this.PathRowBuffer = pathRowBuffer; this.PathTileBuffer = pathTileBuffer; @@ -5618,11 +5600,6 @@ public WebGPUSceneSchedulingArena( /// public nuint ReadbackByteLength { get; } - /// - /// Gets the bin-header scratch buffer. - /// - public WGPUBufferImpl* BinHeaderBuffer { get; private set; } - /// /// Gets the indirect dispatch-count buffer. /// @@ -5677,7 +5654,6 @@ public WebGPUSceneSchedulingArena( /// when the arena can be reused as-is; otherwise, . public bool CanReuse(WebGPUFlushContext flushContext, WebGPUSceneBufferSizes bufferSizes, nuint readbackByteLength) => ReferenceEquals(this.Device, flushContext.DeviceHandle) && - this.BinHeaderBuffer is not null && this.IndirectCountBuffer is not null && this.PathRowBuffer is not null && this.PathTileBuffer is not null && @@ -5687,7 +5663,6 @@ this.BlendBuffer is not null && this.PtclBuffer is not null && this.BumpBuffer is not null && this.ReadbackBuffer is not null && - bufferSizes.BinHeaders.ByteLength <= this.CapacitySizes.BinHeaders.ByteLength && bufferSizes.IndirectCount.ByteLength <= this.CapacitySizes.IndirectCount.ByteLength && bufferSizes.PathRows.ByteLength <= this.CapacitySizes.PathRows.ByteLength && bufferSizes.PathTiles.ByteLength <= this.CapacitySizes.PathTiles.ByteLength && @@ -5730,8 +5705,6 @@ public static void Dispose(WebGPUSceneSchedulingArena? arena) arena.PathTileBuffer = null; ReleaseArenaBuffer(api, arena.IndirectCountBuffer); arena.IndirectCountBuffer = null; - ReleaseArenaBuffer(api, arena.BinHeaderBuffer); - arena.BinHeaderBuffer = null; } /// diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneEncoder.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneEncoder.cs index 1071ccf96..9e235db55 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneEncoder.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneEncoder.cs @@ -98,12 +98,6 @@ internal static class WebGPUSceneEncoder /// private const uint ClipDifferenceMaskBit = 0x80000000U; - /// - /// Bit of the clip blend word marking a hard-edge (aliased) clip mask. - /// Must match CLIP_HARD_MASK_BIT in drawtag.wgsl. - /// - private const uint ClipHardMaskBit = 0x40000000U; - /// /// Bit of the clip blend word marking an isolated group (a layer). Isolated groups seed /// transparent on the GPU and composite back as a unit; canvas clips leave this clear and @@ -146,7 +140,7 @@ private enum PathTag : byte /// /// /// Cap styles are stored as two two-bit fields: end caps in bits 24-25 and start caps - /// in bits 26-27. The flatten shader shifts the start-cap field down before decoding it. + /// in bits 26-27. The path-lowering shader shifts the start-cap field down before decoding it. /// [Flags] private enum StyleFlags : uint @@ -283,12 +277,6 @@ public static bool TryEncode( fillCount += partitions[i]!.FillCount; } - // Rasterization mode and the aliased coverage threshold are carried per fill, so partitions - // impose no flush-wide rasterization constraint. These scene-wide values are retained only - // as encoded-scene metadata and are no longer consumed by the fine pass. - RasterizationMode fineRasterizationMode = RasterizationMode.Antialiased; - float fineCoverageThreshold = 0F; - if (fillCount == 0) { DisposePartitions(partitions); @@ -299,7 +287,7 @@ public static bool TryEncode( try { - encodedScene = SupportedSubsetSceneResolver.Resolve(partitions, targetBounds, allocator, fineRasterizationMode, fineCoverageThreshold); + encodedScene = SupportedSubsetSceneResolver.Resolve(partitions, targetBounds, allocator); error = null; return true; } @@ -802,8 +790,7 @@ private static RasterizerOptions CreateRasterizerOptions(Rectangle interest, Dra return new RasterizerOptions( interest, options.IntersectionRule, - rasterizationMode, - graphicsOptions.AntialiasThreshold); + rasterizationMode); } /// @@ -1595,10 +1582,12 @@ private static void CreatePartitionCommandRanges( /// private ref struct SupportedSubsetSceneEncoding { + private readonly MemoryAllocator allocator; private bool hasLastStyle; private bool streamsDetached; private bool gradientPixelsDetached; private bool pathGradientDataDetached; + private bool profileStorageDetached; private long estimatedPathRowCount; // CPU-side scratch-demand estimates accumulated per draw so the first GPU attempt can be @@ -1636,7 +1625,10 @@ public SupportedSubsetSceneEncoding( in Rectangle rootTargetBounds, in SceneEncodingPlan plan) { + this.allocator = allocator; this.PathTags = new OwnedStream(allocator, plan.PathTagCapacity); + this.ProfileFills = null; + this.ProfileStorage = null; this.PathData = new OwnedStream(allocator, plan.PathDataWordCapacity); this.DrawTags = new OwnedStream(allocator, plan.DrawTagCapacity); this.DrawData = new OwnedStream(allocator, plan.DrawDataWordCapacity); @@ -1658,6 +1650,7 @@ public SupportedSubsetSceneEncoding( this.streamsDetached = false; this.gradientPixelsDetached = false; this.pathGradientDataDetached = false; + this.profileStorageDetached = false; this.lastStyle0 = 0; this.lastStyle1 = 0; this.lastStyle2 = 0; @@ -1676,8 +1669,6 @@ public SupportedSubsetSceneEncoding( this.estimatedTileCrossings = 0; this.estimatedBinFootprint = 0; this.VisibleFillCount = 0; - this.FineRasterizationMode = RasterizationMode.Antialiased; - this.FineCoverageThreshold = 0F; this.PathTags.Add(PackPathTag(PathTag.Transform)); AppendIdentityTransform(ref this.Transforms); @@ -1688,6 +1679,21 @@ public SupportedSubsetSceneEncoding( /// public OwnedStream PathTags; + /// + /// Aliased fills that need profile tags, in path-data order. + /// + /// + /// A partition creates at most one list. Each list element is a value type that stores a + /// geometry reference, a view into the partition profile arena, and a translation. Repeated + /// geometry uses the same arena entry, and no profile buffer is created for each fill. + /// + public List? ProfileFills; + + /// + /// Gets or sets the partition-owned profile arena created by the first compatible aliased fill. + /// + public LinearGeometryProfileStorage? ProfileStorage; + /// /// Gets or sets the encoded path-coordinate stream. /// @@ -1775,7 +1781,7 @@ public SupportedSubsetSceneEncoding( /// /// Gets the CPU-side upper-bound estimate of tile-boundary crossings produced by the - /// scene's flattened lines. Bounds the GPU segment, segment-count, and path-tile demand. + /// scene's final lines. Bounds the GPU segment, segment-count, and path-tile demand. /// public readonly long EstimatedTileCrossings => this.estimatedTileCrossings; @@ -1784,16 +1790,6 @@ public SupportedSubsetSceneEncoding( /// public readonly long EstimatedBinFootprint => this.estimatedBinFootprint; - /// - /// Gets the flush-wide fine rasterization mode selected while encoding visible fills. - /// - public RasterizationMode FineRasterizationMode { get; private set; } - - /// - /// Gets the aliased coverage threshold consumed by the fine pass when aliased mode is selected. - /// - public float FineCoverageThreshold { get; private set; } - /// /// Gets a value indicating whether the encoding produced no fill work. /// @@ -1848,7 +1844,7 @@ public SceneEncodingCheckpoint BeginPackedIndependentRange( this.BeginIndependentRange(targetBounds); SceneEncodingCheckpoint checkpoint = this.CaptureCheckpoint(); - // Each packed range is scanned from its own path-tag base, but flatten.wgsl + // Each packed range is scanned from its own path-tag base, but path_lowering.wgsl // still rebases transform indices by the implicit identity transform. Emit // that identity inside the range so range-local identity paths resolve to // transform index zero instead of underflowing into unrelated transform data. @@ -1987,6 +1983,11 @@ public static WebGPUSceneRange CreateRange( /// public void Dispose() { + if (!this.profileStorageDetached) + { + this.ProfileStorage?.Dispose(); + } + if (!this.gradientPixelsDetached) { this.GradientPixels.Dispose(); @@ -2023,6 +2024,11 @@ public void Dispose() /// public void MarkPathGradientDataDetached() => this.pathGradientDataDetached = true; + /// + /// Marks the profile arena as detached so disposal does not release partition-owned data twice. + /// + public void MarkProfileStorageDetached() => this.profileStorageDetached = true; + /// /// Appends all supported scene operations into the mutable scene streams. /// @@ -2236,7 +2242,7 @@ public bool TryAppend( /// Local rasterizer options used to generate coverage. /// Absolute destination offset where coverage is composited. /// Absolute target bounds that own the composited pixels. - /// Optional pre-flattened geometry for . + /// Optional pre-linearized geometry for . /// The error message when the command cannot be encoded. /// when the command was encoded. public bool TryAppendExternalTextureFill( @@ -2420,10 +2426,8 @@ private bool TryRegisterVisibleFill(in RasterizerOptions options, out string? er { this.VisibleFillCount++; - // Both the rasterization mode (draw-flags aliased bit) and the aliased coverage threshold - // (the per-fill coverage word in CMD_FILL) travel to the fine pass per fill, matching the - // CPU rasterizer exactly. A flush may freely mix antialiased and aliased fills with any - // thresholds, so no flush-wide constraint remains. + // The draw-flags aliased bit carries the rasterization mode per fill. A flush can mix + // antialiased and centre-sampled fills without a flush-wide rasterization state. _ = options; error = null; return true; @@ -2491,6 +2495,16 @@ private void PadPathTagsToSegmentBoundary() this.PathTags.Advance(paddedTags.Length); } + /// + /// Returns whether the transform preserves the coordinate axes with unit scale, the + /// condition under which geometry-space profile extents translate directly into + /// absolute target space. + /// + /// The scene transform to classify. + /// when the transform is a pure translation; otherwise . + private static bool IsProfileCompatibleTransform(in GpuSceneTransform transform) + => transform.M11 == 1F && transform.M22 == 1F && transform.M12 == 0F && transform.M21 == 0F && transform.M14 == 0F && transform.M24 == 0F && transform.M44 == 1F; + /// /// Encodes one visible fill command into the path, draw, style, and auxiliary payload streams. /// @@ -2589,6 +2603,22 @@ private void AppendPlainFill( this.AccumulateDrawRowEstimate(command.RasterizerOptions.Interest, geometryLineCount, geometryTileCrossings); this.FillCount++; this.PathCount += encodedPathCount; + + // Profile ranges use the geometry's original X and Y axes. A translation can be added + // when the scene is packed. Scale, rotation, or shear would require new ranges, so + // those draws keep sentinel tags and do not apply the terminating-tip test. + if (command.RasterizerOptions.RasterizationMode == RasterizationMode.Aliased && IsProfileCompatibleTransform(this.lastTransform)) + { + LinearGeometryProfiles profiles = (this.ProfileStorage ??= new LinearGeometryProfileStorage(this.allocator)).GetOrAdd(geometry); + (this.ProfileFills ??= []).Add( + new GpuProfileFillRecord( + pathDataCheckpoint, + geometry, + profiles, + this.lastTransform.Tx + (pathDataOffset.X - this.rootTargetBounds.X), + this.lastTransform.Ty + (pathDataOffset.Y - this.rootTargetBounds.Y))); + } + this.LineCount += geometryLineCount; this.InfoWordCount += (int)drawTagMonoid.InfoOffset; this.DrawTags.Add(drawTag); @@ -2694,8 +2724,7 @@ private void AppendPlainStroke( this.rootTargetBounds, ref this.PathTags, ref this.PathData, - out int geometryLineCount, - out long geometryTileCrossings); + out int geometryLineCount); if (encodedPathCount == 0) { @@ -2715,7 +2744,7 @@ private void AppendPlainStroke( this.lastStyle7 = style7; this.lastStyle8 = style8; this.lastStyle9 = style9; - this.AccumulateDrawRowEstimate(command.RasterizerOptions.Interest, geometryLineCount, geometryTileCrossings); + this.AccumulateDrawRowEstimate(command.RasterizerOptions.Interest, geometryLineCount, -1); this.FillCount++; this.PathCount += encodedPathCount; this.LineCount += geometryLineCount; @@ -2814,8 +2843,7 @@ private void AppendExplicitStroke( this.rootTargetBounds, ref this.PathTags, ref this.PathData, - out int geometryLineCount, - out long geometryTileCrossings); + out int geometryLineCount); if (encodedPathCount == 0) { @@ -2835,7 +2863,7 @@ private void AppendExplicitStroke( this.lastStyle7 = style7; this.lastStyle8 = style8; this.lastStyle9 = style9; - this.AccumulateDrawRowEstimate(rasterizerOptions.Interest, geometryLineCount, geometryTileCrossings); + this.AccumulateDrawRowEstimate(rasterizerOptions.Interest, geometryLineCount, -1); this.FillCount++; this.PathCount += encodedPathCount; this.LineCount += geometryLineCount; @@ -2936,8 +2964,7 @@ private void AppendExplicitStroke( this.rootTargetBounds, ref this.PathTags, ref this.PathData, - out int geometryLineCount, - out long geometryTileCrossings); + out int geometryLineCount); if (encodedPathCount == 0) { @@ -2957,7 +2984,7 @@ private void AppendExplicitStroke( this.lastStyle7 = style7; this.lastStyle8 = style8; this.lastStyle9 = style9; - this.AccumulateDrawRowEstimate(rasterizerOptions.Interest, geometryLineCount, geometryTileCrossings); + this.AccumulateDrawRowEstimate(rasterizerOptions.Interest, geometryLineCount, -1); this.FillCount++; this.PathCount += encodedPathCount; this.LineCount += geometryLineCount; @@ -3011,7 +3038,9 @@ private bool AppendClipDescriptor(in DrawingClipDescriptor descriptor, Point des uint style0 = descriptor.IntersectionRule == IntersectionRule.EvenOdd ? (uint)StyleFlags.Fill : 0U; const uint style1 = 0U; - const uint style2 = 0U; + uint style2 = descriptor.EdgeMode == DrawingClipEdgeMode.Hard + ? GpuSceneDrawTag.FillInfoFlagsAliasedBit + : 0U; const uint style3 = 0U; const uint style4 = 0U; const uint style5 = 0U; @@ -3286,7 +3315,7 @@ private bool AppendClipDescriptor(in DrawingClipDescriptor descriptor, Point des this.LineCount += clipLineCount; this.InfoWordCount += (int)GpuSceneDrawTag.Map(GpuSceneDrawTag.BeginClip).InfoOffset; this.DrawTags.Add(GpuSceneDrawTag.BeginClip); - AppendClipBeginData(descriptor.Operation, descriptor.EdgeMode, descriptor.AntialiasThreshold, clipBlendMode, ref this.DrawData); + AppendClipBeginData(descriptor.Operation, clipBlendMode, ref this.DrawData); this.ClipCount++; return true; } @@ -3383,9 +3412,9 @@ private void AppendBeginLayer(in CompositionCommand command) /// Adds one draw object's target-space interest rectangle to the sparse scratch estimates after converting it to root-target-local coordinates. /// /// The draw object's absolute raster interest bounds. - /// The draw object's flattened (or GPU-expansion estimated) line count. + /// The draw object's linearized or GPU-expanded line count. /// - /// The exact summed per-line tile-crossing bound when the caller flattened the geometry on the CPU + /// The exact summed per-line tile-crossing bound when the caller linearized the geometry on the CPU /// (fills), or a negative value to fall back to the bounding-box diagonal bound (strokes, clips). /// private void AccumulateDrawRowEstimate(Rectangle absoluteInterest, int lineCount, long tileCrossings = -1) @@ -3397,9 +3426,9 @@ private void AccumulateDrawRowEstimate(Rectangle absoluteInterest, int lineCount /// bin footprint for binning records. /// /// The draw object's root-target-local raster interest bounds. - /// The draw object's flattened (or GPU-expansion estimated) line count. + /// The draw object's linearized or GPU-expanded line count. /// - /// The exact summed per-line tile-crossing bound when the caller flattened the geometry on the CPU + /// The exact summed per-line tile-crossing bound when the caller linearized the geometry on the CPU /// (fills), or a negative value to fall back to the bounding-box diagonal bound (strokes, clips). /// private void AccumulateDrawRowEstimateLocal(Rectangle localBounds, int lineCount, long tileCrossings = -1) @@ -3484,6 +3513,7 @@ private sealed class SceneEncodingPartition : IDisposable private readonly IMemoryOwner stylesOwner; private readonly IMemoryOwner? gradientPixelsOwner; private readonly IMemoryOwner? pathGradientDataOwner; + private readonly LinearGeometryProfileStorage? profileStorage; private bool disposed; /// @@ -3516,8 +3546,8 @@ private sealed class SceneEncodingPartition : IDisposable /// The transform word count. /// The style word count. /// The path-gradient payload word count. - /// The fine-pass rasterization mode selected by the partition. - /// The aliased coverage threshold selected by the partition. + /// The detached partition-owned profile arena. + /// The aliased fills that have profile data. private SceneEncodingPartition( IMemoryOwner pathTagsOwner, IMemoryOwner pathDataOwner, @@ -3546,8 +3576,8 @@ private SceneEncodingPartition( int transformWordCount, int styleWordCount, int pathGradientDataWordCount, - RasterizationMode fineRasterizationMode, - float fineCoverageThreshold) + LinearGeometryProfileStorage? profileStorage, + List? profileFills) { this.pathTagsOwner = pathTagsOwner; this.pathDataOwner = pathDataOwner; @@ -3557,6 +3587,7 @@ private SceneEncodingPartition( this.stylesOwner = stylesOwner; this.gradientPixelsOwner = gradientPixelsOwner; this.pathGradientDataOwner = pathGradientDataOwner; + this.profileStorage = profileStorage; this.Images = images; this.Recolors = recolors; this.FillCount = fillCount; @@ -3576,10 +3607,15 @@ private SceneEncodingPartition( this.TransformWordCount = transformWordCount; this.StyleWordCount = styleWordCount; this.PathGradientDataWordCount = pathGradientDataWordCount; - this.FineRasterizationMode = fineRasterizationMode; - this.FineCoverageThreshold = fineCoverageThreshold; + this.ProfileFills = profileFills; } + /// + /// Gets the aliased fills that have profile data. Their offsets are relative to this + /// partition's path-data stream. + /// + public List? ProfileFills { get; } + /// /// Gets the encoded path-tag bytes. /// @@ -3721,16 +3757,6 @@ public ReadOnlySpan PathGradientData /// public int PathGradientDataWordCount { get; } - /// - /// Gets the fine-pass rasterization mode selected by this partition. - /// - public RasterizationMode FineRasterizationMode { get; } - - /// - /// Gets the aliased coverage threshold selected by this partition. - /// - public float FineCoverageThreshold { get; } - /// /// Detaches all retained streams from a mutable partition encoder. /// @@ -3754,6 +3780,7 @@ public static SceneEncodingPartition Detach(ref SupportedSubsetSceneEncoding enc IMemoryOwner transformsOwner = encoding.Transforms.DetachOwner(); IMemoryOwner stylesOwner = encoding.Styles.DetachOwner(); encoding.MarkStreamsDetached(); + encoding.MarkProfileStorageDetached(); IMemoryOwner? gradientPixelsOwner = null; if (gradientRowCount > 0) @@ -3797,8 +3824,8 @@ public static SceneEncodingPartition Detach(ref SupportedSubsetSceneEncoding enc transformWordCount, styleWordCount, pathGradientDataWordCount, - encoding.FineRasterizationMode, - encoding.FineCoverageThreshold); + encoding.ProfileStorage, + encoding.ProfileFills); } /// @@ -3820,6 +3847,7 @@ public void Dispose() this.stylesOwner.Dispose(); this.gradientPixelsOwner?.Dispose(); this.pathGradientDataOwner?.Dispose(); + this.profileStorage?.Dispose(); } } @@ -3858,6 +3886,27 @@ public static WebGPUEncodedScene Resolve( int transformBase = drawDataBase + drawDataWordCount; int styleBase = transformBase + transformWordCount; int sceneWordCount = styleBase + styleWordCount; + List? profileFills = encoding.ProfileFills; + int profileSlotWordCount = 0; + int xProfileCount = 0; + int yProfileCount = 0; + int profileSlotsBase = 0; + int profileRecordsBase = 0; + if (profileFills is not null) + { + GetProfileRegionLayout( + profileFills, + pathTagWordCount, + pathDataWordCount, + out profileSlotWordCount, + out xProfileCount, + out yProfileCount); + + profileSlotsBase = sceneWordCount; + profileRecordsBase = checked(profileSlotsBase + profileSlotWordCount); + sceneWordCount = checked(profileRecordsBase + 2 + ((xProfileCount + yProfileCount) * 3)); + } + GpuSceneLayout layout = new( (uint)drawTagCount, (uint)encoding.PathCount, @@ -3884,9 +3933,22 @@ public static WebGPUEncodedScene Resolve( encoding.DrawData.WrittenSpan, encoding.Transforms.WrittenSpan, encoding.Styles.WrittenSpan, - sceneDataOwner.Memory.Span[..sceneWordCount]); + sceneDataOwner.Memory.Span[..(profileSlotsBase == 0 ? sceneWordCount : profileSlotsBase)]); + + if (profileFills is not null) + { + WriteProfileRegions( + sceneDataOwner.Memory.Span[..sceneWordCount], + profileFills, + pathTagWordCount, + profileSlotsBase, + profileSlotWordCount, + profileRecordsBase, + xProfileCount, + yProfileCount); + } - return new WebGPUEncodedScene( + WebGPUEncodedScene resolvedScene = new( targetBounds.Size, encoding.InfoWordCount, sceneDataOwner, @@ -3913,11 +3975,12 @@ public static WebGPUEncodedScene Resolve( Math.Max(encoding.EstimatedPathRowCount, encoding.PathCount), DivideRoundUp(targetBounds.Width, TileWidth), DivideRoundUp(targetBounds.Height, TileHeight), - encoding.FineRasterizationMode, - encoding.FineCoverageThreshold, encoding.EstimatedTileCrossings, encoding.EstimatedBinFootprint, operations); + + resolvedScene.SetProfileRegions((uint)profileSlotsBase, (uint)profileRecordsBase); + return resolvedScene; } catch { @@ -3926,21 +3989,359 @@ public static WebGPUEncodedScene Resolve( } } + /// + /// Calculates the segment-to-profile table size and the number of profile records. + /// + /// The aliased fills that have profile data. + /// The word offset of path data in the scene. + /// The path-data length in words. + /// Receives the segment-to-profile table length in words. + /// Receives the addressable X-profile count. + /// Receives the addressable Y-profile count. + private static void GetProfileRegionLayout( + List fills, + int dataRegionBase, + int dataWordCount, + out int slotWordCount, + out int xProfileCount, + out int yProfileCount) + { + // Every line starts at a two-word point. The shader divides that point's absolute scene + // offset by two to find its profile tag. Include the words before path data so a scene + // rendered in several ranges still uses one stable index. + slotWordCount = (dataRegionBase + dataWordCount + 1) >> 1; + xProfileCount = 0; + yProfileCount = 0; + for (int fillIndex = 0; fillIndex < fills.Count; fillIndex++) + { + LinearGeometryProfiles profiles = fills[fillIndex].Profiles; + + // Each axis identifier has 16 bits. The largest value means "no profile", so the + // record count stops before that value. + xProfileCount = (int)Math.Min((long)xProfileCount + profiles.XProfileCount, LinearGeometryProfiles.SentinelId); + yProfileCount = (int)Math.Min((long)yProfileCount + profiles.YProfileCount, LinearGeometryProfiles.SentinelId); + } + } + + /// + /// Calculates profile storage for ordered partitions without copying their fill lists. + /// + /// The ordered partition encodings. + /// The word offset of path data in the scene. + /// The path-data length in words. + /// Receives the segment-to-profile table length in words. + /// Receives the addressable X-profile count. + /// Receives the addressable Y-profile count. + private static void GetProfileRegionLayout( + SceneEncodingPartition?[] partitions, + int dataRegionBase, + int dataWordCount, + out int slotWordCount, + out int xProfileCount, + out int yProfileCount) + { + slotWordCount = (dataRegionBase + dataWordCount + 1) >> 1; + xProfileCount = 0; + yProfileCount = 0; + for (int partitionIndex = 0; partitionIndex < partitions.Length; partitionIndex++) + { + List? fills = partitions[partitionIndex]!.ProfileFills; + if (fills is null) + { + continue; + } + + for (int fillIndex = 0; fillIndex < fills.Count; fillIndex++) + { + LinearGeometryProfiles profiles = fills[fillIndex].Profiles; + + // Profile identifiers are scene-wide because all render ranges share these + // records. The largest 16-bit value remains reserved for "no profile". + xProfileCount = (int)Math.Min((long)xProfileCount + profiles.XProfileCount, LinearGeometryProfiles.SentinelId); + yProfileCount = (int)Math.Min((long)yProfileCount + profiles.YProfileCount, LinearGeometryProfiles.SentinelId); + } + } + } + + /// + /// Writes one axis of profile records in scene-global identifier order. + /// + /// The destination record list. + /// The geometry-space profile data. + /// Whether to write the X-axis profile table. + /// The number of addressable records to write. + /// The scene-global identifier base for this fill's axis profiles. + /// The translation from geometry space to absolute target space. + private static void WriteProfileRecords( + Span records, + LinearGeometryProfiles profiles, + bool xAxis, + int count, + int idBase, + float translate) + { + if (xAxis) + { + // Select the source table outside the loop so this hot packing pass does not test + // the axis again for every record. + for (int i = 0; i < count; i++) + { + profiles.GetXProfile(i, out float minimum, out float maximum, out int link); + WriteProfileRecord(records, i, minimum, maximum, link, idBase, translate); + } + + return; + } + + for (int i = 0; i < count; i++) + { + profiles.GetYProfile(i, out float minimum, out float maximum, out int link); + WriteProfileRecord(records, i, minimum, maximum, link, idBase, translate); + } + } + + /// + /// Writes one three-word profile record. + /// + /// The destination record list. + /// The destination profile index. + /// The lowest coordinate reached by the profile. + /// The highest coordinate reached by the profile. + /// The profile's contour connection data. + /// The scene-global identifier base. + /// The translation from geometry space to absolute target space. + private static void WriteProfileRecord( + Span records, + int index, + float minimum, + float maximum, + int link, + int idBase, + float translate) + { + // Words 0 and 1 are the inclusive axis range in absolute target-space 24.8 format. + records[index * 3] = unchecked((uint)FloatToProfileFixed(minimum + translate)); + records[(index * 3) + 1] = unchecked((uint)FloatToProfileFixed(maximum + translate)); + + // Bit 0 records a connection to the previous profile. Bits 1..31 contain the one-based + // identifier joined across a closed contour's final point. Rebase only that identifier; + // zero continues to mean that there is no closing connection. + int partner = link >> 1; + if (partner != 0) + { + partner += idBase; + } + + records[(index * 3) + 2] = unchecked((uint)((link & 1) | (partner << 1))); + } + + /// + /// Converts a geometry-local segment tag to scene-global profile identifiers. + /// + /// The per-geometry profile tag. + /// The scene-global x identifier base. + /// The scene-global y identifier base. + /// The rebased tag. + private static uint RebaseProfileTag(uint tag, int xBase, int yBase) + { + uint sentinel = LinearGeometryProfiles.SentinelId; + uint xId = tag & sentinel; + uint yId = tag >> 16; + + // Adding the shared-table base also preserves an existing sentinel: the sum remains at + // or above the reserved value and is replaced with the same sentinel. + xId = xId + (uint)xBase >= sentinel ? sentinel : xId + (uint)xBase; + yId = yId + (uint)yBase >= sentinel ? sentinel : yId + (uint)yBase; + return xId | (yId << 16); + } + + /// + /// Converts one geometry-space coordinate to absolute target-space 24.8 fixed-point with + /// the same clamp and rounding the CPU rasterizer's profile tables use. + /// + /// The coordinate to convert. + /// The 24.8 fixed-point value. + private static int FloatToProfileFixed(float value) => (int)MathF.Round(Math.Clamp(value, -8388608F, 8388607F) * 256F); + + /// + /// Writes both profile tables for a single encoding stream. + /// + /// The full scene word span. + /// The aliased fills that have profile data, in path-data order. + /// The word offset of path data in the scene. + /// The word offset of the segment-to-profile table. + /// The segment-to-profile table length in words. + /// The word offset of the profile range and contour-link table. + /// The addressable X-profile count. + /// The addressable Y-profile count. + private static void WriteProfileRegions( + Span sceneWords, + List fills, + int dataRegionBase, + int slotsBase, + int slotWordCount, + int recordsBase, + int xProfileCount, + int yProfileCount) + { + Span slots = sceneWords.Slice(slotsBase, slotWordCount); + + // Not every encoded point starts a line. Mark all point pairs as having no profile, + // then write tags only at the line starts of the recorded aliased fills. + slots.Fill(uint.MaxValue); + sceneWords[recordsBase] = (uint)xProfileCount; + sceneWords[recordsBase + 1] = (uint)yProfileCount; + + int xBase = 0; + int yBase = 0; + int xRecordOffset = recordsBase + 2; + int yRecordOffset = xRecordOffset + (xProfileCount * 3); + for (int fillIndex = 0; fillIndex < fills.Count; fillIndex++) + { + GpuProfileFillRecord fill = fills[fillIndex]; + WriteProfileFill( + sceneWords, + slots, + fill, + dataRegionBase + fill.PathDataOffset, + xProfileCount, + yProfileCount, + ref xBase, + ref yBase, + ref xRecordOffset, + ref yRecordOffset); + } + } + + /// + /// Writes both profile tables from ordered partitions without copying their fill lists. + /// + /// The full scene word span. + /// The ordered partition encodings. + /// The word offset of path data in the scene. + /// The word offset of the segment-to-profile table. + /// The segment-to-profile table length in words. + /// The word offset of the profile range and contour-link table. + /// The addressable X-profile count. + /// The addressable Y-profile count. + private static void WriteProfileRegions( + Span sceneWords, + SceneEncodingPartition?[] partitions, + int dataRegionBase, + int slotsBase, + int slotWordCount, + int recordsBase, + int xProfileCount, + int yProfileCount) + { + Span slots = sceneWords.Slice(slotsBase, slotWordCount); + + // All partitions share one table indexed by absolute scene position. Initialize + // padding, non-line points, and fills without profiles to the sentinel tag. + slots.Fill(uint.MaxValue); + sceneWords[recordsBase] = (uint)xProfileCount; + sceneWords[recordsBase + 1] = (uint)yProfileCount; + + int xBase = 0; + int yBase = 0; + int xRecordOffset = recordsBase + 2; + int yRecordOffset = xRecordOffset + (xProfileCount * 3); + int partitionDataOffset = 0; + for (int partitionIndex = 0; partitionIndex < partitions.Length; partitionIndex++) + { + SceneEncodingPartition partition = partitions[partitionIndex]!; + List? fills = partition.ProfileFills; + if (fills is not null) + { + for (int fillIndex = 0; fillIndex < fills.Count; fillIndex++) + { + GpuProfileFillRecord fill = fills[fillIndex]; + WriteProfileFill( + sceneWords, + slots, + fill, + dataRegionBase + partitionDataOffset + fill.PathDataOffset, + xProfileCount, + yProfileCount, + ref xBase, + ref yBase, + ref xRecordOffset, + ref yRecordOffset); + } + } + + partitionDataOffset += partition.PathDataWordCount; + } + } + + /// + /// Writes one fill's profile records and maps each line start to its profile identifiers. + /// + /// The full scene word span. + /// The segment-to-profile table. + /// The aliased fill that has profile data. + /// The fill's first absolute path-data word. + /// The addressable X-profile count. + /// The addressable Y-profile count. + /// The next scene-global X-profile identifier. + /// The next scene-global Y-profile identifier. + /// The next X-profile record word offset. + /// The next Y-profile record word offset. + private static void WriteProfileFill( + Span sceneWords, + Span slots, + in GpuProfileFillRecord fill, + int contourStart, + int xProfileCount, + int yProfileCount, + ref int xBase, + ref int yBase, + ref int xRecordOffset, + ref int yRecordOffset) + { + LinearGeometryProfiles profiles = fill.Profiles; + + // The scene-wide 16-bit limit can truncate the final fill's profile tables. Tags for + // records beyond the limit become sentinels when they are rebased below. + int xCount = Math.Min(profiles.XProfileCount, xProfileCount - xBase); + int yCount = Math.Min(profiles.YProfileCount, yProfileCount - yBase); + WriteProfileRecords(sceneWords.Slice(xRecordOffset, xCount * 3), profiles, xAxis: true, xCount, xBase, fill.TranslateX); + WriteProfileRecords(sceneWords.Slice(yRecordOffset, yCount * 3), profiles, xAxis: false, yCount, yBase, fill.TranslateY); + + int segmentOrdinal = 0; + LinearContour[] contours = (LinearContour[])fill.Geometry.Contours; + + // A profile tag is stored at the start-point pair of its line. Walk the contour layout + // to find those pairs and write directly to the final table. This second linear pass + // avoids a separate offset array for every fill. + for (int contourIndex = 0; contourIndex < contours.Length; contourIndex++) + { + LinearContour contour = contours[contourIndex]; + for (int j = 0; j < contour.SegmentCount; j++) + { + slots[(contourStart >> 1) + j] = RebaseProfileTag(profiles.GetSegmentTag(segmentOrdinal++), xBase, yBase); + } + + contourStart += 2 + (contour.SegmentCount * 2); + } + + xBase += xCount; + yBase += yCount; + xRecordOffset += xCount * 3; + yRecordOffset += yCount * 3; + } + /// /// Resolves ordered partition encodings into the final packed scene buffers. /// /// The ordered partition encodings. /// The target bounds. /// The allocator used for scene buffers. - /// The fine-pass rasterization mode. - /// The scene-wide aliased coverage threshold. /// The resolved encoded scene. public static WebGPUEncodedScene Resolve( SceneEncodingPartition?[] partitions, in Rectangle targetBounds, - MemoryAllocator allocator, - RasterizationMode fineRasterizationMode, - float fineCoverageThreshold) + MemoryAllocator allocator) { int pathTagByteCount = 0; int pathDataWordCount = 0; @@ -3957,6 +4358,7 @@ public static WebGPUEncodedScene Resolve( int lineCount = 0; int infoWordCount = 0; int clipCount = 0; + int profileFillCount = 0; long estimatedPathRowCount = 0; long estimatedTileCrossings = 0; long estimatedBinFootprint = 0; @@ -3979,6 +4381,7 @@ public static WebGPUEncodedScene Resolve( lineCount += partition.LineCount; infoWordCount += partition.InfoWordCount; clipCount += partition.ClipCount; + profileFillCount += partition.ProfileFills?.Count ?? 0; estimatedPathRowCount = Math.Min(estimatedPathRowCount + partition.EstimatedPathRowCount, int.MaxValue); estimatedTileCrossings += partition.EstimatedTileCrossings; estimatedBinFootprint += partition.EstimatedBinFootprint; @@ -3992,6 +4395,27 @@ public static WebGPUEncodedScene Resolve( int transformBase = drawDataBase + drawDataWordCount; int styleBase = transformBase + transformWordCount; int sceneWordCount = styleBase + styleWordCount; + + int profileSlotWordCount = 0; + int xProfileCount = 0; + int yProfileCount = 0; + int profileSlotsBase = 0; + int profileRecordsBase = 0; + if (profileFillCount != 0) + { + GetProfileRegionLayout( + partitions, + pathTagWordCount, + pathDataWordCount, + out profileSlotWordCount, + out xProfileCount, + out yProfileCount); + + profileSlotsBase = sceneWordCount; + profileRecordsBase = checked(profileSlotsBase + profileSlotWordCount); + sceneWordCount = checked(profileRecordsBase + 2 + ((xProfileCount + yProfileCount) * 3)); + } + GpuSceneLayout layout = new( (uint)drawTagCount, (uint)pathCount, @@ -4084,6 +4508,19 @@ public static WebGPUEncodedScene Resolve( } sceneBytes[pathTagByteCount..(pathTagWordCount * sizeof(uint))].Clear(); + if (profileFillCount != 0) + { + WriteProfileRegions( + sceneWords, + partitions, + pathTagWordCount, + profileSlotsBase, + profileSlotWordCount, + profileRecordsBase, + xProfileCount, + yProfileCount); + } + WebGPUEncodedScene resolved = new( targetBounds.Size, infoWordCount, @@ -4111,12 +4548,11 @@ public static WebGPUEncodedScene Resolve( Math.Max((int)estimatedPathRowCount, pathCount), DivideRoundUp(targetBounds.Width, TileWidth), DivideRoundUp(targetBounds.Height, TileHeight), - fineRasterizationMode, - fineCoverageThreshold, estimatedTileCrossings, estimatedBinFootprint, []); + resolved.SetProfileRegions((uint)profileSlotsBase, (uint)profileRecordsBase); sceneDataOwner = null; gradientPixelsOwner = null; pathGradientDataOwner = null; @@ -4440,7 +4876,6 @@ private static bool TryResolveRasterization( clippedDestination, options.IntersectionRule, options.RasterizationMode, - options.AntialiasThreshold, options.CoverageBoost); brushBounds = absoluteInterest; @@ -4733,7 +5168,7 @@ private static uint GetDrawTag(in ResolvedPathCommand command) /// Encodes a lowered path into path-tag and path-data streams in target-local space. /// /// The absolute destination offset applied to every point. - /// The flattened geometry to encode. + /// The linear geometry to encode. /// The root target bounds used for target-local conversion. /// The path-tag stream. /// The path-data stream. @@ -4844,42 +5279,21 @@ private static float Sum(Vector2 value) => Vector128.Sum(value.AsVector128()); /// - /// Scales a stroke centerline's tile-crossing count up to a conservative bound on the tiles the GPU - /// stroker's emitted geometry covers. The stroker expands each centerline into two offset sides plus - /// joins and caps, and a stroke of the given width spreads each side across roughly one extra tile per - /// 16 pixels, so the centerline count alone under-seeds the segment and path-tile scratch buffers. - /// - /// The summed centerline tile-crossing count. - /// The estimated emitted stroke line count, including joins, caps and arcs. - /// The transform-scaled stroke width in pixels. - /// The conservative emitted-stroke tile-crossing bound. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static long ScaleStrokeTileCrossings(long centerlineCrossings, int lineCount, float strokeWidth) - { - long widthTiles = 1 + (long)MathF.Ceiling(MathF.Max(strokeWidth, 0F) / TileWidth); - return (centerlineCrossings + Math.Max(lineCount, 1)) * widthTiles; - } - - /// - /// Encodes a stroke centerline into Vello-style path tags and path data in target-local space, - /// applying the CPU stroker's contour preprocessing so the GPU stroker consumes the same - /// segment list the CPU stroker strokes. + /// Encodes the final line segments of a stroke centreline in target-local space. /// /// - /// This ports DefaultRasterizer.StrokeLinearizer.BuildContourSegments (exact-duplicate - /// and 1/64 px micro-segment collapse) and IsContourClosedForEmission (closed-contour - /// normalization) to encode time. The GPU shader then expands clean segments with the ported - /// CPU join and cap geometry; it never sees a degenerate-tangent segment. + /// The encoder removes duplicate and sub-1/64-pixel segments and applies the same open/closed + /// contour rules as the CPU stroker. The shader receives lines only and expands those lines into + /// stroke bodies, joins, and caps. /// - /// The flattened centerline geometry to encode. + /// The linear centreline geometry to encode. /// The absolute destination offset applied to every point. /// The pen that defines stroke width, joins, and caps. /// The transform-derived scale applied to the stroke width. /// The root target bounds used for target-local conversion. /// The path-tag stream. /// The path-data stream. - /// Receives the estimated flattened line workload for the stroke. - /// Receives the conservative emitted-stroke tile-crossing bound used to seed the scratch buffers. + /// Receives the estimated final line workload for the stroke. /// The number of encoded path objects: 1, or 0 when no contour survived preprocessing. private static int EncodeStrokePath( LinearGeometry geometry, @@ -4889,162 +5303,164 @@ private static int EncodeStrokePath( in Rectangle rootTargetBounds, ref OwnedStream pathTags, ref OwnedStream pathData, - out int lineCount, - out long tileCrossings) + out int lineCount) { float pointTranslateX = destinationOffset.X - rootTargetBounds.X; float pointTranslateY = destinationOffset.Y - rootTargetBounds.Y; - Vector2 translate = new(pointTranslateX, pointTranslateY); lineCount = EstimateStrokeLineCount(geometry, pen, widthScale); - tileCrossings = 0; float strokeWidth = pen.StrokeWidth * widthScale; int encodedContourCount = 0; - IReadOnlyList geometryPoints = geometry.Points; + LinearContour[] contours = (LinearContour[])geometry.Contours; + PointF[] geometryPoints = (PointF[])geometry.Points; - for (int i = 0; i < geometry.Contours.Count; i++) + for (int i = 0; i < contours.Length; i++) { - LinearContour contour = geometry.Contours[i]; + LinearContour contour = contours[i]; if (contour.PointCount == 0) { continue; } - PointF[] kept = ArrayPool.Shared.Rent(contour.PointCount); - try + ReadOnlySpan contourPoints = geometryPoints.AsSpan(contour.PointStart, contour.PointCount); + PointF firstPoint = contourPoints[0]; + PointF previousKept = firstPoint; + PointF pointLike = firstPoint; + PointF secondPoint = default; + PointF lastKept = firstPoint; + int pointIndex = 1; + int keptCount = 1; + + // Count the filtered points and retain only the few values needed to classify the + // contour. The second pass writes them directly to the scene streams, so no temporary + // point buffer is rented for each stroke geometry. + while (TryGetNextStrokePoint(contourPoints, ref pointIndex, ref previousKept, ref pointLike, out PointF point)) { - // CPU stroker preprocessing: collapse exact duplicates and micro-segments below - // 1/64 px forward onto the last kept point. - int keptCount = 0; - PointF firstPoint = geometryPoints[contour.PointStart]; - kept[keptCount++] = firstPoint; - PointF pointLike = firstPoint; - for (int j = 1; j < contour.PointCount; j++) + if (keptCount == 1) { - PointF point = geometryPoints[contour.PointStart + j]; - PointF previous = kept[keptCount - 1]; - if (point == previous) - { - continue; - } - - if (Vector2.DistanceSquared(previous, point) > StrokeMicroSegmentEpsilon * StrokeMicroSegmentEpsilon) - { - kept[keptCount++] = point; - } - - pointLike = point; + secondPoint = point; } - int segmentCount = keptCount - 1; - - // CPU stroker closed-contour normalization. - bool isClosed = false; - if (contour.PointCount >= 3) - { - PointF last = geometryPoints[contour.PointStart + contour.PointCount - 1]; - float closeThreshold = MathF.Max(strokeWidth, 1E-3F); - isClosed = contour.IsClosed || - firstPoint == last || - Vector2.DistanceSquared(firstPoint, last) <= closeThreshold * closeThreshold; - } + lastKept = point; + keptCount++; + } - bool duplicateClosingPoint = keptCount > 1 && kept[keptCount - 1] == kept[0]; - int distinctPointCount = keptCount - (isClosed && duplicateClosingPoint ? 1 : 0); + int segmentCount = keptCount - 1; - if (segmentCount == 0) - { - EncodePointStrokeContour(pointLike, pointTranslateX, pointTranslateY, ref pathTags, ref pathData); - Vector2 pointCenter = (Vector2)pointLike + translate; - Vector2 pointHalf = new(PointStrokeSegmentHalfLength, 0F); - tileCrossings += CountLineTileCrossings(pointCenter - pointHalf, pointCenter + pointHalf); + // Match the CPU closure rule. A contour with at least three source points is closed + // when it is declared closed or its endpoints are within one scaled stroke width. + bool isClosed = false; + if (contourPoints.Length >= 3) + { + PointF last = contourPoints[^1]; + float closeThreshold = MathF.Max(strokeWidth, 1E-3F); + isClosed = contour.IsClosed || + firstPoint == last || + Vector2.DistanceSquared(firstPoint, last) <= closeThreshold * closeThreshold; + } - encodedContourCount++; - continue; - } + bool duplicateClosingPoint = keptCount > 1 && lastKept == firstPoint; + int distinctPointCount = keptCount - (isClosed && duplicateClosingPoint ? 1 : 0); - if (segmentCount == 1 || distinctPointCount == 2) - { - // The CPU stroker emits these as one capped open segment even when declared closed. - EncodeOpenStrokeContour(kept.AsSpan(0, 2), pointTranslateX, pointTranslateY, ref pathTags, ref pathData); - tileCrossings += CountLineTileCrossings((Vector2)kept[0] + translate, (Vector2)kept[1] + translate); + if (segmentCount == 0) + { + EncodePointStrokeContour(pointLike, pointTranslateX, pointTranslateY, ref pathTags, ref pathData); + encodedContourCount++; + continue; + } - encodedContourCount++; - continue; - } + if (segmentCount == 1 || distinctPointCount == 2) + { + // The CPU stroker emits these as one capped open segment even when declared closed. + Span segmentPoints = [firstPoint, secondPoint]; + EncodeOpenStrokeContour(segmentPoints, pointTranslateX, pointTranslateY, ref pathTags, ref pathData); + encodedContourCount++; + continue; + } - if (isClosed) + if (isClosed) + { + int emitCount = duplicateClosingPoint ? keptCount - 1 : keptCount; + + // Emit the final line back to the first point only when that gap survived the + // micro-segment filter. A smaller gap is closed by the join alone. + bool closingSegment = duplicateClosingPoint || + (segmentCount > 1 && + Vector2.DistanceSquared(lastKept, firstPoint) > StrokeMicroSegmentEpsilon * StrokeMicroSegmentEpsilon); + + int linetoCount = (emitCount - 1) + (closingSegment ? 1 : 0); + Span contourData = pathData.GetAppendSpan(2 + (linetoCount * 2) + 2); + Span contourTags = pathTags.GetAppendSpan(linetoCount + 1); + + int dataIndex = 0; + int tagIndex = 0; + contourData[dataIndex++] = BitcastSingle(firstPoint.X + pointTranslateX); + contourData[dataIndex++] = BitcastSingle(firstPoint.Y + pointTranslateY); + + previousKept = firstPoint; + pointLike = firstPoint; + pointIndex = 1; + PointF lastEndpoint = firstPoint; + for (int j = 1; j < emitCount; j++) { - int emitCount = duplicateClosingPoint ? keptCount - 1 : keptCount; - - // A closing segment exists when the contour ends exactly on its first point or - // the gap back to it is a real (non micro) segment; a sub-1/64 gap is bridged - // by the wrap join, matching the CPU stroker. - bool closingSegment = duplicateClosingPoint || - (segmentCount > 1 && - Vector2.DistanceSquared(kept[keptCount - 1], kept[0]) > StrokeMicroSegmentEpsilon * StrokeMicroSegmentEpsilon); - - int linetoCount = (emitCount - 1) + (closingSegment ? 1 : 0); - Span contourData = pathData.GetAppendSpan(2 + (linetoCount * 2) + 2); - Span contourTags = pathTags.GetAppendSpan(linetoCount + 1); - - int dataIndex = 0; - int tagIndex = 0; - contourData[dataIndex++] = BitcastSingle(kept[0].X + pointTranslateX); - contourData[dataIndex++] = BitcastSingle(kept[0].Y + pointTranslateY); - for (int j = 1; j < emitCount; j++) - { - contourData[dataIndex++] = BitcastSingle(kept[j].X + pointTranslateX); - contourData[dataIndex++] = BitcastSingle(kept[j].Y + pointTranslateY); - contourTags[tagIndex++] = PackPathTag(PathTag.LineToF32); - } - - PointF lastEndpoint = kept[emitCount - 1]; - if (closingSegment) - { - contourData[dataIndex++] = BitcastSingle(kept[0].X + pointTranslateX); - contourData[dataIndex++] = BitcastSingle(kept[0].Y + pointTranslateY); - contourTags[tagIndex++] = PackPathTag(PathTag.LineToF32); - lastEndpoint = kept[0]; - } - - // Closed-stroke marker: carries the first segment's tangent and length so the - // wrap join at the closing corner matches the CPU stroker's join arguments. - contourData[dataIndex++] = BitcastSingle(lastEndpoint.X + (kept[1].X - kept[0].X) + pointTranslateX); - contourData[dataIndex] = BitcastSingle(lastEndpoint.Y + (kept[1].Y - kept[0].Y) + pointTranslateY); - contourTags[tagIndex] = PackPathTag(PathTag.LineToF32 | PathTag.SubpathEnd); - - pathData.Advance(contourData.Length); - pathTags.Advance(contourTags.Length); - - Vector2 current = (Vector2)kept[0] + translate; - for (int j = 1; j < emitCount; j++) - { - Vector2 next = (Vector2)kept[j] + translate; - tileCrossings += CountLineTileCrossings(current, next); - current = next; - } - - if (closingSegment) - { - tileCrossings += CountLineTileCrossings(current, (Vector2)kept[0] + translate); - } - - encodedContourCount++; - continue; + _ = TryGetNextStrokePoint(contourPoints, ref pointIndex, ref previousKept, ref pointLike, out PointF point); + contourData[dataIndex++] = BitcastSingle(point.X + pointTranslateX); + contourData[dataIndex++] = BitcastSingle(point.Y + pointTranslateY); + contourTags[tagIndex++] = PackPathTag(PathTag.LineToF32); + lastEndpoint = point; } - EncodeOpenStrokeContour(kept.AsSpan(0, keptCount), pointTranslateX, pointTranslateY, ref pathTags, ref pathData); - for (int j = 0; j < keptCount - 1; j++) + if (closingSegment) { - tileCrossings += CountLineTileCrossings((Vector2)kept[j] + translate, (Vector2)kept[j + 1] + translate); + contourData[dataIndex++] = BitcastSingle(firstPoint.X + pointTranslateX); + contourData[dataIndex++] = BitcastSingle(firstPoint.Y + pointTranslateY); + contourTags[tagIndex++] = PackPathTag(PathTag.LineToF32); + lastEndpoint = firstPoint; } + // The closing marker extends from the last emitted endpoint in the direction of + // the first segment. The shader uses this synthetic line only to calculate the + // wrap join; it does not emit another stroke body. + contourData[dataIndex++] = BitcastSingle(lastEndpoint.X + (secondPoint.X - firstPoint.X) + pointTranslateX); + contourData[dataIndex] = BitcastSingle(lastEndpoint.Y + (secondPoint.Y - firstPoint.Y) + pointTranslateY); + contourTags[tagIndex] = PackPathTag(PathTag.LineToF32 | PathTag.SubpathEnd); + + pathData.Advance(contourData.Length); + pathTags.Advance(contourTags.Length); + encodedContourCount++; + continue; } - finally + + int openLinetoCount = keptCount - 1; + Span openData = pathData.GetAppendSpan(2 + (openLinetoCount * 2) + 4); + Span openTags = pathTags.GetAppendSpan(openLinetoCount + 1); + int openDataIndex = 0; + int openTagIndex = 0; + openData[openDataIndex++] = BitcastSingle(firstPoint.X + pointTranslateX); + openData[openDataIndex++] = BitcastSingle(firstPoint.Y + pointTranslateY); + + previousKept = firstPoint; + pointLike = firstPoint; + pointIndex = 1; + while (TryGetNextStrokePoint(contourPoints, ref pointIndex, ref previousKept, ref pointLike, out PointF point)) { - ArrayPool.Shared.Return(kept); + openData[openDataIndex++] = BitcastSingle(point.X + pointTranslateX); + openData[openDataIndex++] = BitcastSingle(point.Y + pointTranslateY); + openTags[openTagIndex++] = PackPathTag(PathTag.LineToF32); } + + // The open marker stores the start point and one point on its outgoing tangent. QuadTo + // selects this four-word marker layout; it does not represent a curve in the shader. + PointF tangentPoint = GetStrokeMarkerTangentPoint(firstPoint, secondPoint); + openData[openDataIndex++] = BitcastSingle(firstPoint.X + pointTranslateX); + openData[openDataIndex++] = BitcastSingle(firstPoint.Y + pointTranslateY); + openData[openDataIndex++] = BitcastSingle(tangentPoint.X + pointTranslateX); + openData[openDataIndex] = BitcastSingle(tangentPoint.Y + pointTranslateY); + openTags[openTagIndex] = PackPathTag(PathTag.QuadToF32 | PathTag.SubpathEnd); + + pathData.Advance(openData.Length); + pathTags.Advance(openTags.Length); + encodedContourCount++; } if (encodedContourCount == 0) @@ -5052,13 +5468,54 @@ private static int EncodeStrokePath( return 0; } - // The loop counted centerline tiles; the GPU stroker emits offset sides, joins and caps, so - // scale the count up to the emitted geometry to avoid under-seeding the segment scratch. - tileCrossings = ScaleStrokeTileCrossings(tileCrossings, lineCount, strokeWidth); pathTags.Add(PackPathTag(PathTag.Path)); return 1; } + /// + /// Gets the next stroke contour point that survives exact-duplicate and micro-segment collapse. + /// + /// The source contour points. + /// The next source point index. + /// The last point that survived filtering. + /// The last non-duplicate source point, used when the contour collapses to a point. + /// Receives the next retained point. + /// when another point survives filtering. + private static bool TryGetNextStrokePoint( + ReadOnlySpan points, + ref int index, + ref PointF previousKept, + ref PointF pointLike, + out PointF point) + { + while (index < points.Length) + { + point = points[index++]; + + // Exact duplicates do not update pointLike because they provide no direction and do + // not change the location represented by a fully collapsed contour. + if (point == previousKept) + { + continue; + } + + pointLike = point; + + // A non-duplicate micro segment updates the representative point but does not become + // a stroke segment. Measure the next candidate from the last point that was retained. + if (Vector2.DistanceSquared(previousKept, point) <= StrokeMicroSegmentEpsilon * StrokeMicroSegmentEpsilon) + { + continue; + } + + previousKept = point; + return true; + } + + point = default; + return false; + } + /// /// Encodes one open (capped) stroke centerline contour from preprocessed points. /// @@ -5089,7 +5546,8 @@ private static void EncodeOpenStrokeContour( contourTags[tagIndex++] = PackPathTag(PathTag.LineToF32); } - // Open-stroke cap marker: stores the subpath start point and its outgoing tangent point. + // QuadTo selects the open-marker layout. These two points define a line tangent; no curve + // reaches the shader. PointF tangentPoint = GetStrokeMarkerTangentPoint(points[0], points[1]); contourData[dataIndex++] = BitcastSingle(points[0].X + pointTranslateX); contourData[dataIndex++] = BitcastSingle(points[0].Y + pointTranslateY); @@ -5116,6 +5574,8 @@ private static void EncodePointStrokeContour( ref OwnedStream pathTags, ref OwnedStream pathData) { + // A zero-length centreline has no tangent. Use the same short horizontal segment as the + // CPU point-stroke path so cap construction has a stable direction. PointF start = new(point.X - PointStrokeSegmentHalfLength, point.Y); PointF end = new(point.X + PointStrokeSegmentHalfLength, point.Y); PointF tangentEnd = GetStrokeMarkerTangentPoint(start, end); @@ -5147,8 +5607,7 @@ private static void EncodePointStrokeContour( /// The root target bounds used for target-local conversion. /// The path-tag stream. /// The path-data stream. - /// Receives the estimated flattened line workload for the stroke. - /// Receives the conservative emitted-stroke tile-crossing bound used to seed the scratch buffers. + /// Receives the estimated final line workload for the stroke. /// The number of encoded path objects; always 1. private static int EncodeOpenSegmentStrokePath( PointF start, @@ -5159,8 +5618,7 @@ private static int EncodeOpenSegmentStrokePath( in Rectangle rootTargetBounds, ref OwnedStream pathTags, ref OwnedStream pathData, - out int lineCount, - out long tileCrossings) + out int lineCount) { float pointTranslateX = destinationOffset.X - rootTargetBounds.X; float pointTranslateY = destinationOffset.Y - rootTargetBounds.Y; @@ -5169,12 +5627,6 @@ private static int EncodeOpenSegmentStrokePath( EncodeOpenStrokeContour(segmentPoints, pointTranslateX, pointTranslateY, ref pathTags, ref pathData); pathTags.Add(PackPathTag(PathTag.Path)); lineCount = EstimateStrokeLineCountForOpenSegment(pen, widthScale); - Vector2 translate = new(pointTranslateX, pointTranslateY); - tileCrossings = ScaleStrokeTileCrossings( - CountLineTileCrossings((Vector2)start + translate, (Vector2)end + translate), - lineCount, - pen.StrokeWidth * widthScale); - return 1; } @@ -5664,7 +6116,7 @@ private static void ReservePlainStrokeCapacityForOpenSegment( } /// - /// Estimates the flattened line workload for one stroke. + /// Estimates the final line workload for one stroke. /// /// The centerline geometry to estimate. /// The pen that defines stroke width, joins, and caps. @@ -5697,7 +6149,7 @@ private static int EstimateStrokeLineCount(LinearGeometry geometry, Pen pen, flo } /// - /// Estimates the flattened line workload for one explicit open two-point stroke segment. + /// Estimates the final line workload for one explicit open two-point stroke segment. /// /// The pen that defines stroke width, joins, and caps. /// The transform-derived scale applied to the stroke width. @@ -5707,7 +6159,7 @@ private static int EstimateStrokeLineCountForOpenSegment(Pen pen, float widthSca => Math.Max(2 + (GetStrokeCapLineCost(pen, widthScale) * 2), 1); /// - /// Returns the conservative flattened line cost of one stroke join. + /// Returns the conservative final line cost of one stroke join. /// /// The pen that defines the join style. /// The transform-derived scale applied to the stroke width. @@ -5728,7 +6180,7 @@ private static int GetStrokeJoinLineCost(Pen pen, float widthScale) } /// - /// Returns the conservative flattened line cost of one stroke cap. + /// Returns the conservative final line cost of one stroke cap. /// /// The pen that defines the cap style. /// The transform-derived scale applied to the stroke width. @@ -5746,7 +6198,7 @@ private static int GetStrokeCapLineCost(Pen pen, float widthScale) } /// - /// Returns the conservative flattened line cost of one round arc in the stroke shaders. + /// Returns the conservative final line cost of one round arc in the stroke shaders. /// /// The arc radius in pixels. /// The swept angle in radians. @@ -5913,12 +6365,10 @@ private static void AppendIdentityTransform(ref OwnedStream transforms) /// Packs the style words that describe stroke behavior for one draw record. /// /// - /// Word layout consumed by flatten.wgsl: 0 = style flags, 1 = device-space stroke width, - /// 2 = packed draw flags, 3 = miter limit, 4 = arc detail scale, 5 = coverage parameter, + /// Word layout consumed by path_lowering.wgsl: 0 = style flags, 1 = device-space stroke width, + /// 2 = packed draw flags, 3 = miter limit, 4 = arc detail scale, 5 = zero, /// 6-9 = target-local interest rectangle as left, top, right, bottom. - /// The coverage parameter carries the quantization threshold for aliased strokes and zero - /// for antialiased strokes: fine interprets the antialiased slot as the perceptual coverage - /// boost, which never applies to strokes. + /// Strokes do not use antialiased text coverage boost data, so word 5 is zero in both modes. /// /// The graphics options used for blending. /// The pen that defines stroke width, joins, caps, and miter limit. @@ -5952,7 +6402,7 @@ private static ( PackStyleDrawFlags(options), BitcastSingle((float)pen.StrokeOptions.MiterLimit), BitcastSingle((float)pen.StrokeOptions.ArcDetailScale), - BitcastSingle(options.Antialias ? 0F : options.AntialiasThreshold), + 0U, BitcastSingle(interestBounds.Left), BitcastSingle(interestBounds.Top), BitcastSingle(interestBounds.Right), @@ -5981,7 +6431,7 @@ private static float GetTransformWidthScale(Matrix4x4 transform) } /// - /// Packs the stroke join flags consumed by the staged flatten shader. + /// Packs the stroke join flags consumed by the staged path-lowering shader. /// /// The pen join style. /// The join style flags. @@ -5997,7 +6447,7 @@ private static StyleFlags EncodeStrokeJoinFlags(LineJoin lineJoin) }; /// - /// Packs the start and end cap flags consumed by the staged flatten shader. + /// Packs the start and end cap flags consumed by the staged path-lowering shader. /// /// The pen cap style, applied to both segment ends. /// The cap style flags. @@ -6017,9 +6467,8 @@ private static StyleFlags EncodeStrokeCapFlags(LineCap lineCap) /// Shares the stroke style word layout with the stroke-only words zeroed: 0 = style flags /// (the fill bit selects even-odd), 2 = packed draw flags, 5 = coverage parameter, /// 6-9 = target-local interest rectangle as left, top, right, bottom. - /// The coverage parameter is mode-dependent because the two uses are mutually exclusive: - /// aliased fills carry the quantization threshold, antialiased fills carry the perceptual - /// coverage boost applied by fine (zero for plain fills, non-zero only for text). + /// Antialiased fills carry the perceptual coverage boost applied by fine in word 5 + /// (zero for plain fills, non-zero only for text). Aliased fills leave word 5 zero. /// /// The graphics options used for blending. /// The fill rule for self-intersecting geometry. @@ -6044,14 +6493,19 @@ private static ( float coverageBoost = 0F) { StyleFlags styleFlags = intersectionRule == IntersectionRule.EvenOdd ? StyleFlags.Fill : StyleFlags.None; - float coverageParam = options.Antialias ? coverageBoost : options.AntialiasThreshold; + + // Antialiased fills carry the perceptual coverage boost as a float. Aliased fills sample + // the shape at pixel centres, a rule with no threshold and no boost, so their coverage + // word carries nothing. + uint coverageWord = options.Antialias ? BitcastSingle(coverageBoost) : 0U; + return ( (uint)styleFlags, 0U, PackStyleDrawFlags(options), 0U, 0U, - BitcastSingle(coverageParam), + coverageWord, BitcastSingle(interestBounds.Left), BitcastSingle(interestBounds.Top), BitcastSingle(interestBounds.Right), @@ -6059,7 +6513,7 @@ private static ( } /// - /// Packs the draw-flags word carried through flatten into coarse and fine. + /// Packs the draw-flags word carried through path lowering into coarse and fine. /// /// The graphics options supplying blend mode, blend percentage, and antialiasing. /// The packed draw-flags word: blend mode in bits 1-13, alpha in bits 14-29, and the aliased bit. @@ -6251,14 +6705,10 @@ private static void AppendBeginClipData(GraphicsOptions options, ref OwnedStream /// Appends the draw-data payload for an ordinary clip record. /// /// The clip set operation; Difference sets the mask-inversion bit. - /// The clip edge mode; hard edges set the hard-mask bit. - /// The coverage threshold used for hard-edge clips. /// The base clip blend word. /// The draw-data stream. private static void AppendClipBeginData( ClipOperation operation, - DrawingClipEdgeMode edgeMode, - float antialiasThreshold, uint clipBlendMode, ref OwnedStream drawData) { @@ -6268,13 +6718,8 @@ private static void AppendClipBeginData( ? clipBlendMode | ClipDifferenceMaskBit : clipBlendMode; - if (edgeMode == DrawingClipEdgeMode.Hard) - { - blendMode |= ClipHardMaskBit; - } - drawData.Add(blendMode); - drawData.Add(BitcastSingle(edgeMode == DrawingClipEdgeMode.Hard ? antialiasThreshold : 1F)); + drawData.Add(BitcastSingle(1F)); } /// @@ -6708,6 +7153,27 @@ private static uint MapExtendMode(GradientRepetitionMode repetitionMode) /// internal sealed class WebGPUEncodedScene : IDisposable { + /// + /// Gets the word offset of the segment-to-profile table, or zero when absent. + /// + public uint ProfileSlotsBase { get; private set; } + + /// + /// Gets the word offset of the profile range and contour-link table, or zero when absent. + /// + public uint ProfileRecordsBase { get; private set; } + + /// + /// Sets the two profile-table offsets for the encoded scene. + /// + /// The word offset of the segment-to-profile table. + /// The word offset of the profile range and contour-link table. + public void SetProfileRegions(uint slotsBase, uint recordsBase) + { + this.ProfileSlotsBase = slotsBase; + this.ProfileRecordsBase = recordsBase; + } + /// /// Gets the empty encoded scene instance. /// @@ -6738,8 +7204,6 @@ internal sealed class WebGPUEncodedScene : IDisposable 0, 0, 0, - RasterizationMode.Antialiased, - 0F, 0L, 0L, []); @@ -6781,8 +7245,6 @@ internal sealed class WebGPUEncodedScene : IDisposable /// The total scheduled path-row count. /// The horizontal tile count. /// The vertical tile count. - /// The fine-pass rasterization mode. - /// The scene-wide aliased coverage threshold. /// The CPU-side upper bound for tile-boundary crossings. /// The CPU-side upper bound for per-(draw, bin) binning records. /// The scene operations associated with the encoded payload. @@ -6813,8 +7275,6 @@ public WebGPUEncodedScene( int totalPathRowCount, int tileCountX, int tileCountY, - RasterizationMode fineRasterizationMode, - float fineCoverageThreshold, long estimatedTileCrossings, long estimatedBinFootprint, WebGPUSceneOperation[] operations) @@ -6846,8 +7306,6 @@ public WebGPUEncodedScene( this.TotalPathRowCount = totalPathRowCount; this.TileCountX = tileCountX; this.TileCountY = tileCountY; - this.FineRasterizationMode = fineRasterizationMode; - this.FineCoverageThreshold = fineCoverageThreshold; this.EstimatedTileCrossings = estimatedTileCrossings; this.EstimatedBinFootprint = estimatedBinFootprint; @@ -6931,7 +7389,7 @@ public WebGPUEncodedScene( /// /// Gets the CPU-side upper-bound estimate of tile-boundary crossings produced by the scene's - /// flattened lines. Bounds the GPU segment, segment-count, and path-tile demand. + /// final lines. Bounds the GPU segment, segment-count, and path-tile demand. /// public long EstimatedTileCrossings { get; } @@ -7134,16 +7592,6 @@ public ReadOnlyMemory PathGradientData /// public int TileCountY { get; } - /// - /// Gets the fine-pass rasterization mode selected for this flush. - /// - public RasterizationMode FineRasterizationMode { get; } - - /// - /// Gets the scene-wide aliased coverage threshold consumed by the fine pass. - /// - public float FineCoverageThreshold { get; } - /// /// Gets the total tile count. /// @@ -7383,8 +7831,58 @@ public GpuRecolorDescriptor(Color sourceColor, Color targetColor, float threshol } /// -/// Represents one 2D affine transform encoded in the GPU scene format. +/// Identifies one aliased fill that needs profile data in the final GPU scene. /// +internal readonly struct GpuProfileFillRecord +{ + /// + /// Initializes a new instance of the struct. + /// + /// The fill's first path-data word, relative to its partition's path-data stream. + /// The fill's linear geometry. + /// The fill's partition-owned profile view. + /// The X translation from geometry space to absolute target space. + /// The Y translation from geometry space to absolute target space. + public GpuProfileFillRecord( + int pathDataOffset, + LinearGeometry geometry, + LinearGeometryProfiles profiles, + float translateX, + float translateY) + { + this.PathDataOffset = pathDataOffset; + this.Geometry = geometry; + this.Profiles = profiles; + this.TranslateX = translateX; + this.TranslateY = translateY; + } + + /// + /// Gets the fill's first path-data word, relative to its partition's path-data stream. + /// + public int PathDataOffset { get; } + + /// + /// Gets the fill's linear geometry. + /// + public LinearGeometry Geometry { get; } + + /// + /// Gets the fill's profile data in its partition-owned arena. + /// + public LinearGeometryProfiles Profiles { get; } + + /// + /// Gets the X translation from geometry space to absolute target space. + /// + public float TranslateX { get; } + + /// + /// Gets the Y translation from geometry space to absolute target space. + /// + public float TranslateY { get; } +} + internal readonly struct GpuSceneTransform : IEquatable { /// diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneResources.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneResources.cs index c3c87dfc6..722e2f044 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneResources.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneResources.cs @@ -475,7 +475,8 @@ public static GpuSceneConfig CreateHeader(WebGPUEncodedScene scene, WebGPUSceneC config.BumpSizes.Segments, config.BumpSizes.BlendSpill, config.BumpSizes.Ptcl, - scene.FineCoverageThreshold); + scene.ProfileSlotsBase, + scene.ProfileRecordsBase); } /// @@ -528,7 +529,8 @@ public static GpuSceneConfig CreateHeader(WebGPUEncodedScene scene, WebGPUSceneR config.BumpSizes.Segments, config.BumpSizes.BlendSpill, config.BumpSizes.Ptcl, - scene.FineCoverageThreshold); + scene.ProfileSlotsBase, + scene.ProfileRecordsBase); } /// @@ -731,7 +733,6 @@ private static bool TryCreateGradientTexture( } const TextureUsage usage = TextureUsage.TextureBinding | TextureUsage.CopyDst; - WGPUTextureImpl* texture; // Prefer a device-pooled texture. Extra rows in a larger entry are never addressed: the // fine shader loads gradient texels by exact ramp row index. @@ -740,7 +741,7 @@ private static bool TryCreateGradientTexture( (ulong)usage, 512, (uint)scene.GradientRowCount, - out texture, + out WGPUTextureImpl* texture, out textureView, out uint rentedWidth, out uint rentedHeight)) @@ -1006,12 +1007,10 @@ private static bool TryUploadImageBrushEntry( size = totalByteLength }; - using (WebGPUHandle.HandleReference deviceReference = flushContext.DeviceHandle.AcquireReference()) - { - WGPUBufferImpl* buffer = flushContext.Api.DeviceCreateBuffer((WGPUDeviceImpl*)deviceReference.Handle, in descriptor); - UploadBrushData(flushContext, buffer, infoWordCount, scene); - return buffer; - } + using WebGPUHandle.HandleReference deviceReference = flushContext.DeviceHandle.AcquireReference(); + WGPUBufferImpl* buffer = flushContext.Api.DeviceCreateBuffer((WGPUDeviceImpl*)deviceReference.Handle, in descriptor); + UploadBrushData(flushContext, buffer, infoWordCount, scene); + return buffer; } /// @@ -1328,18 +1327,16 @@ private static uint PackImageSampleInfo( size = byteLength }; - using (WebGPUHandle.HandleReference deviceReference = flushContext.DeviceHandle.AcquireReference()) - { - WGPUBufferImpl* buffer = flushContext.Api.DeviceCreateBuffer((WGPUDeviceImpl*)deviceReference.Handle, in descriptor); - using WebGPUHandle.HandleReference queueReference = flushContext.QueueHandle.AcquireReference(); - flushContext.Api.QueueWriteBuffer( - (WGPUQueueImpl*)queueReference.Handle, - buffer, - 0, - Unsafe.AsPointer(ref Unsafe.AsRef(in value)), - byteLength); - return buffer; - } + using WebGPUHandle.HandleReference deviceReference = flushContext.DeviceHandle.AcquireReference(); + WGPUBufferImpl* buffer = flushContext.Api.DeviceCreateBuffer((WGPUDeviceImpl*)deviceReference.Handle, in descriptor); + using WebGPUHandle.HandleReference queueReference = flushContext.QueueHandle.AcquireReference(); + flushContext.Api.QueueWriteBuffer( + (WGPUQueueImpl*)queueReference.Handle, + buffer, + 0, + Unsafe.AsPointer(ref Unsafe.AsRef(in value)), + byteLength); + return buffer; } /// @@ -1368,26 +1365,24 @@ private static uint PackImageSampleInfo( size = byteLength }; - using (WebGPUHandle.HandleReference deviceReference = flushContext.DeviceHandle.AcquireReference()) + using WebGPUHandle.HandleReference deviceReference = flushContext.DeviceHandle.AcquireReference(); + WGPUBufferImpl* buffer = flushContext.Api.DeviceCreateBuffer((WGPUDeviceImpl*)deviceReference.Handle, in descriptor); + if (!values.IsEmpty) { - WGPUBufferImpl* buffer = flushContext.Api.DeviceCreateBuffer((WGPUDeviceImpl*)deviceReference.Handle, in descriptor); - if (!values.IsEmpty) + nuint uploadByteLength = checked((nuint)values.Length * (nuint)Unsafe.SizeOf()); + using WebGPUHandle.HandleReference queueReference = flushContext.QueueHandle.AcquireReference(); + fixed (T* dataPtr = values) { - nuint uploadByteLength = checked((nuint)values.Length * (nuint)Unsafe.SizeOf()); - using WebGPUHandle.HandleReference queueReference = flushContext.QueueHandle.AcquireReference(); - fixed (T* dataPtr = values) - { - flushContext.Api.QueueWriteBuffer( - (WGPUQueueImpl*)queueReference.Handle, - buffer, - 0, - dataPtr, - uploadByteLength); - } + flushContext.Api.QueueWriteBuffer( + (WGPUQueueImpl*)queueReference.Handle, + buffer, + 0, + dataPtr, + uploadByteLength); } - - return buffer; } + + return buffer; } /// @@ -1439,8 +1434,8 @@ internal readonly unsafe struct WebGPUSceneResourceSet /// The clip bic (stack-monoid) reduction buffer. /// The clip bounding-box buffer. /// The draw bounding-box buffer. - /// The flattened path buffer. - /// The flattened line buffer. + /// The per-path scheduling buffer. + /// The final device-space line buffer. /// The gradient texture view. /// The image atlas texture view. public WebGPUSceneResourceSet( @@ -1566,7 +1561,7 @@ public WebGPUSceneResourceSet( public WGPUBufferImpl* PathBuffer { get; } /// - /// Gets the flattened line buffer. + /// Gets the final device-space line buffer. /// public WGPUBufferImpl* LineBuffer { get; } @@ -1617,7 +1612,7 @@ internal sealed unsafe class WebGPUSceneResourceArena /// The clip bounding-box buffer. /// The draw bounding-box buffer. /// The per-path scheduling buffer. - /// The flattened line buffer. + /// The final device-space line buffer. public WebGPUSceneResourceArena( WebGPU api, WebGPUDeviceHandle device, @@ -1772,7 +1767,7 @@ public WebGPUSceneResourceArena( public WGPUBufferImpl* PathBuffer { get; private set; } /// - /// Gets the flattened line buffer. + /// Gets the final device-space line buffer. /// public WGPUBufferImpl* LineBuffer { get; private set; } @@ -1935,7 +1930,7 @@ internal struct GpuSceneBumpAllocators public uint BlendSpill; /// - /// The flattened line record allocation head. + /// The final line record allocation head. /// public uint Lines; } @@ -1987,7 +1982,7 @@ public GpuTagMonoid(uint transIndex, uint pathSegmentOffset, uint styleIndex, ui } /// -/// Per-path bounding box and scheduling data written by the flatten pass. +/// Per-path bounding box and scheduling data written by the path-lowering pass. /// /// /// Mirrors PathBbox in Shared/bbox.wgsl; field order and the 48-byte stride must @@ -2006,7 +2001,7 @@ internal readonly struct GpuPathBbox /// The bottom edge of the transformed integer path bounds. /// The draw flags associated with the path. /// The transform index associated with the path. - /// The aliased coverage threshold for the path. + /// The antialiased coverage adjustment for the path. /// The reserved layout slot matching PathBbox._padding in bbox.wgsl. /// The root-target-local raster interest rectangle. public GpuPathBbox( @@ -2016,7 +2011,7 @@ public GpuPathBbox( int y1, uint drawFlags, uint transIndex, - float coverageThreshold, + float coverageData, uint padding, Vector4 interest) { @@ -2026,7 +2021,7 @@ public GpuPathBbox( this.Y1 = y1; this.DrawFlags = drawFlags; this.TransIndex = transIndex; - this.CoverageThreshold = coverageThreshold; + this.CoverageData = coverageData; this.Padding = padding; this.Interest = interest; } @@ -2062,9 +2057,9 @@ public GpuPathBbox( public uint TransIndex { get; } /// - /// Gets the aliased coverage threshold for this path. + /// Gets the antialiased coverage adjustment for this path. /// - public float CoverageThreshold { get; } + public float CoverageData { get; } /// /// Gets the reserved layout slot matching PathBbox._padding in bbox.wgsl. @@ -2413,7 +2408,7 @@ internal readonly struct GpuSceneConfig /// The number of real tile rows rendered by this attempt. /// The packed RGBA8 base color applied by the fine pass. /// The scene-buffer layout metadata. - /// The flattened line buffer capacity in elements. + /// The final line buffer capacity in elements. /// The bin-data scratch capacity in words. /// The sparse path-row buffer capacity in elements. /// The path-tile buffer capacity in elements. @@ -2421,7 +2416,8 @@ internal readonly struct GpuSceneConfig /// The segment buffer capacity in elements. /// The blend-spill buffer capacity in slots. /// The PTCL buffer capacity in words. - /// The scene-wide aliased coverage threshold. + /// The word offset of the segment-to-profile table, or zero when absent. + /// The word offset of the profile range and contour-link table, or zero when absent. public GpuSceneConfig( uint widthInTiles, uint heightInTiles, @@ -2439,8 +2435,14 @@ public GpuSceneConfig( uint segmentsSize, uint blendSize, uint ptclSize, - float fineCoverageThreshold) + uint profileSlotsBase, + uint profileRecordsBase) { + this.ProfileSlotsBase = profileSlotsBase; + this.ProfileRecordsBase = profileRecordsBase; + this.profilePad0 = 0; + this.profilePad1 = 0; + this.profilePad2 = 0; this.WidthInTiles = widthInTiles; this.HeightInTiles = heightInTiles; this.TargetWidth = targetWidth; @@ -2457,7 +2459,6 @@ public GpuSceneConfig( this.SegmentsSize = segmentsSize; this.BlendSize = blendSize; this.PtclSize = ptclSize; - this.FineCoverageThreshold = fineCoverageThreshold; } /// @@ -2501,7 +2502,7 @@ public GpuSceneConfig( public GpuSceneLayout Layout { get; } /// - /// Gets the flattened line buffer capacity in elements. + /// Gets the final line buffer capacity in elements. /// public uint LinesSize { get; } @@ -2541,9 +2542,20 @@ public GpuSceneConfig( public uint PtclSize { get; } /// - /// Gets the scene-wide coverage threshold consumed by the aliased fine pass. + /// Gets the word offset of the segment-to-profile table, or zero when absent. + /// + public uint ProfileSlotsBase { get; } + + /// + /// Gets the word offset of the profile range and contour-link table, or zero when absent. /// - public float FineCoverageThreshold { get; } + public uint ProfileRecordsBase { get; } + + // Config in config.wgsl declares three final pad words so its size is a multiple of 16 bytes. + // Declare the same words after the auto-properties to preserve the uniform buffer layout. + private readonly uint profilePad0; + private readonly uint profilePad1; + private readonly uint profilePad2; } /// @@ -2555,9 +2567,8 @@ public GpuSceneConfig( [StructLayout(LayoutKind.Sequential)] internal readonly struct GpuScenePath { - // Path in tile.wgsl has a vec4 followed by one u32 and therefore a - // 32-byte array stride. The padding keeps reusable GPU buffer sizing exact. - private readonly uint padding0; + // Path in tile.wgsl has a 16-byte-aligned vec4 followed by two u32 values. WGSL rounds + // the 24-byte payload up to a 32-byte array stride, so these fields represent the final 8 bytes. private readonly uint padding1; private readonly uint padding2; @@ -2569,14 +2580,15 @@ internal readonly struct GpuScenePath /// The maximum x of the path bounds in tiles. /// The maximum y of the path bounds in tiles. /// The first sparse row record owned by the path. - public GpuScenePath(uint bboxMinX, uint bboxMinY, uint bboxMaxX, uint bboxMaxY, uint rowOffset) + /// The path flags. Bit zero means that the original path starts left of its clipped drawing bounds. + public GpuScenePath(uint bboxMinX, uint bboxMinY, uint bboxMaxX, uint bboxMaxY, uint rowOffset, uint flags) { this.BboxMinX = bboxMinX; this.BboxMinY = bboxMinY; this.BboxMaxX = bboxMaxX; this.BboxMaxY = bboxMaxY; this.RowOffset = rowOffset; - this.padding0 = 0; + this.Flags = flags; this.padding1 = 0; this.padding2 = 0; } @@ -2605,6 +2617,11 @@ public GpuScenePath(uint bboxMinX, uint bboxMinY, uint bboxMaxX, uint bboxMaxY, /// Gets the first sparse row record owned by this path. /// public uint RowOffset { get; } + + /// + /// Gets the path flags. Bit zero means that the original path starts left of its clipped drawing bounds. + /// + public uint Flags { get; } } /// @@ -2654,7 +2671,7 @@ public GpuPathRow(uint minTileX, uint maxTileX, int backdrop, uint tileOffset) } /// -/// Flattened line record emitted from the path stream for segment-counting and tiling. +/// Final device-space line record emitted from the path stream for segment-counting and tiling. /// /// /// Mirrors LineSoup in Shared/segment.wgsl; field order and the 24-byte stride must match. @@ -2740,7 +2757,7 @@ internal readonly struct GpuSegmentCount /// /// Initializes a new instance of the struct. /// - /// The index of the source line in the flattened line buffer. + /// The index of the source line in the final line buffer. /// The packed segment indices; see . public GpuSegmentCount(uint lineIndex, uint counts) { @@ -2749,7 +2766,7 @@ public GpuSegmentCount(uint lineIndex, uint counts) } /// - /// Gets the index of the source line in the flattened line buffer. + /// Gets the index of the source line in the final line buffer. /// public uint LineIndex { get; } diff --git a/src/ImageSharp.Drawing/CubicBezierLineSegment.cs b/src/ImageSharp.Drawing/CubicBezierLineSegment.cs index 540a69d34..acd6d747b 100644 --- a/src/ImageSharp.Drawing/CubicBezierLineSegment.cs +++ b/src/ImageSharp.Drawing/CubicBezierLineSegment.cs @@ -15,16 +15,10 @@ public sealed class CubicBezierLineSegment : ILineSegment // Code for this taken from /// - /// The squared distance between span endpoints below which subdivision stops. + /// The squared midpoint-to-chord error accepted by the curve flattener. The linear geometry + /// must locate pixel-centre crossings to within one sixty-fourth of a device pixel. /// - private const float MinimumSqrDistance = 1.75f; - - /// - /// The dot-product threshold used to decide whether a span is flat enough. Directions from the - /// midpoint to each endpoint are nearly opposite (dot close to -1) when the span is straight; - /// values above this threshold indicate curvature that requires further subdivision. - /// - private const float DivisionThreshold = -.9995f; + private const float FlatnessSqrTolerance = (1F / 64F) * (1F / 64F); /// /// The bezier control points; the length is a multiple of 3 plus 1. @@ -179,7 +173,7 @@ private static PointF[] FlattenCurve(PointF[] controlPoints, Vector2 scale) output.Add((PointF)p0); } - SubdivideAndAppend(0F, 1F, p0, p1, p2, p3, ref output, 0); + SubdivideAndAppend(0F, 1F, p0, p3, p0, p1, p2, p3, ref output, 0); output.Add((PointF)p3); } @@ -191,6 +185,8 @@ private static PointF[] FlattenCurve(PointF[] controlPoints, Vector2 scale) /// /// The curve parameter at the start of the span. /// The curve parameter at the end of the span. + /// The curve point at , carried down so each level evaluates one new point. + /// The curve point at , carried down likewise. /// The start point of the cubic. /// The first control point of the cubic. /// The second control point of the cubic. @@ -200,6 +196,8 @@ private static PointF[] FlattenCurve(PointF[] controlPoints, Vector2 scale) private static void SubdivideAndAppend( float t0, float t1, + Vector2 left, + Vector2 right, Vector2 p0, Vector2 p1, Vector2 p2, @@ -207,15 +205,9 @@ private static void SubdivideAndAppend( ref FlattenedPointBuilder output, int depth) { - if (depth > 999) - { - return; - } - - Vector2 left = CalculateBezierPoint(t0, p0, p1, p2, p3); - Vector2 right = CalculateBezierPoint(t1, p0, p1, p2, p3); - - if ((left - right).LengthSquared() < MinimumSqrDistance) + // Normal finite curves reach the flatness test first. The limit prevents unbounded + // recursion for extreme input. + if (depth > 20) { return; } @@ -223,15 +215,18 @@ private static void SubdivideAndAppend( float midT = (t0 + t1) / 2; Vector2 mid = CalculateBezierPoint(midT, p0, p1, p2, p3); - Vector2 leftDirection = Vector2.Normalize(left - mid); - Vector2 rightDirection = Vector2.Normalize(right - mid); - - if (Vector2.Dot(leftDirection, rightDirection) > DivisionThreshold || Math.Abs(midT - 0.5f) < 0.0001f) + // Compare the curve midpoint with the midpoint of the candidate line. An S-shaped cubic + // can cross its chord at the root midpoint while still bending on both sides. Subdivide the + // root once before trusting this test so the two bends are tested separately. + Vector2 chordMid = (left + right) * 0.5F; + if (depth > 0 && (mid - chordMid).LengthSquared() <= FlatnessSqrTolerance) { - SubdivideAndAppend(t0, midT, p0, p1, p2, p3, ref output, depth + 1); - output.Add((PointF)mid); - SubdivideAndAppend(midT, t1, p0, p1, p2, p3, ref output, depth + 1); + return; } + + SubdivideAndAppend(t0, midT, left, mid, p0, p1, p2, p3, ref output, depth + 1); + output.Add((PointF)mid); + SubdivideAndAppend(midT, t1, mid, right, p0, p1, p2, p3, ref output, depth + 1); } /// diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj index 60d2088df..51696823f 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -40,7 +40,11 @@ - + + + + + diff --git a/src/ImageSharp.Drawing/LinearGeometryProfiles.cs b/src/ImageSharp.Drawing/LinearGeometryProfiles.cs new file mode 100644 index 000000000..47e451d6e --- /dev/null +++ b/src/ImageSharp.Drawing/LinearGeometryProfiles.cs @@ -0,0 +1,502 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Drawing; + +/// +/// Provides the contour profile data used by aliased thin-feature recovery. +/// +/// +/// A profile is a consecutive run of contour segments that continues in one direction on one +/// axis. A change from left to right, right to left, up to down, or down to up starts a new +/// profile. The rasterizer uses the run bounds and contour links to identify a terminating tip. +/// +internal readonly struct LinearGeometryProfiles +{ + private const int RecordStride = 3; + private const int HeaderWordCount = 3; + private const int SegmentCountOffset = 0; + private const int XProfileCountOffset = 1; + private const int YProfileCountOffset = 2; + + private readonly LinearGeometryProfileBuffer? buffer; + private readonly int offset; + private readonly int length; + + /// + /// The reserved identifier for a segment whose profile cannot be represented in sixteen bits. + /// + public const int SentinelId = ushort.MaxValue; + + /// + /// The segment tag whose X and Y fields both contain . + /// + public const uint SentinelTag = uint.MaxValue; + + /// + /// Initializes a new instance of the struct. + /// + /// The shared backing buffer that contains the packed profile words. + /// The first word of the profile entry. + /// The number of words in the profile entry. + internal LinearGeometryProfiles(LinearGeometryProfileBuffer buffer, int offset, int length) + { + this.buffer = buffer; + this.offset = offset; + this.length = length; + } + + /// + /// Gets a value indicating whether this view contains profile data. + /// + internal bool IsEmpty => this.buffer is null; + + /// + /// Gets the number of X-axis profiles. + /// + public int XProfileCount => (int)this.GetData()[XProfileCountOffset]; + + /// + /// Gets the number of Y-axis profiles. + /// + public int YProfileCount => (int)this.GetData()[YProfileCountOffset]; + + /// + /// Gets the X-axis and Y-axis profile identifiers for one source segment. + /// + /// The source segment index. + /// The X identifier in the low sixteen bits and the Y identifier in the high sixteen bits. + public uint GetSegmentTag(int index) => this.GetData()[HeaderWordCount + index]; + + /// + /// Gets one X-axis profile record. + /// + /// The profile index. + /// Receives the lowest coordinate reached by the profile. + /// Receives the highest coordinate reached by the profile. + /// Receives the packed contour adjacency data. + public void GetXProfile(int index, out float minimum, out float maximum, out int link) + { + ReadOnlySpan data = this.GetData(); + GetProfile(data, HeaderWordCount + (int)data[SegmentCountOffset], index, out minimum, out maximum, out link); + } + + /// + /// Gets one Y-axis profile record. + /// + /// The profile index. + /// Receives the lowest coordinate reached by the profile. + /// Receives the highest coordinate reached by the profile. + /// Receives the packed contour adjacency data. + public void GetYProfile(int index, out float minimum, out float maximum, out int link) + { + ReadOnlySpan data = this.GetData(); + int baseOffset = HeaderWordCount + (int)data[SegmentCountOffset] + ((int)data[XProfileCountOffset] * RecordStride); + GetProfile(data, baseOffset, index, out minimum, out maximum, out link); + } + + /// + /// Gets the maximum storage needed to analyze one geometry. + /// + /// The geometry to measure. + /// The maximum number of packed words required by the analysis. + internal static int GetMaximumWordCount(LinearGeometry geometry) + { + int segmentCount = geometry.Info.SegmentCount; + int maximumProfilesPerAxis = Math.Min(segmentCount, SentinelId); + return checked(HeaderWordCount + segmentCount + (maximumProfilesPerAxis * RecordStride * 2)); + } + + /// + /// Analyzes one geometry into caller-owned storage. + /// + /// + /// Each source segment contributes one tag. Each axis profile contributes its minimum, + /// maximum, and contour link. The method walks the contours once. It writes Y records after + /// the maximum possible X region, then moves them beside the X records when the actual X count + /// is known. This small contiguous move avoids a second contour walk. + /// + /// The geometry to analyze. + /// Storage with at least words. + /// The number of words written. + internal static int Analyze(LinearGeometry geometry, Span data) + { + int segmentCount = geometry.Info.SegmentCount; + int maximumProfilesPerAxis = Math.Min(segmentCount, SentinelId); + int segmentTagOffset = HeaderWordCount; + int xProfileOffset = segmentTagOffset + segmentCount; + int yProfileWorkingOffset = xProfileOffset + (maximumProfilesPerAxis * RecordStride); + + ProfileWriter x = new(data.Slice(xProfileOffset, maximumProfilesPerAxis * RecordStride), maximumProfilesPerAxis); + ProfileWriter y = new(data.Slice(yProfileWorkingOffset, maximumProfilesPerAxis * RecordStride), maximumProfilesPerAxis); + + int segmentOrdinal = 0; + LinearContour[] contours = (LinearContour[])geometry.Contours; + PointF[] geometryPoints = (PointF[])geometry.Points; + for (int i = 0; i < contours.Length; i++) + { + LinearContour contour = contours[i]; + if (contour.SegmentCount == 0) + { + continue; + } + + ReadOnlySpan points = geometryPoints.AsSpan(contour.PointStart, contour.PointCount); + PointF current = points[0]; + int xFirst = x.StartRun(current.X, 0); + int yFirst = y.StartRun(current.Y, 0); + + for (int j = 0; j < contour.SegmentCount; j++) + { + int endPointIndex = (j + 1) == points.Length ? 0 : j + 1; + PointF end = points[endPointIndex]; + + x.Advance(current.X, end.X); + y.Advance(current.Y, end.Y); + + // A reversing segment starts the new run. Read the identifiers after Advance so + // the segment receives the profile that contains its movement. + data[segmentTagOffset + segmentOrdinal++] = (uint)(x.CurrentId | (y.CurrentId << 16)); + current = end; + } + + x.EndContour(contour.IsClosed, xFirst); + y.EndContour(contour.IsClosed, yFirst); + } + + int xProfileCount = x.Count; + int yProfileCount = y.Count; + int yProfileOffset = xProfileOffset + (xProfileCount * RecordStride); + + // Span.CopyTo supports overlap. Compact only the Y records; tags and X records are already + // in their final positions. + data.Slice(yProfileWorkingOffset, yProfileCount * RecordStride).CopyTo(data[yProfileOffset..]); + data[SegmentCountOffset] = (uint)segmentCount; + data[XProfileCountOffset] = (uint)xProfileCount; + data[YProfileCountOffset] = (uint)yProfileCount; + return yProfileOffset + (yProfileCount * RecordStride); + } + + /// + /// Gets the packed words in this profile view. + /// + /// The packed profile entry. + private ReadOnlySpan GetData() + { + if (this.buffer is LinearGeometryProfileBuffer buffer) + { + return buffer.GetSpan(this.offset, this.length); + } + + return []; + } + + /// + /// Decodes one packed profile record. + /// + /// The complete packed profile entry. + /// The first word of the selected axis table. + /// The profile index. + /// Receives the minimum coordinate. + /// Receives the maximum coordinate. + /// Receives the contour adjacency data. + private static void GetProfile( + ReadOnlySpan data, + int baseOffset, + int index, + out float minimum, + out float maximum, + out int link) + { + int recordOffset = baseOffset + (index * RecordStride); + minimum = BitConverter.Int32BitsToSingle((int)data[recordOffset]); + maximum = BitConverter.Int32BitsToSingle((int)data[recordOffset + 1]); + link = (int)data[recordOffset + 2]; + } + + /// + /// Builds the profiles for one coordinate axis. + /// + private ref struct ProfileWriter + { + private readonly Span records; + private readonly int capacity; + private int direction; + private float minimum; + private float maximum; + + /// + /// Initializes a new instance of the struct. + /// + /// The destination profile records. + /// The number of records available. + public ProfileWriter(Span records, int capacity) + { + this.records = records; + this.capacity = capacity; + this.direction = 0; + this.minimum = 0F; + this.maximum = 0F; + this.Count = 0; + this.CurrentId = SentinelId; + } + + /// + /// Gets the number of profiles written. + /// + public int Count { get; private set; } + + /// + /// Gets the current profile identifier, or when the table is full. + /// + public int CurrentId { get; private set; } + + /// + /// Starts one monotone run. + /// + /// The first coordinate in the run. + /// One when the run follows the preceding run in the contour; otherwise zero. + /// The profile identifier, or when the table is full. + public int StartRun(float position, int connectedFlag) + { + if (this.Count >= this.capacity) + { + this.CurrentId = SentinelId; + return SentinelId; + } + + this.CurrentId = this.Count; + + // Bit zero identifies the normal connection to the preceding profile. The remaining + // bits are populated by EndContour only when a closed contour joins across its end. + this.records[(this.Count * RecordStride) + 2] = (uint)connectedFlag; + this.Count++; + this.direction = 0; + this.minimum = position; + this.maximum = position; + return this.CurrentId; + } + + /// + /// Adds one segment to the current run and opens a new run when the direction reverses. + /// + /// The segment start coordinate. + /// The segment end coordinate. + public void Advance(float from, float to) + { + float delta = to - from; + if (delta != 0F) + { + int segmentDirection = delta > 0F ? 1 : -1; + if (this.direction != 0 && segmentDirection != this.direction) + { + // The reversing segment belongs to the new run. Both runs include the shared + // extremum so the stub test can compare their closed coordinate ranges. + this.CloseRun(); + _ = this.StartRun(from, 1); + } + + this.direction = segmentDirection; + } + + if (to < this.minimum) + { + this.minimum = to; + } + else if (to > this.maximum) + { + this.maximum = to; + } + } + + /// + /// Closes the final run and records the connection across a closed contour's endpoint. + /// + /// Whether the contour returns to its first point. + /// The first profile identifier in the contour. + public void EndContour(bool isClosed, int firstId) + { + int lastId = this.CurrentId; + this.CloseRun(); + this.CurrentId = SentinelId; + + if (!isClosed || firstId == SentinelId || lastId == SentinelId || firstId == lastId) + { + return; + } + + int firstLinkOffset = (firstId * RecordStride) + 2; + int lastLinkOffset = (lastId * RecordStride) + 2; + + // Closing links use one-based identifiers, which leaves zero available for an open + // contour with no connection across its endpoint. + this.records[firstLinkOffset] = (this.records[firstLinkOffset] & 1U) | (uint)((lastId + 1) << 1); + this.records[lastLinkOffset] = (this.records[lastLinkOffset] & 1U) | (uint)((firstId + 1) << 1); + } + + /// + /// Writes the current run's inclusive coordinate range. + /// + private readonly void CloseRun() + { + if (this.CurrentId == SentinelId) + { + return; + } + + int recordOffset = this.CurrentId * RecordStride; + + // Preserve the float bits. The CPU decodes them directly, and the GPU converts the + // same values to target-space fixed point when it packs the final scene. + this.records[recordOffset] = unchecked((uint)BitConverter.SingleToInt32Bits(this.minimum)); + this.records[recordOffset + 1] = unchecked((uint)BitConverter.SingleToInt32Bits(this.maximum)); + } + } +} + +/// +/// Holds the growable packed profile words shared by all entries in one partition. +/// +/// +/// Profile views reference this object instead of a particular allocator rental. Growing the +/// buffer can therefore replace the rental without invalidating existing views. +/// +internal sealed class LinearGeometryProfileBuffer : IDisposable +{ + private readonly MemoryAllocator allocator; + private IMemoryOwner owner; + + /// + /// Initializes a new instance of the class. + /// + /// The allocator used for backing storage. + /// The initial capacity in packed words. + public LinearGeometryProfileBuffer(MemoryAllocator allocator, int capacity) + { + this.allocator = allocator; + this.owner = allocator.Allocate(capacity); + } + + /// + /// Gets writable storage from an offset to the end of the current rental. + /// + /// The first writable word. + /// The writable tail of the buffer. + public Span GetWritableSpan(int offset) => this.owner.Memory.Span[offset..]; + + /// + /// Gets an immutable range from the current rental. + /// + /// The first word. + /// The number of words. + /// The requested packed profile words. + public ReadOnlySpan GetSpan(int offset, int length) => this.owner.Memory.Span.Slice(offset, length); + + /// + /// Ensures that the buffer can hold the requested total word count. + /// + /// The required total capacity. + /// The number of initialized words to preserve. + public void EnsureCapacity(int requiredCapacity, int usedLength) + { + if (requiredCapacity <= this.owner.Memory.Length) + { + return; + } + + int nextCapacity = Math.Max(requiredCapacity, checked(this.owner.Memory.Length * 2)); + IMemoryOwner next = this.allocator.Allocate(nextCapacity); + this.owner.Memory.Span[..usedLength].CopyTo(next.Memory.Span); + this.owner.Dispose(); + this.owner = next; + } + + /// + /// Releases the current allocator rental. + /// + public void Dispose() => this.owner.Dispose(); +} + +/// +/// Owns the profile arena for one render or encoding partition. +/// +/// +/// Building is confined to one partition thread. After the partition is complete, raster workers +/// may read its entries concurrently. The arena grows geometrically, so a scene keeps a bounded +/// number of allocator rentals instead of one buffer for every geometry. +/// +internal sealed class LinearGeometryProfileStorage : IDisposable +{ + private readonly MemoryAllocator allocator; + private LinearGeometryProfileBuffer? buffer; + private LinearGeometry? firstGeometry; + private LinearGeometryProfiles firstProfiles; + private Dictionary? cache; + private int count; + + /// + /// Initializes a new instance of the class. + /// + /// The allocator used for the growable backing storage. + public LinearGeometryProfileStorage(MemoryAllocator allocator) => this.allocator = allocator; + + /// + /// Gets the existing entry for a geometry or analyzes it into this arena. + /// + /// The geometry whose profiles are required. + /// A stable offset view into this arena. + public LinearGeometryProfiles GetOrAdd(LinearGeometry geometry) + { + if (ReferenceEquals(geometry, this.firstGeometry)) + { + return this.firstProfiles; + } + + if (this.cache is not null && this.cache.TryGetValue(geometry, out LinearGeometryProfiles cached)) + { + return cached; + } + + int maximumWordCount = LinearGeometryProfiles.GetMaximumWordCount(geometry); + LinearGeometryProfileBuffer buffer = this.buffer ??= new LinearGeometryProfileBuffer(this.allocator, maximumWordCount); + buffer.EnsureCapacity(checked(this.count + maximumWordCount), this.count); + + int offset = this.count; + int wordCount = LinearGeometryProfiles.Analyze(geometry, buffer.GetWritableSpan(offset)); + LinearGeometryProfiles profiles = new(buffer, offset, wordCount); + this.count += wordCount; + + if (this.firstGeometry is null) + { + this.firstGeometry = geometry; + this.firstProfiles = profiles; + return profiles; + } + + // Most small scenes contain one aliased geometry. Delay the dictionary until a second + // distinct geometry appears, while still making repeated glyphs and paths constant-time. + this.cache ??= new Dictionary + { + [this.firstGeometry] = this.firstProfiles + }; + this.cache.Add(geometry, profiles); + return profiles; + } + + /// + /// Releases the arena and all geometry references held by its lookup table. + /// + public void Dispose() + { + this.buffer?.Dispose(); + this.buffer = null; + this.firstGeometry = null; + this.firstProfiles = default; + this.cache?.Clear(); + this.cache = null; + this.count = 0; + } +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs index f0d314180..34baf3923 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs @@ -520,7 +520,6 @@ private static RasterizerOptions CreateApplyRasterizerOptions( return new RasterizerOptions( interest, options.IntersectionRule, - rasterizationMode, - graphicsOptions.AntialiasThreshold); + rasterizationMode); } } diff --git a/src/ImageSharp.Drawing/Processing/Backends/DEFAULT_RASTERIZER.md b/src/ImageSharp.Drawing/Processing/Backends/DEFAULT_RASTERIZER.md index 673899dcf..f2e2dae24 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DEFAULT_RASTERIZER.md +++ b/src/ImageSharp.Drawing/Processing/Backends/DEFAULT_RASTERIZER.md @@ -401,12 +401,12 @@ The fill rule is therefore an emission-time decision, not a geometry-preprocessi ## Antialiased And Aliased Modes -The rasterizer can emit either continuous or thresholded coverage. +The rasterizer has separate coverage paths for the two modes. -- `Antialiased` mode keeps the continuous coverage produced by the area-and-cover math -- `Aliased` mode thresholds that continuous coverage using `AntialiasThreshold` +- `Antialiased` mode keeps the continuous coverage produced by the area-and-cover math. +- `Aliased` mode walks sorted crossings at pixel row and column centres. A pixel is covered when the shape contains its centre. If a closed interval contains no centre, the rasterizer lights its midpoint pixel unless the two boundary profiles show that the interval ends at a contour tip. A second, vertical walk finds horizontal features between row centres. -The scan-conversion core stays the same in both modes. Only the final conversion from area to emitted coverage changes. +Aliased coverage is therefore determined directly from geometry. It is not produced by thresholding antialiased area coverage. ## Why Self-Intersections Work diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.Helpers.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.Helpers.cs index 5b2577bac..27867cd8c 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.Helpers.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.Helpers.cs @@ -188,7 +188,7 @@ private void ApplyRectangleIntersection( Span destination, Span coverage) { - float yCoverage = GetAxisCoverage(y, rectangle.Top, rectangle.Bottom, descriptor.EdgeMode, descriptor.AntialiasThreshold); + float yCoverage = GetAxisCoverage(y, rectangle.Top, rectangle.Bottom, descriptor.EdgeMode); if (yCoverage <= 0F) { return; @@ -223,7 +223,7 @@ private void ApplyRectangleDifference( Span destination, Span coverage) { - float yCoverage = GetAxisCoverage(y, rectangle.Top, rectangle.Bottom, descriptor.EdgeMode, descriptor.AntialiasThreshold); + float yCoverage = GetAxisCoverage(y, rectangle.Top, rectangle.Bottom, descriptor.EdgeMode); if (yCoverage <= 0F) { this.ApplyClipDescriptors(clipIndex + 1, y, startX, destination, coverage); @@ -274,13 +274,13 @@ private void ApplyRectangleInterior( int x = clipStart; while (x < clipEnd) { - float xCoverage = GetAxisCoverage(x, rectangle.Left, rectangle.Right, descriptor.EdgeMode, descriptor.AntialiasThreshold); + float xCoverage = GetAxisCoverage(x, rectangle.Left, rectangle.Right, descriptor.EdgeMode); int runStart = x++; float multiplier = xCoverage * yCoverage; // Rectangle edge coverage is constant across interior pixels and only changes // at the left/right edge pixels, so collapse adjacent equal-coverage columns. - while (x < clipEnd && GetAxisCoverage(x, rectangle.Left, rectangle.Right, descriptor.EdgeMode, descriptor.AntialiasThreshold) == xCoverage) + while (x < clipEnd && GetAxisCoverage(x, rectangle.Left, rectangle.Right, descriptor.EdgeMode) == xCoverage) { x++; } @@ -317,13 +317,13 @@ private void ApplyRectangleDifferenceInterior( int x = clipStart; while (x < clipEnd) { - float xCoverage = GetAxisCoverage(x, rectangle.Left, rectangle.Right, descriptor.EdgeMode, descriptor.AntialiasThreshold); + float xCoverage = GetAxisCoverage(x, rectangle.Left, rectangle.Right, descriptor.EdgeMode); int runStart = x++; float multiplier = 1F - (xCoverage * yCoverage); // Subtraction uses the inverse coverage of the rectangle, but the same run // coalescing applies: edge pixels differ, interior pixels share one value. - while (x < clipEnd && GetAxisCoverage(x, rectangle.Left, rectangle.Right, descriptor.EdgeMode, descriptor.AntialiasThreshold) == xCoverage) + while (x < clipEnd && GetAxisCoverage(x, rectangle.Left, rectangle.Right, descriptor.EdgeMode) == xCoverage) { x++; } @@ -455,7 +455,7 @@ private void ApplyFloatingRegionDifference( RectangleF rectangle = rectangles[i]; rectangle.Offset(this.destinationOffset.X, this.destinationOffset.Y); - float yCoverage = GetAxisCoverage(y, rectangle.Top, rectangle.Bottom, descriptor.EdgeMode, descriptor.AntialiasThreshold); + float yCoverage = GetAxisCoverage(y, rectangle.Top, rectangle.Bottom, descriptor.EdgeMode); if (yCoverage <= 0F) { continue; @@ -534,7 +534,7 @@ private void ApplyPathClip( DefaultRasterizer.Context context = scratch.CreateContext( bandInfo.IntersectionRule, bandInfo.RasterizationMode, - bandInfo.AntialiasThreshold); + bandInfo.LineCount); PathClipCoverageRowHandler rowHandler = new( this.workerState, @@ -694,23 +694,23 @@ private void ApplyCoverageRun( /// /// The integer pixel coordinate. /// The inclusive clip interval start. - /// The exclusive clip interval end. - /// The edge mode used to quantize coverage. - /// The hard-edge threshold used when is hard. + /// The inclusive clip interval end for centre sampling. + /// The edge mode used to select continuous or centre-sampled coverage. /// The coverage contribution for the pixel. private static float GetAxisCoverage( int pixel, float clipStart, float clipEnd, - DrawingClipEdgeMode edgeMode, - float antialiasThreshold) + DrawingClipEdgeMode edgeMode) { - float coverage = MathF.Min(pixel + 1F, clipEnd) - MathF.Max(pixel, clipStart); - coverage = Math.Clamp(coverage, 0F, 1F); + if (edgeMode == DrawingClipEdgeMode.Hard) + { + float pixelCentre = pixel + 0.5F; + return pixelCentre >= clipStart && pixelCentre <= clipEnd ? 1F : 0F; + } - return edgeMode == DrawingClipEdgeMode.Hard - ? coverage > antialiasThreshold ? 1F : 0F - : coverage; + float coverage = MathF.Min(pixel + 1F, clipEnd) - MathF.Max(pixel, clipStart); + return Math.Clamp(coverage, 0F, 1F); } } @@ -1070,7 +1070,7 @@ private static void TrimPoolIfIdle() return; } - trimmed = Pool.ToArray(); + trimmed = [.. Pool]; Pool.Clear(); } diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs index 88283627b..429104005 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -537,7 +537,7 @@ private static void ExecuteFillOperation( DefaultRasterizer.Context context = scratch.CreateContext( bandInfo.IntersectionRule, bandInfo.RasterizationMode, - bandInfo.AntialiasThreshold); + bandInfo.LineCount); FillCoverageRowHandler rowHandler = new( renderer, @@ -583,7 +583,7 @@ private static void ExecuteStrokeOperation( DefaultRasterizer.Context context = scratch.CreateContext( bandInfo.IntersectionRule, bandInfo.RasterizationMode, - bandInfo.AntialiasThreshold); + bandInfo.LineCount); FillCoverageRowHandler rowHandler = new( renderer, diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Linearizer.Outputs.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Linearizer.Outputs.cs index 99e07eb76..674c789c6 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Linearizer.Outputs.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Linearizer.Outputs.cs @@ -30,6 +30,13 @@ internal interface ILineBlock /// The number of leading lines to rasterize from this block. /// The mutable scan-conversion context to write into. public void Rasterize(int count, ref Context context); + + /// + /// Rasterizes the leading lines and supplies each line's profile tag. + /// + /// The number of leading lines to rasterize from this block. + /// The mutable scan-conversion context to write into. + public void RasterizeTagged(int count, ref Context context); } /// @@ -88,18 +95,27 @@ internal sealed class LinearizedRasterData /// The retained line-block chain for each row band. /// The valid line count in each row's front block. /// The retained start-cover seeds for each row band. + /// The geometry-space profile data, or an empty view outside aliased rendering. + /// The absolute X translation applied to profile extents. + /// The absolute Y translation applied to profile extents. public LinearizedRasterData( LinearGeometry geometry, TileBounds bounds, TLineBlock?[] lines, int[] firstBlockLineCounts, - IMemoryOwner?[] startCoverTable) + IMemoryOwner?[] startCoverTable, + LinearGeometryProfiles profiles, + float profileTranslateX, + float profileTranslateY) { this.Geometry = geometry; this.Bounds = bounds; this.Lines = lines; this.FirstBlockLineCounts = firstBlockLineCounts; this.StartCoverTable = startCoverTable; + this.Profiles = profiles; + this.ProfileTranslateX = profileTranslateX; + this.ProfileTranslateY = profileTranslateY; } /// @@ -127,6 +143,21 @@ public LinearizedRasterData( /// public IMemoryOwner?[] StartCoverTable { get; } + /// + /// Gets the geometry-space profile data, or an empty view outside aliased rendering. + /// + public LinearGeometryProfiles Profiles { get; } + + /// + /// Gets the absolute X translation applied to profile extents. + /// + public float ProfileTranslateX { get; } + + /// + /// Gets the absolute Y translation applied to profile extents. + /// + public float ProfileTranslateY { get; } + /// /// Iterates the retained line blocks for one row band. /// diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Linearizer.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Linearizer.cs index b07d75a80..2ea00aaad 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Linearizer.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Linearizer.cs @@ -17,6 +17,7 @@ private abstract class Linearizer where TL : class { private bool hasAnyCoverage; + private int segmentOrdinal; /// /// Initializes a new instance of the class. @@ -32,6 +33,10 @@ private abstract class Linearizer /// The first retained row-band index touched by the geometry. /// The number of retained row bands owned by the geometry. /// The allocator used for retained start-cover storage. + /// + /// Whether to retain horizontal edges. They do not change row winding, but the aliased + /// column pass needs them to detect horizontal features between two row centres. + /// protected Linearizer( LinearGeometry geometry, Matrix4x4 residual, @@ -43,8 +48,10 @@ protected Linearizer( int height, int firstBandIndex, int rowBandCount, - MemoryAllocator allocator) + MemoryAllocator allocator, + bool keepHorizontalLines) { + this.KeepHorizontalLines = keepHorizontalLines; this.Geometry = geometry; this.Residual = residual; this.HasResidual = !residual.IsIdentity; @@ -64,6 +71,11 @@ protected Linearizer( this.LineArrays = new TL?[rowBandCount]; } + /// + /// Gets a value indicating whether horizontal edges are retained for the aliased column pass. + /// + protected bool KeepHorizontalLines { get; } + /// /// Gets the source geometry being lowered. /// @@ -154,6 +166,38 @@ protected Linearizer( /// protected ref bool HasAnyCoverage => ref this.hasAnyCoverage; + /// + /// Gets the geometry-space profile data, or an empty view outside aliased rendering. + /// + public LinearGeometryProfiles Profiles { get; private set; } + + /// + /// Gets the absolute X translation applied to geometry-space profile extents. + /// + public float ProfileTranslateX { get; private set; } + + /// + /// Gets the absolute Y translation applied to geometry-space profile extents. + /// + public float ProfileTranslateY { get; private set; } + + /// + /// Prepares the profile tags used by aliased fill line emission. + /// + /// The partition-owned profile arena. + protected void PrepareProfiles(LinearGeometryProfileStorage profileStorage) + { + // Each profile stores a range on one original geometry axis. Translation preserves + // that range. Scale, rotation, and shear do not, so those draws use sentinel tags and + // skip the terminating-tip test. + if (this.KeepHorizontalLines && IsAxisPreservingTranslation(this.Residual)) + { + this.Profiles = profileStorage.GetOrAdd(this.Geometry); + this.ProfileTranslateX = this.Residual.Translation.X + this.TranslateX; + this.ProfileTranslateY = this.Residual.Translation.Y + this.TranslateY; + } + } + /// /// Executes the linearization pass and finalizes the retained row payloads. /// @@ -215,7 +259,8 @@ protected void ProcessContained() FloatToFixed24Dot8((p0.X + this.TranslateX) - this.MinX), FloatToFixed24Dot8((p0.Y + this.TranslateY) - this.MinY), FloatToFixed24Dot8((p1.X + this.TranslateX) - this.MinX), - FloatToFixed24Dot8((p1.Y + this.TranslateY) - this.MinY)); + FloatToFixed24Dot8((p1.Y + this.TranslateY) - this.MinY), + this.NextSegmentTag()); } } @@ -242,10 +287,29 @@ protected void ProcessUncontained() (p0.X + this.TranslateX) - this.MinX, (p0.Y + this.TranslateY) - this.MinY, (p1.X + this.TranslateX) - this.MinX, - (p1.Y + this.TranslateY) - this.MinY); + (p1.Y + this.TranslateY) - this.MinY, + this.NextSegmentTag()); } } + /// + /// Gets the X and Y profile identifiers for the next derived segment. + /// + /// The packed identifiers, or the all-sentinel tag when this draw has no profile data. + private uint NextSegmentTag() + => this.Profiles.IsEmpty ? LinearGeometryProfiles.SentinelTag : this.Profiles.GetSegmentTag(this.segmentOrdinal++); + + /// + /// Tests whether device coordinates differ from geometry coordinates only by translation. + /// + /// The residual transform to classify. + /// when the residual is a pure translation. + private static bool IsAxisPreservingTranslation(in Matrix4x4 residual) + => residual.M11 == 1F && residual.M22 == 1F && residual.M33 == 1F + && residual.M12 == 0F && residual.M21 == 0F + && residual.M13 == 0F && residual.M31 == 0F + && residual.M23 == 0F && residual.M32 == 0F; + /// /// Clips one geometry line against the destination interest and adds the retained result. /// @@ -258,11 +322,29 @@ protected void ProcessUncontained() /// The starting Y coordinate in translated float space. /// The ending X coordinate in translated float space. /// The ending Y coordinate in translated float space. - protected void AddUncontainedLine(float x0, float y0, float x1, float y1) + /// The segment profile tag, retained with every clipped piece. + protected void AddUncontainedLine(float x0, float y0, float x1, float y1, uint tag) { - // Horizontal segments never change scanline winding. + // A horizontal edge does not change winding along a row. Continuous coverage can + // discard it. Aliased rendering retains its visible part because the column pass needs + // both horizontal boundaries of a feature that lies between two row centres. if (y0 == y1) { + if (this.KeepHorizontalLines && y0 >= 0F && y0 < this.Height) + { + float hx0 = Math.Clamp(x0, 0F, this.Width); + float hx1 = Math.Clamp(x1, 0F, this.Width); + if (hx0 != hx1) + { + this.AddContainedLineF24Dot8( + Math.Clamp(FloatToFixed24Dot8(hx0), 0, this.Width * FixedOne), + Math.Clamp(FloatToFixed24Dot8(y0), 0, this.Height * FixedOne), + Math.Clamp(FloatToFixed24Dot8(hx1), 0, this.Width * FixedOne), + Math.Clamp(FloatToFixed24Dot8(y1), 0, this.Height * FixedOne), + tag); + } + } + return; } @@ -298,7 +380,7 @@ protected void AddUncontainedLine(float x0, float y0, float x1, float y1) } else { - this.AddContainedLineF24Dot8(x0c, p0y, x0c, p1y); + this.AddContainedLineF24Dot8(x0c, p0y, x0c, p1y, tag); } return; @@ -359,7 +441,8 @@ protected void AddUncontainedLine(float x0, float y0, float x1, float y1) Math.Clamp(FloatToFixed24Dot8((float)rx0), 0, this.Width * FixedOne), Math.Clamp(FloatToFixed24Dot8((float)ry0), 0, this.Height * FixedOne), Math.Clamp(FloatToFixed24Dot8((float)rx1), 0, this.Width * FixedOne), - Math.Clamp(FloatToFixed24Dot8((float)ry1), 0, this.Height * FixedOne)); + Math.Clamp(FloatToFixed24Dot8((float)ry1), 0, this.Height * FixedOne), + tag); return; } @@ -402,7 +485,7 @@ protected void AddUncontainedLine(float x0, float y0, float x1, float y1) this.hasAnyCoverage = true; // The visible portion begins exactly at x == 0 after the left-edge clip. - this.AddContainedLineF24Dot8(0, by, cx, cy); + this.AddContainedLineF24Dot8(0, by, cx, cy, tag); } else { @@ -410,7 +493,8 @@ protected void AddUncontainedLine(float x0, float y0, float x1, float y1) Math.Clamp(FloatToFixed24Dot8((float)rx0), 0, this.Width * FixedOne), Math.Clamp(FloatToFixed24Dot8((float)ry0), 0, this.Height * FixedOne), Math.Clamp(FloatToFixed24Dot8((float)bx1), 0, this.Width * FixedOne), - Math.Clamp(FloatToFixed24Dot8((float)by1), 0, this.Height * FixedOne)); + Math.Clamp(FloatToFixed24Dot8((float)by1), 0, this.Height * FixedOne), + tag); } } else @@ -435,7 +519,7 @@ protected void AddUncontainedLine(float x0, float y0, float x1, float y1) // The right-to-left case mirrors the left-edge handling above: emit the // visible portion first, then retain the winding-only tail as start covers. - this.AddContainedLineF24Dot8(ax, ay, 0, by); + this.AddContainedLineF24Dot8(ax, ay, 0, by, tag); this.UpdateStartCoversClipped(by, c); this.hasAnyCoverage = true; } @@ -445,7 +529,8 @@ protected void AddUncontainedLine(float x0, float y0, float x1, float y1) Math.Clamp(FloatToFixed24Dot8((float)bx0), 0, this.Width * FixedOne), Math.Clamp(FloatToFixed24Dot8((float)by0), 0, this.Height * FixedOne), Math.Clamp(FloatToFixed24Dot8((float)rx1), 0, this.Width * FixedOne), - Math.Clamp(FloatToFixed24Dot8((float)ry1), 0, this.Height * FixedOne)); + Math.Clamp(FloatToFixed24Dot8((float)ry1), 0, this.Height * FixedOne), + tag); } } } @@ -457,11 +542,38 @@ protected void AddUncontainedLine(float x0, float y0, float x1, float y1) /// The starting Y coordinate. /// The ending X coordinate. /// The ending Y coordinate. - protected void AddContainedLineF24Dot8(int x0, int y0, int x1, int y1) + /// The segment profile tag, retained with every band piece. + protected void AddContainedLineF24Dot8(int x0, int y0, int x1, int y1, uint tag) { - // Horizontal segments never change scanline winding. + // A retained horizontal edge belongs to the band containing its Y coordinate. At an + // exact band boundary it is copied to both adjacent bands. Only the band containing + // the matching edge can form a closed column interval; the other band ignores its + // unmatched crossing. if (y0 == y1) { + if (!this.KeepHorizontalLines || x0 == x1) + { + return; + } + + int bandTop = this.BandTopStart * FixedOne; + int bandExtent = PreferredRowHeight * FixedOne; + int band = (y0 - bandTop) / bandExtent; + if ((uint)band < (uint)this.RowBandCount) + { + int rowTop = bandTop + (band * bandExtent); + this.AppendLine(band, x0, y0 - rowTop, x1, y1 - rowTop, tag); + this.LineCounts[band]++; + this.hasAnyCoverage = true; + } + + if ((y0 - bandTop) % bandExtent == 0 && (uint)(band - 1) < (uint)this.RowBandCount) + { + int rowTop = bandTop + ((band - 1) * bandExtent); + this.AppendLine(band - 1, x0, y0 - rowTop, x1, y1 - rowTop, tag); + this.LineCounts[band - 1]++; + } + return; } @@ -469,7 +581,7 @@ protected void AddContainedLineF24Dot8(int x0, int y0, int x1, int y1) { // Winding direction is carried by the y endpoint order itself, so both the // downward and upward cases hand the endpoints to the band splitter as-is. - this.SplitAcrossBands(x0, y0, x0, y1); + this.SplitAcrossBands(x0, y0, x0, y1, tag); return; } @@ -484,8 +596,8 @@ protected void AddContainedLineF24Dot8(int x0, int y0, int x1, int y1) // shrinks and the recursion overflows the stack (issue #403). int mx = (int)(((long)x0 + x1) >> 1); int my = (int)(((long)y0 + y1) >> 1); - this.AddContainedLineF24Dot8(x0, y0, mx, my); - this.AddContainedLineF24Dot8(mx, my, x1, y1); + this.AddContainedLineF24Dot8(x0, y0, mx, my, tag); + this.AddContainedLineF24Dot8(mx, my, x1, y1, tag); return; } @@ -516,13 +628,13 @@ protected void AddContainedLineF24Dot8(int x0, int y0, int x1, int y1) if (rowIndex0 == rowIndex1) { int rowTop = bandTopStart + (rowIndex0 * bandHeight); - this.AppendLine(rowIndex0, x0, y0 - rowTop, x1, y1 - rowTop); + this.AppendLine(rowIndex0, x0, y0 - rowTop, x1, y1 - rowTop, tag); this.LineCounts[rowIndex0]++; this.hasAnyCoverage = true; return; } - this.SplitAcrossBands(x0, y0, x1, y1); + this.SplitAcrossBands(x0, y0, x1, y1, tag); } /// @@ -539,7 +651,8 @@ protected void AddContainedLineF24Dot8(int x0, int y0, int x1, int y1) /// The starting Y coordinate relative to the row band. /// The ending X coordinate relative to the row band. /// The ending Y coordinate relative to the row band. - protected abstract void AppendLine(int rowIndex, int x0, int y0, int x1, int y1); + /// The segment profile tag. + protected abstract void AppendLine(int rowIndex, int x0, int y0, int x1, int y1, uint tag); /// /// Finalizes the mutable collectors into the retained line-block representation. @@ -571,7 +684,8 @@ protected TL GetOrCreateLineArray(int rowIndex) /// The starting Y coordinate. /// The ending X coordinate. /// The ending Y coordinate. - private void SplitAcrossBands(int x0, int y0, int x1, int y1) + /// The segment profile tag, retained with every band piece. + private void SplitAcrossBands(int x0, int y0, int x1, int y1, uint tag) { int dy = y1 - y0; int dx = x1 - x0; @@ -594,7 +708,7 @@ private void SplitAcrossBands(int x0, int y0, int x1, int y1) int rowTop = bandTopStart + (currentBand * bandHeight); // Each retained segment is stored in the local coordinate space of its owning band. - this.AppendLine(currentBand, currentX, currentY - rowTop, nextX, bandBoundaryY - rowTop); + this.AppendLine(currentBand, currentX, currentY - rowTop, nextX, bandBoundaryY - rowTop, tag); this.LineCounts[currentBand]++; this.hasAnyCoverage = true; currentX = nextX; @@ -609,7 +723,7 @@ private void SplitAcrossBands(int x0, int y0, int x1, int y1) } int finalRowTop = bandTopStart + (endBand * bandHeight); - this.AppendLine(endBand, currentX, currentY - finalRowTop, x1, y1 - finalRowTop); + this.AppendLine(endBand, currentX, currentY - finalRowTop, x1, y1 - finalRowTop, tag); this.LineCounts[endBand]++; this.hasAnyCoverage = true; } @@ -787,6 +901,8 @@ private static void UpdateCoverTableUp(Span covers, int y0, int y1) /// private sealed class LinearizerX32Y16 : Linearizer { + private readonly LinearGeometryProfileStorage profileStorage; + /// /// Initializes a new instance of the class. /// @@ -801,6 +917,8 @@ private sealed class LinearizerX32Y16 : Linearizer /// The first retained row-band index touched by the geometry. /// The number of retained row bands owned by the geometry. /// The allocator used for retained start-cover storage. + /// The partition-owned profile arena. + /// Whether to retain horizontal edges for aliased thin-feature recovery between row centres. public LinearizerX32Y16( LinearGeometry geometry, Matrix4x4 residual, @@ -812,9 +930,14 @@ public LinearizerX32Y16( int height, int firstBandIndex, int rowBandCount, - MemoryAllocator allocator) - : base(geometry, residual, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, allocator) - => this.FinalLines = new LineArrayX32Y16Block?[rowBandCount]; + MemoryAllocator allocator, + LinearGeometryProfileStorage profileStorage, + bool keepHorizontalLines) + : base(geometry, residual, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, allocator, keepHorizontalLines) + { + this.profileStorage = profileStorage; + this.FinalLines = new LineArrayX32Y16Block?[rowBandCount]; + } /// /// Gets the finalized retained line blocks for each row band. @@ -822,11 +945,11 @@ public LinearizerX32Y16( public LineArrayX32Y16Block?[] FinalLines { get; } /// - protected override LineArrayX32Y16 CreateLineArray() => new(); + protected override LineArrayX32Y16 CreateLineArray() => new(this.KeepHorizontalLines); /// - protected override void AppendLine(int rowIndex, int x0, int y0, int x1, int y1) - => this.GetOrCreateLineArray(rowIndex).AppendLine(x0, y0, x1, y1); + protected override void AppendLine(int rowIndex, int x0, int y0, int x1, int y1, uint tag) + => this.GetOrCreateLineArray(rowIndex).AppendLine(x0, y0, x1, y1, tag); /// protected override void FinalizeLines() @@ -846,6 +969,8 @@ protected override void FinalizeLines() /// when retained coverage was produced; otherwise . public bool TryProcess(out LinearizedRasterData result) { + this.PrepareProfiles(this.profileStorage); + if (!this.ProcessCore()) { result = null!; @@ -857,7 +982,10 @@ public bool TryProcess(out LinearizedRasterData result) new TileBounds(this.MinX, this.FirstBandIndex, this.Width, this.RowBandCount), this.FinalLines, this.FirstBlockLineCounts, - this.StartCoverTable); + this.StartCoverTable, + this.Profiles, + this.ProfileTranslateX, + this.ProfileTranslateY); return true; } @@ -868,6 +996,8 @@ public bool TryProcess(out LinearizedRasterData result) /// private sealed class LinearizerX16Y16 : Linearizer { + private readonly LinearGeometryProfileStorage profileStorage; + /// /// Initializes a new instance of the class. /// @@ -882,6 +1012,8 @@ private sealed class LinearizerX16Y16 : Linearizer /// The first retained row-band index touched by the geometry. /// The number of retained row bands owned by the geometry. /// The allocator used for retained start-cover storage. + /// The partition-owned profile arena. + /// Whether to retain horizontal edges for aliased thin-feature recovery between row centres. public LinearizerX16Y16( LinearGeometry geometry, Matrix4x4 residual, @@ -893,9 +1025,14 @@ public LinearizerX16Y16( int height, int firstBandIndex, int rowBandCount, - MemoryAllocator allocator) - : base(geometry, residual, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, allocator) - => this.FinalLines = new LineArrayX16Y16Block?[rowBandCount]; + MemoryAllocator allocator, + LinearGeometryProfileStorage profileStorage, + bool keepHorizontalLines) + : base(geometry, residual, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, allocator, keepHorizontalLines) + { + this.profileStorage = profileStorage; + this.FinalLines = new LineArrayX16Y16Block?[rowBandCount]; + } /// /// Gets the finalized retained line blocks for each row band. @@ -903,11 +1040,11 @@ public LinearizerX16Y16( public LineArrayX16Y16Block?[] FinalLines { get; } /// - protected override LineArrayX16Y16 CreateLineArray() => new(); + protected override LineArrayX16Y16 CreateLineArray() => new(this.KeepHorizontalLines); /// - protected override void AppendLine(int rowIndex, int x0, int y0, int x1, int y1) - => this.GetOrCreateLineArray(rowIndex).AppendLine(x0, y0, x1, y1); + protected override void AppendLine(int rowIndex, int x0, int y0, int x1, int y1, uint tag) + => this.GetOrCreateLineArray(rowIndex).AppendLine(x0, y0, x1, y1, tag); /// protected override void FinalizeLines() @@ -927,6 +1064,8 @@ protected override void FinalizeLines() /// when retained coverage was produced; otherwise . public bool TryProcess(out LinearizedRasterData result) { + this.PrepareProfiles(this.profileStorage); + if (!this.ProcessCore()) { result = null!; @@ -938,7 +1077,10 @@ public bool TryProcess(out LinearizedRasterData result) new TileBounds(this.MinX, this.FirstBandIndex, this.Width, this.RowBandCount), this.FinalLines, this.FirstBlockLineCounts, - this.StartCoverTable); + this.StartCoverTable, + this.Profiles, + this.ProfileTranslateX, + this.ProfileTranslateY); return true; } diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.RasterizableGeometry.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.RasterizableGeometry.cs index 8a6cdf62f..eedac9e09 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.RasterizableGeometry.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.RasterizableGeometry.cs @@ -17,6 +17,7 @@ internal sealed class RasterizableGeometry : IDisposable private readonly LineArrayX32Y16Block?[]? linesX32; private readonly int[] firstBlockLineCounts; private readonly IMemoryOwner?[] startCoverTable; + private readonly LinearGeometryProfiles profiles; /// /// Initializes a new instance of the class. @@ -33,6 +34,9 @@ internal sealed class RasterizableGeometry : IDisposable /// The retained wide line chains for each local row band. /// The valid line count in each front retained block. /// The retained start-cover table for each local row band. + /// The geometry-space profile data, or an empty view. + /// The absolute X translation applied to profile extents. + /// The absolute Y translation applied to profile extents. public RasterizableGeometry( int firstRowBandIndex, int rowBandCount, @@ -45,8 +49,14 @@ public RasterizableGeometry( LineArrayX16Y16Block?[]? linesX16, LineArrayX32Y16Block?[]? linesX32, int[] firstBlockLineCounts, - IMemoryOwner?[] startCoverTable) + IMemoryOwner?[] startCoverTable, + LinearGeometryProfiles profiles, + float profileTranslateX, + float profileTranslateY) { + this.profiles = profiles; + this.ProfileTranslateX = profileTranslateX; + this.ProfileTranslateY = profileTranslateY; this.FirstRowBandIndex = firstRowBandIndex; this.RowBandCount = rowBandCount; this.Width = width; @@ -96,6 +106,21 @@ public RasterizableGeometry( /// public bool IsX16 { get; } + /// + /// Gets the geometry-space profile data, or an empty view. + /// + public LinearGeometryProfiles Profiles => this.profiles; + + /// + /// Gets the absolute X translation applied to profile extents. + /// + public float ProfileTranslateX { get; } + + /// + /// Gets the absolute Y translation applied to profile extents. + /// + public float ProfileTranslateY { get; } + /// /// Returns when the given local row band has retained coverage payload. /// diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.RetainedTypes.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.RetainedTypes.cs index 8dcb57fa4..79c42cb1a 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.RetainedTypes.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.RetainedTypes.cs @@ -102,7 +102,6 @@ internal readonly struct RasterizableBandInfo /// The absolute destination Y coordinate of the band's top row. /// The fill rule used when resolving accumulated winding. /// The rasterization mode used by the band. - /// The aliased threshold used when the band runs in aliased mode. /// The perceptual coverage boost used when the band runs in antialiased mode. /// Indicates whether the band has non-zero start-cover seeds. public RasterizableBandInfo( @@ -115,7 +114,6 @@ public RasterizableBandInfo( int destinationTop, IntersectionRule intersectionRule, RasterizationMode rasterizationMode, - float antialiasThreshold, float coverageBoost, bool hasStartCovers) { @@ -128,7 +126,6 @@ public RasterizableBandInfo( this.DestinationTop = destinationTop; this.IntersectionRule = intersectionRule; this.RasterizationMode = rasterizationMode; - this.AntialiasThreshold = antialiasThreshold; this.CoverageBoost = coverageBoost; this.HasStartCovers = hasStartCovers; } @@ -178,11 +175,6 @@ public RasterizableBandInfo( /// public RasterizationMode RasterizationMode { get; } - /// - /// Gets the aliased threshold used when the band runs in aliased mode. - /// - public float AntialiasThreshold { get; } - /// /// Gets the perceptual coverage boost used when the band runs in antialiased mode. /// Partial coverage is remapped by the S-curve a + boost * a * (1 - a) * (2a - 1); @@ -206,12 +198,22 @@ public RasterizableBandInfo( /// internal sealed class LineArrayX32Y16 { + private readonly bool retainTags; private LineArrayX32Y16Block? current; // Starting at full capacity forces the first AppendLine to allocate the front block, // so empty rows never pay for a block allocation. private int count = LineArrayX32Y16Block.LineCount; + /// + /// Initializes a new instance of the class. + /// + /// + /// Whether each line stores a profile tag. Only aliased fills need tags. Antialiased fills + /// and strokes therefore use the smaller base block and do not allocate tag storage. + /// + public LineArrayX32Y16(bool retainTags) => this.retainTags = retainTags; + /// /// Gets the front block in the retained line chain. /// @@ -231,26 +233,42 @@ internal sealed class LineArrayX32Y16 /// The starting Y coordinate in 24.8 fixed-point. /// The ending X coordinate in 24.8 fixed-point. /// The ending Y coordinate in 24.8 fixed-point. - public void AppendLine(int x0, int y0, int x1, int y1) + /// The segment profile tag. + public void AppendLine(int x0, int y0, int x1, int y1, uint tag) { - // Horizontal segments never change scanline winding, so they carry no raster payload. - if (y0 == y1) - { - return; - } - int packedY0Y1 = Pack(y0, y1); LineArrayX32Y16Block? block = this.current; int currentCount = this.count; if (currentCount < LineArrayX32Y16Block.LineCount) { - block!.Set(currentCount, packedY0Y1, x0, x1); + // retainTags does not change during the collector's lifetime. When it is true, + // every block in this chain was created as the tagged derived type. + if (this.retainTags) + { + ((TaggedLineArrayX32Y16Block)block!).SetTagged(currentCount, packedY0Y1, x0, x1, tag); + } + else + { + block!.Set(currentCount, packedY0Y1, x0, x1); + } + this.count = currentCount + 1; } else { - LineArrayX32Y16Block next = new(block); - next.Set(0, packedY0Y1, x0, x1); + LineArrayX32Y16Block next; + if (this.retainTags) + { + TaggedLineArrayX32Y16Block tagged = new(block); + tagged.SetTagged(0, packedY0Y1, x0, x1, tag); + next = tagged; + } + else + { + next = new LineArrayX32Y16Block(block); + next.Set(0, packedY0Y1, x0, x1); + } + this.current = next; this.count = 1; } @@ -269,7 +287,7 @@ public void AppendLine(int x0, int y0, int x1, int y1) /// /// Represents one retained 32-bit-X line block. /// - internal sealed class LineArrayX32Y16Block : ILineBlock + internal class LineArrayX32Y16Block : ILineBlock { private const int BlockLineCount = 32; private PackedLineX32Y16Buffer lines; @@ -313,6 +331,20 @@ public void Rasterize(int count, ref Context context) } } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RasterizeTagged(int count, ref Context context) + { + // This method is used only for a chain created with retainTags. The base block keeps + // the common line data so other rasterization modes do not store unused tags. + ReadOnlySpan tags = ((TaggedLineArrayX32Y16Block)this).Tags; + for (int i = 0; i < count; i++) + { + PackedLineX32Y16 line = this.lines[i]; + context.RasterizeLineSegment(line.X0, UnpackLo(line.PackedY0Y1), line.X1, UnpackHi(line.PackedY0Y1), tags[i]); + } + } + /// /// Iterates the retained block chain and rasterizes each block in sequence. /// @@ -330,6 +362,23 @@ public void Iterate(int firstBlockLineCount, ref Context context) } } + /// + /// Iterates a tagged retained block chain and supplies each line's profile tag. + /// + /// The number of valid lines stored in the front block. + /// The mutable scan-conversion context. + public void IterateTagged(int firstBlockLineCount, ref Context context) + { + int count = firstBlockLineCount; + LineArrayX32Y16Block? lineBlock = this; + while (lineBlock is not null) + { + lineBlock.RasterizeTagged(count, ref context); + lineBlock = lineBlock.Next; + count = LineCount; + } + } + /// /// Unpacks the low signed 16-bit value from a packed endpoint pair. /// @@ -377,17 +426,73 @@ private struct PackedLineX32Y16Buffer } } + /// + /// Retains profile tags inline with a 32-bit-X line block for aliased replay. + /// + internal sealed class TaggedLineArrayX32Y16Block : LineArrayX32Y16Block + { + private TagBuffer tags; + + /// + /// Initializes a new instance of the class. + /// + /// The next block in the retained chain. + public TaggedLineArrayX32Y16Block(LineArrayX32Y16Block? next) + : base(next) + { + } + + /// + /// Gets the block's profile tags for aliased replay. + /// + public ReadOnlySpan Tags => this.tags; + + /// + /// Stores one retained line and its profile tag into the block. + /// + /// The block-local line index. + /// The packed 16-bit Y endpoints. + /// The starting X coordinate in 24.8 fixed-point. + /// The ending X coordinate in 24.8 fixed-point. + /// The segment profile tag. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetTagged(int index, int packedY0Y1, int x0, int x1, uint tag) + { + this.Set(index, packedY0Y1, x0, x1); + this.tags[index] = tag; + } + + /// + /// Holds the fixed-capacity profile tags inline with an aliased line block. + /// + [InlineArray(32)] + private struct TagBuffer + { + private uint element0; + } + } + /// /// Collects retained line segments whose X coordinates fit in packed 16-bit storage. /// internal sealed class LineArrayX16Y16 { + private readonly bool retainTags; private LineArrayX16Y16Block? current; // Starting at full capacity forces the first AppendLine to allocate the front block, // so empty rows never pay for a block allocation. private int count = LineArrayX16Y16Block.LineCount; + /// + /// Initializes a new instance of the class. + /// + /// + /// Whether each line stores a profile tag. Only aliased fills need tags. Antialiased fills + /// and strokes therefore use the smaller base block and do not allocate tag storage. + /// + public LineArrayX16Y16(bool retainTags) => this.retainTags = retainTags; + /// /// Gets the front block in the retained line chain. /// @@ -407,27 +512,43 @@ internal sealed class LineArrayX16Y16 /// The starting Y coordinate in 24.8 fixed-point. /// The ending X coordinate in 24.8 fixed-point. /// The ending Y coordinate in 24.8 fixed-point. - public void AppendLine(int x0, int y0, int x1, int y1) + /// The segment profile tag. + public void AppendLine(int x0, int y0, int x1, int y1, uint tag) { - // Horizontal segments never change scanline winding, so they carry no raster payload. - if (y0 == y1) - { - return; - } - int packedY0Y1 = Pack(y0, y1); int packedX0X1 = Pack(x0, x1); LineArrayX16Y16Block? block = this.current; int currentCount = this.count; if (currentCount < LineArrayX16Y16Block.LineCount) { - block!.Set(currentCount, packedY0Y1, packedX0X1); + // retainTags does not change during the collector's lifetime. When it is true, + // every block in this chain was created as the tagged derived type. + if (this.retainTags) + { + ((TaggedLineArrayX16Y16Block)block!).SetTagged(currentCount, packedY0Y1, packedX0X1, tag); + } + else + { + block!.Set(currentCount, packedY0Y1, packedX0X1); + } + this.count = currentCount + 1; } else { - LineArrayX16Y16Block next = new(block); - next.Set(0, packedY0Y1, packedX0X1); + LineArrayX16Y16Block next; + if (this.retainTags) + { + TaggedLineArrayX16Y16Block tagged = new(block); + tagged.SetTagged(0, packedY0Y1, packedX0X1, tag); + next = tagged; + } + else + { + next = new LineArrayX16Y16Block(block); + next.Set(0, packedY0Y1, packedX0X1); + } + this.current = next; this.count = 1; } @@ -446,7 +567,7 @@ public void AppendLine(int x0, int y0, int x1, int y1) /// /// Represents one retained 16-bit-X line block. /// - internal sealed class LineArrayX16Y16Block : ILineBlock + internal class LineArrayX16Y16Block : ILineBlock { private const int BlockLineCount = 32; private PackedLineX16Y16Buffer lines; @@ -492,6 +613,25 @@ public void Rasterize(int count, ref Context context) } } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RasterizeTagged(int count, ref Context context) + { + // This method is used only for a chain created with retainTags. The base block keeps + // the common line data so other rasterization modes do not store unused tags. + ReadOnlySpan tags = ((TaggedLineArrayX16Y16Block)this).Tags; + for (int i = 0; i < count; i++) + { + PackedLineX16Y16 line = this.lines[i]; + context.RasterizeLineSegment( + UnpackLo(line.PackedX0X1), + UnpackLo(line.PackedY0Y1), + UnpackHi(line.PackedX0X1), + UnpackHi(line.PackedY0Y1), + tags[i]); + } + } + /// /// Iterates the retained block chain and rasterizes each block in sequence. /// @@ -509,6 +649,23 @@ public void Iterate(int firstBlockLineCount, ref Context context) } } + /// + /// Iterates a tagged retained block chain and supplies each line's profile tag. + /// + /// The number of valid lines stored in the front block. + /// The mutable scan-conversion context. + public void IterateTagged(int firstBlockLineCount, ref Context context) + { + int count = firstBlockLineCount; + LineArrayX16Y16Block? lineBlock = this; + while (lineBlock is not null) + { + lineBlock.RasterizeTagged(count, ref context); + lineBlock = lineBlock.Next; + count = LineCount; + } + } + /// /// Unpacks the low signed 16-bit value from a packed endpoint pair. /// @@ -550,4 +707,49 @@ private struct PackedLineX16Y16Buffer private PackedLineX16Y16 element0; } } + + /// + /// Retains profile tags inline with a 16-bit-X line block for aliased replay. + /// + internal sealed class TaggedLineArrayX16Y16Block : LineArrayX16Y16Block + { + private TagBuffer tags; + + /// + /// Initializes a new instance of the class. + /// + /// The next block in the retained chain. + public TaggedLineArrayX16Y16Block(LineArrayX16Y16Block? next) + : base(next) + { + } + + /// + /// Gets the block's profile tags for aliased replay. + /// + public ReadOnlySpan Tags => this.tags; + + /// + /// Stores one retained line and its profile tag into the block. + /// + /// The block-local line index. + /// The packed 16-bit Y endpoints. + /// The packed 16-bit X endpoints. + /// The segment profile tag. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetTagged(int index, int packedY0Y1, int packedX0X1, uint tag) + { + this.Set(index, packedY0Y1, packedX0X1); + this.tags[index] = tag; + } + + /// + /// Holds the fixed-capacity profile tags inline with an aliased line block. + /// + [InlineArray(32)] + private struct TagBuffer + { + private uint element0; + } + } } diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Stroke.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Stroke.cs index 3c7ac166a..790e4afa3 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Stroke.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Stroke.cs @@ -12,7 +12,7 @@ internal static partial class DefaultRasterizer { /// /// The length threshold below which a segment direction, arc radius, or edge extent is treated as degenerate. - /// Mirrored on the GPU as TANGENT_THRESH in flatten.wgsl; the two must stay in lockstep. + /// Mirrored on the GPU as TANGENT_THRESH in path_lowering.wgsl; the two must stay in lockstep. /// private const float StrokeDirectionEpsilon = 1e-6F; @@ -27,6 +27,12 @@ internal static partial class DefaultRasterizer /// private const int DirectStrokeVerticalSampleCount = 4; + // Two-point aliased strokes share this immutable centreline. A value-type residual maps its + // unit X segment onto each requested line. This avoids one LinearGeometry object and its two + // backing arrays for every draw item. + private static readonly LinearGeometry CanonicalLineGeometry = + LinearGeometry.CreateOpenPolyline([new PointF(0F, 0F), new PointF(1F, 0F)]); + /// /// Creates retained row-local raster payload for one stroked centerline geometry. /// @@ -92,6 +98,38 @@ internal static partial class DefaultRasterizer } StrokeStyle strokeStyle = new(pen, widthScale); + if (options.RasterizationMode == RasterizationMode.Aliased) + { + // Aliased strokes use the same retained outline and centre-sampled row/column + // walks as aliased fills. Map the shared unit centreline to these endpoints; stroke + // expansion happens after this transform, so its width remains in device pixels. + Matrix4x4 residual = new( + end.X - start.X, + end.Y - start.Y, + 0F, + 0F, + 0F, + 1F, + 0F, + 0F, + 0F, + 0F, + 1F, + 0F, + start.X, + start.Y, + 0F, + 1F); + + return CreateRetainedStrokeRasterizableGeometry( + CanonicalLineGeometry, + residual, + strokeStyle, + translateX, + translateY, + in options, + allocator); + } RectangleF bounds = RectangleF.FromLTRB( MathF.Min(start.X, end.X), @@ -142,7 +180,6 @@ internal static partial class DefaultRasterizer bandTop, options.IntersectionRule, options.RasterizationMode, - options.AntialiasThreshold, coverageBoost: 0F, hasStartCovers: false); } @@ -233,6 +270,7 @@ internal static partial class DefaultRasterizer height, firstRowBandIndex, rowBandCount, + options.RasterizationMode == RasterizationMode.Aliased, allocator); if (!linearizer.TryProcess(out LinearizedRasterData result)) @@ -263,6 +301,7 @@ internal static partial class DefaultRasterizer height, firstRowBandIndex, rowBandCount, + options.RasterizationMode == RasterizationMode.Aliased, allocator); if (!wideLinearizer.TryProcess(out LinearizedRasterData wideResult)) @@ -318,7 +357,6 @@ private static StrokeRasterizableGeometry CreateRetainedStrokeRasterizableGeomet bandTop, options.IntersectionRule, options.RasterizationMode, - options.AntialiasThreshold, coverageBoost: 0F, hasStartCovers); } @@ -335,7 +373,10 @@ private static StrokeRasterizableGeometry CreateRetainedStrokeRasterizableGeomet result.Lines, null, result.FirstBlockLineCounts, - result.StartCoverTable); + result.StartCoverTable, + result.Profiles, + result.ProfileTranslateX, + result.ProfileTranslateY); return new StrokeRasterizableGeometry( retained.FirstRowBandIndex, @@ -386,7 +427,6 @@ private static StrokeRasterizableGeometry CreateRetainedStrokeRasterizableGeomet bandTop, options.IntersectionRule, options.RasterizationMode, - options.AntialiasThreshold, coverageBoost: 0F, hasStartCovers); } @@ -403,7 +443,10 @@ private static StrokeRasterizableGeometry CreateRetainedStrokeRasterizableGeomet null, result.Lines, result.FirstBlockLineCounts, - result.StartCoverTable); + result.StartCoverTable, + result.Profiles, + result.ProfileTranslateX, + result.ProfileTranslateY); return new StrokeRasterizableGeometry( retained.FirstRowBandIndex, @@ -778,8 +821,6 @@ private readonly struct DirectLineSegmentBandRasterizer private readonly int height; private readonly int destinationLeft; private readonly int destinationTop; - private readonly RasterizationMode rasterizationMode; - private readonly float antialiasThreshold; /// /// Initializes a new instance of the struct. @@ -809,8 +850,6 @@ private DirectLineSegmentBandRasterizer( this.height = bandInfo.BandHeight; this.destinationLeft = bandInfo.DestinationLeft; this.destinationTop = bandInfo.DestinationTop; - this.rasterizationMode = bandInfo.RasterizationMode; - this.antialiasThreshold = bandInfo.AntialiasThreshold; } /// @@ -1170,7 +1209,7 @@ private void EmitPointCoverageRow( } /// - /// Applies the selected rasterization mode and emits the non-zero runs for one row. + /// Emits the non-zero antialiased runs for one row. /// /// The coverage row handler type. /// The band-local row index. @@ -1184,14 +1223,6 @@ private void FinalizeCoverageRow( ref TRowHandler rowHandler) where TRowHandler : struct, IRasterizerCoverageRowHandler { - if (this.rasterizationMode == RasterizationMode.Aliased) - { - for (int i = 0; i < rowCoverage.Length; i++) - { - rowCoverage[i] = rowCoverage[i] >= this.antialiasThreshold ? 1F : 0F; - } - } - EmitCoverageRuns(rowCoverage, startColumn, this.destinationLeft, this.destinationTop + row, ref rowHandler); } @@ -1424,7 +1455,7 @@ private static void AppendEdgeInterval( /// /// Returns the tessellation segment count used for one round join or cap arc. - /// Ported to the GPU as stroke_arc_subdivision_count in flatten.wgsl; + /// Ported to the GPU as stroke_arc_subdivision_count in path_lowering.wgsl; /// changes here must be mirrored there. /// /// The arc radius. @@ -1520,7 +1551,7 @@ private static bool TryIntersectOffsetLines( /// /// Normalizes an angle into the inclusive-exclusive range [0, 2 * PI). - /// Ported to the GPU as stroke_normalize_positive_angle in flatten.wgsl. + /// Ported to the GPU as stroke_normalize_positive_angle in path_lowering.wgsl. /// /// The angle to normalize. /// The normalized angle. diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.StrokeLinearizer.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.StrokeLinearizer.cs index 4603d2fed..0c513d26c 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.StrokeLinearizer.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.StrokeLinearizer.cs @@ -15,7 +15,7 @@ internal static partial class DefaultRasterizer /// /// /// The stroke expansion below is a port of AGG's PolygonStroker and is the reference - /// implementation for the GPU stroker: flatten.wgsl's stroke_side_join, + /// implementation for the GPU stroker: path_lowering.wgsl's stroke_side_join, /// stroke_calc_miter, stroke_calc_arc, and stroke_chain_point are direct /// ports of the corresponding members here, and WebGPUSceneEncoder.EncodeStrokePath /// ports the contour preprocessing. Any behavioral change here must be mirrored there. @@ -47,6 +47,7 @@ private abstract class StrokeLinearizer : Linearizer /// The visible destination height in pixels. /// The first retained row-band index. /// The retained row-band count. + /// Whether to retain horizontal outline edges for aliased thin-feature recovery between row centres. /// The allocator used for retained start-cover storage. protected StrokeLinearizer( LinearGeometry geometry, @@ -60,8 +61,9 @@ protected StrokeLinearizer( int height, int firstBandIndex, int rowBandCount, + bool keepHorizontalLines, MemoryAllocator allocator) - : base(geometry, residual, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, allocator) + : base(geometry, residual, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, allocator, keepHorizontalLines) => this.stroke = stroke; /// @@ -614,7 +616,7 @@ private void EmitDirectedArcContour( /// /// Appends a contour arc directly to the active stroke contour. Ported to the GPU as - /// stroke_chain_arc in flatten.wgsl; changes here must be mirrored there. + /// stroke_chain_arc in path_lowering.wgsl; changes here must be mirrored there. /// /// The active contour state. /// The arc center. @@ -667,7 +669,7 @@ private void AppendDirectedArcContour( /// join geometry as the reference CPU stroker. Each side of the outline calls this /// once per source vertex; the reverse side reverses the vertex order exactly /// like PolygonStroker's Outline2 state. Ported to the GPU as stroke_side_join - /// in flatten.wgsl; changes here must be mirrored there. + /// in path_lowering.wgsl; changes here must be mirrored there. /// /// The active contour state. /// Previous source vertex in the emission's traversal order. @@ -785,7 +787,7 @@ private void AppendSideJoinContour( /// /// Direct port of PolygonStroker.CalcMiter. Emits the miter apex (or the /// configured overflow fallback) at the join vertex. Ported to the GPU as - /// stroke_calc_miter in flatten.wgsl; changes here must be mirrored there. + /// stroke_calc_miter in path_lowering.wgsl; changes here must be mirrored there. /// /// The active contour state. /// Previous source vertex in the emission's traversal order. @@ -897,7 +899,7 @@ private void CalcMiter( /// /// Direct port of PolygonStroker.CalcArc. Emits intermediate arc vertices /// around a join center between two offset vectors. Ported to the GPU as - /// stroke_calc_arc in flatten.wgsl; changes here must be mirrored there. + /// stroke_calc_arc in path_lowering.wgsl; changes here must be mirrored there. /// /// The active contour state. /// The join center X coordinate. @@ -954,7 +956,7 @@ private void CalcArc( /// Signed area of triangle (a, b, point), matching PolygonStroker.CrossProduct; /// used as a side-of-line test for against the line through /// and . Ported to the GPU as - /// stroke_cross_product in flatten.wgsl. + /// stroke_cross_product in path_lowering.wgsl. /// /// The first line point. /// The second line point. @@ -967,7 +969,7 @@ private static float CrossProduct(Vector2 a, Vector2 b, Vector2 point) /// /// Intersects two infinite lines defined by point pairs (a, b) and (c, d), /// matching PolygonStroker.TryCalcIntersection. Ported to the GPU as - /// stroke_try_intersect in flatten.wgsl. + /// stroke_try_intersect in path_lowering.wgsl. /// /// The first point on the first line. /// The second point on the first line. @@ -994,7 +996,7 @@ private static bool TryCalcIntersection(Vector2 a, Vector2 b, Vector2 c, Vector2 /// /// Appends one point to the active contour, emitting a line from the previously appended - /// point. Ported to the GPU as stroke_chain_point in flatten.wgsl; changes + /// point. Ported to the GPU as stroke_chain_point in path_lowering.wgsl; changes /// here must be mirrored there. /// /// The active contour state. @@ -1044,13 +1046,16 @@ private void CloseContour(ref ContourState state, bool contained) /// Indicates whether the edge is fully contained within the interest. private void EmitLine(PointF start, PointF end, bool contained) { + // The stroker creates new outline edges, so they have no profiles from the source + // geometry. Sentinel tags keep centre-free stroke intervals visible. if (contained) { this.AddContainedLineF24Dot8( FloatToFixed24Dot8((start.X + this.TranslateX) - this.MinX), FloatToFixed24Dot8((start.Y + this.TranslateY) - this.MinY), FloatToFixed24Dot8((end.X + this.TranslateX) - this.MinX), - FloatToFixed24Dot8((end.Y + this.TranslateY) - this.MinY)); + FloatToFixed24Dot8((end.Y + this.TranslateY) - this.MinY), + LinearGeometryProfiles.SentinelTag); return; } @@ -1058,7 +1063,8 @@ private void EmitLine(PointF start, PointF end, bool contained) (start.X + this.TranslateX) - this.MinX, (start.Y + this.TranslateY) - this.MinY, (end.X + this.TranslateX) - this.MinX, - (end.Y + this.TranslateY) - this.MinY); + (end.Y + this.TranslateY) - this.MinY, + LinearGeometryProfiles.SentinelTag); } /// @@ -1156,6 +1162,7 @@ private sealed class StrokeLinearizerX32Y16 : StrokeLinearizer /// The visible destination height in pixels. /// The first retained row-band index. /// The retained row-band count. + /// Whether to retain horizontal outline edges for aliased thin-feature recovery between row centres. /// The allocator used for retained start-cover storage. public StrokeLinearizerX32Y16( LinearGeometry geometry, @@ -1169,8 +1176,9 @@ public StrokeLinearizerX32Y16( int height, int firstBandIndex, int rowBandCount, + bool keepHorizontalLines, MemoryAllocator allocator) - : base(geometry, residual, stroke, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, allocator) + : base(geometry, residual, stroke, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, keepHorizontalLines, allocator) => this.FinalLines = new LineArrayX32Y16Block?[rowBandCount]; /// @@ -1179,11 +1187,11 @@ public StrokeLinearizerX32Y16( public LineArrayX32Y16Block?[] FinalLines { get; } /// - protected override LineArrayX32Y16 CreateLineArray() => new(); + protected override LineArrayX32Y16 CreateLineArray() => new(false); /// - protected override void AppendLine(int rowIndex, int x0, int y0, int x1, int y1) - => this.GetOrCreateLineArray(rowIndex).AppendLine(x0, y0, x1, y1); + protected override void AppendLine(int rowIndex, int x0, int y0, int x1, int y1, uint tag) + => this.GetOrCreateLineArray(rowIndex).AppendLine(x0, y0, x1, y1, tag); /// protected override void FinalizeLines() @@ -1214,7 +1222,10 @@ public bool TryProcess(out LinearizedRasterData result) new TileBounds(this.MinX, this.FirstBandIndex, this.Width, this.RowBandCount), this.FinalLines, this.FirstBlockLineCounts, - this.StartCoverTable); + this.StartCoverTable, + this.Profiles, + this.ProfileTranslateX, + this.ProfileTranslateY); return true; } @@ -1239,6 +1250,7 @@ private sealed class StrokeLinearizerX16Y16 : StrokeLinearizer /// The visible destination height in pixels. /// The first retained row-band index. /// The retained row-band count. + /// Whether to retain horizontal outline edges for aliased thin-feature recovery between row centres. /// The allocator used for retained start-cover storage. public StrokeLinearizerX16Y16( LinearGeometry geometry, @@ -1252,8 +1264,9 @@ public StrokeLinearizerX16Y16( int height, int firstBandIndex, int rowBandCount, + bool keepHorizontalLines, MemoryAllocator allocator) - : base(geometry, residual, stroke, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, allocator) + : base(geometry, residual, stroke, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, keepHorizontalLines, allocator) => this.FinalLines = new LineArrayX16Y16Block?[rowBandCount]; /// @@ -1262,11 +1275,11 @@ public StrokeLinearizerX16Y16( public LineArrayX16Y16Block?[] FinalLines { get; } /// - protected override LineArrayX16Y16 CreateLineArray() => new(); + protected override LineArrayX16Y16 CreateLineArray() => new(false); /// - protected override void AppendLine(int rowIndex, int x0, int y0, int x1, int y1) - => this.GetOrCreateLineArray(rowIndex).AppendLine(x0, y0, x1, y1); + protected override void AppendLine(int rowIndex, int x0, int y0, int x1, int y1, uint tag) + => this.GetOrCreateLineArray(rowIndex).AppendLine(x0, y0, x1, y1, tag); /// protected override void FinalizeLines() @@ -1297,7 +1310,10 @@ public bool TryProcess(out LinearizedRasterData result) new TileBounds(this.MinX, this.FirstBandIndex, this.Width, this.RowBandCount), this.FinalLines, this.FirstBlockLineCounts, - this.StartCoverTable); + this.StartCoverTable, + this.Profiles, + this.ProfileTranslateX, + this.ProfileTranslateY); return true; } diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs index 7f241e5d6..dd07e0c86 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs @@ -4,6 +4,7 @@ using System.Buffers; using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; @@ -59,6 +60,43 @@ internal static partial class DefaultRasterizer /// private const int AreaToCoverageShift = 9; + /// + /// Half of , the offset from a pixel's left edge to its centre. + /// + private const int FixedHalf = FixedOne >> 1; + + /// + /// Maximum number of crossings stored for one column in a 16-row band. Later crossings do not + /// take part in the secondary column pass. The primary row result is unchanged. + /// + private const int ColumnCrossingCapacity = 16; + + /// + /// Maximum number of pixels produced by the secondary column pass for one band. The fixed + /// bound keeps this temporary list on the stack. + /// + private const int MaxColumnDropoutsPerBand = 256; + + /// + /// Number of low bits reserved for a crossing's profile identifier. + /// + private const int ProfileIdBits = 16; + + /// + /// Mask for the low sixteen-bit profile field in a packed crossing. + /// + private const int ProfileIdMask = (1 << ProfileIdBits) - 1; + + /// + /// The bit inside a packed crossing that records a positive crossing direction. + /// + private const long CrossingDirectionBit = 1L << ProfileIdBits; + + /// + /// The left shift that positions a crossing coordinate above its direction and profile bits. + /// + private const int CrossingShift = ProfileIdBits + 1; + /// /// Number of discrete coverage steps per fully covered pixel (one 24.8 unit of winding). /// @@ -108,11 +146,28 @@ public static void ExecuteRasterizableItem( bandInfo.BandHeight, bandInfo.IntersectionRule, bandInfo.RasterizationMode, - bandInfo.AntialiasThreshold, bandInfo.CoverageBoost); context.SeedStartCovers(item.GetActualCovers()); - if (item.Rasterizable.IsX16) + if (bandInfo.RasterizationMode == RasterizationMode.Aliased) + { + context.SetProfileTables( + item.Rasterizable.Profiles, + item.Rasterizable.ProfileTranslateX, + item.Rasterizable.ProfileTranslateY); + + if (item.Rasterizable.IsX16) + { + LineArrayX16Y16Block? lines = item.GetLineArrayX16(); + lines?.IterateTagged(item.GetFirstBlockLineCount(), ref context); + } + else + { + LineArrayX32Y16Block? lines = item.GetLineArrayX32(); + lines?.IterateTagged(item.GetFirstBlockLineCount(), ref context); + } + } + else if (item.Rasterizable.IsX16) { LineArrayX16Y16Block? lines = item.GetLineArrayX16(); lines?.Iterate(item.GetFirstBlockLineCount(), ref context); @@ -153,9 +208,11 @@ public static void ExecuteStrokeRasterizableItem( bandInfo.BandHeight, bandInfo.IntersectionRule, bandInfo.RasterizationMode, - bandInfo.AntialiasThreshold, bandInfo.CoverageBoost); + // Stroke outline edges have no source profiles. Clear the previous fill's profile data so + // a stroke cannot use it when classifying a centre-free interval. + context.SetProfileTables(default, 0F, 0F); item.Rasterizable.ExecuteBand(ref context, in bandInfo, scanline, strokeBandCoverage, ref rowHandler); } @@ -230,6 +287,7 @@ private static void ThrowInterestBoundsTooLarge() /// The integer Y translation applied after the residual transform. /// The rasterizer options carrying interest bounds, fill rule, and antialias settings. /// The memory allocator used for retained start-cover storage. + /// The partition-owned profile arena used by aliased fills. /// The retained geometry payload, or when nothing is visible. public static RasterizableGeometry? CreateRasterizableGeometry( LinearGeometry geometry, @@ -237,7 +295,8 @@ private static void ThrowInterestBoundsTooLarge() int translateX, int translateY, in RasterizerOptions options, - MemoryAllocator allocator) + MemoryAllocator allocator, + LinearGeometryProfileStorage profileStorage) { RectangleF translatedBounds = residual.IsIdentity ? geometry.Info.Bounds : RectangleF.Transform(geometry.Info.Bounds, residual); translatedBounds.Offset(translateX, translateY); @@ -284,7 +343,9 @@ private static void ThrowInterestBoundsTooLarge() height, firstRowBandIndex, rowBandCount, - allocator); + allocator, + profileStorage, + options.RasterizationMode == RasterizationMode.Aliased); if (!linearizer.TryProcess(out LinearizedRasterData result)) { @@ -306,7 +367,6 @@ private static void ThrowInterestBoundsTooLarge() bandTop, options.IntersectionRule, options.RasterizationMode, - options.AntialiasThreshold, options.CoverageBoost, hasStartCovers); } @@ -323,7 +383,10 @@ private static void ThrowInterestBoundsTooLarge() result.Lines, null, result.FirstBlockLineCounts, - result.StartCoverTable); + result.StartCoverTable, + result.Profiles, + result.ProfileTranslateX, + result.ProfileTranslateY); } else { @@ -338,7 +401,9 @@ private static void ThrowInterestBoundsTooLarge() height, firstRowBandIndex, rowBandCount, - allocator); + allocator, + profileStorage, + options.RasterizationMode == RasterizationMode.Aliased); if (!linearizer.TryProcess(out LinearizedRasterData result)) { @@ -360,7 +425,6 @@ private static void ThrowInterestBoundsTooLarge() bandTop, options.IntersectionRule, options.RasterizationMode, - options.AntialiasThreshold, options.CoverageBoost, hasStartCovers); } @@ -377,7 +441,10 @@ private static void ThrowInterestBoundsTooLarge() null, result.Lines, result.FirstBlockLineCounts, - result.StartCoverTable); + result.StartCoverTable, + result.Profiles, + result.ProfileTranslateX, + result.ProfileTranslateY); } } @@ -426,13 +493,22 @@ internal ref struct Context private readonly Span rowHasBits; private readonly Span rowTouched; private readonly Span touchedRows; + private readonly Span crossings; + private readonly Span crossingCounts; + private readonly Span columnCrossings; + private readonly Span columnCrossingCounts; + private readonly Span touchedColumns; + private LinearGeometryProfiles profiles; + private float columnProfileTranslate; + private float rowProfileTranslate; + private int touchedColumnCount; + private readonly int crossingStride; private int width; private int height; private int wordsPerRow; private int coverStride; private IntersectionRule intersectionRule; private RasterizationMode rasterizationMode; - private float antialiasThreshold; private float coverageBoost; private int touchedRowCount; @@ -447,9 +523,14 @@ internal ref struct Context /// Scratch flags indicating whether a row has any bit-vector backed cell data. /// Scratch flags indicating whether a row has received any contribution in the current band. /// Scratch list of rows touched in the current band so emission can skip untouched rows. + /// Scratch storage for where the outline crosses each scanline's centre line. + /// Scratch storage for the number of crossings recorded on each row. + /// Scratch storage for where the outline crosses each column's centre line. + /// Scratch storage for the number of crossings recorded on each column. + /// Scratch list of columns that received centre line crossings. + /// The exact per-row crossing capacity for the retained band. /// The fill rule used when converting accumulated winding/coverage into final alpha. - /// The rasterization mode that controls how antialiasing thresholds are interpreted. - /// The threshold used when antialiasing is conditionally reduced or disabled. + /// The rasterization mode that selects continuous or centre-sampled coverage. public Context( Span bitVectors, Span coverArea, @@ -459,10 +540,25 @@ public Context( Span rowHasBits, Span rowTouched, Span touchedRows, + Span crossings, + Span crossingCounts, + Span columnCrossings, + Span columnCrossingCounts, + Span touchedColumns, + int crossingStride, IntersectionRule intersectionRule, - RasterizationMode rasterizationMode, - float antialiasThreshold) + RasterizationMode rasterizationMode) { + this.crossings = crossings; + this.crossingCounts = crossingCounts; + this.columnCrossings = columnCrossings; + this.columnCrossingCounts = columnCrossingCounts; + this.touchedColumns = touchedColumns; + this.profiles = default; + this.columnProfileTranslate = 0F; + this.rowProfileTranslate = 0F; + this.touchedColumnCount = 0; + this.crossingStride = crossingStride; this.bitVectors = bitVectors; this.coverArea = coverArea; this.startCover = startCover; @@ -477,7 +573,6 @@ public Context( this.coverStride = 0; this.intersectionRule = intersectionRule; this.rasterizationMode = rasterizationMode; - this.antialiasThreshold = antialiasThreshold; this.coverageBoost = 0F; this.touchedRowCount = 0; } @@ -490,8 +585,7 @@ public Context( /// The stride, in cells, between rows in the cover/area table. /// The height, in pixels, of the current destination band. /// The fill rule used when converting accumulated winding/coverage into final alpha. - /// The rasterization mode that controls how antialiasing thresholds are interpreted. - /// The threshold used when antialiasing is conditionally reduced or disabled. + /// The rasterization mode that selects continuous or centre-sampled coverage. /// The perceptual coverage boost applied to antialiased coverage; zero disables it. public void Reconfigure( int width, @@ -500,7 +594,6 @@ public void Reconfigure( int height, IntersectionRule intersectionRule, RasterizationMode rasterizationMode, - float antialiasThreshold, float coverageBoost) { this.width = width; @@ -509,7 +602,6 @@ public void Reconfigure( this.coverStride = coverStride; this.intersectionRule = intersectionRule; this.rasterizationMode = rasterizationMode; - this.antialiasThreshold = antialiasThreshold; this.coverageBoost = coverageBoost; } @@ -599,7 +691,18 @@ public void AddClippedStartCover(int y0, int y1) /// The ending X coordinate in 24.8 fixed-point destination space. /// The ending Y coordinate in 24.8 fixed-point destination space. public void RasterizeLineSegment(int x0, int y0, int x1, int y1) - => this.RasterizeLine(x0, y0, x1, y1); + => this.RasterizeLine(x0, y0, x1, y1, LinearGeometryProfiles.SentinelTag); + + /// + /// Rasterizes one line segment and retains its profile identifiers for aliased tip classification. + /// + /// The starting X coordinate in 24.8 fixed-point destination space. + /// The starting Y coordinate in 24.8 fixed-point destination space. + /// The ending X coordinate in 24.8 fixed-point destination space. + /// The ending Y coordinate in 24.8 fixed-point destination space. + /// The segment's profile tag. + public void RasterizeLineSegment(int x0, int y0, int x1, int y1, uint tag) + => this.RasterizeLine(x0, y0, x1, y1, tag); /// /// Converts accumulated cover/area tables into non-zero coverage span callbacks. @@ -615,6 +718,30 @@ public readonly void EmitCoverageRows( Span scanline, ref TRowHandler rowHandler) where TRowHandler : struct, IRasterizerCoverageRowHandler + { + if (this.rasterizationMode == RasterizationMode.Aliased) + { + this.EmitCentreSampledRows(destinationTop, destinationLeft, scanline, ref rowHandler); + return; + } + + this.EmitRows(destinationTop, destinationLeft, scanline, ref rowHandler); + } + + /// + /// Walks every touched row and hands its spans to the supplied handler. + /// + /// The struct coverage handler type; constrained to a value type so calls devirtualize. + /// Absolute destination Y corresponding to row zero in this context. + /// Absolute destination X corresponding to column zero in this context. + /// Reusable scanline scratch buffer used to materialize emitted spans. + /// Coverage callback invoked for each emitted non-zero span. + private readonly void EmitRows( + int destinationTop, + int destinationLeft, + Span scanline, + ref TRowHandler rowHandler) + where TRowHandler : struct, IRasterizerCoverageRowHandler { // Iterate only rows that actually received coverage contributions. // MarkRowTouched is called from AddCell for all contributions, including @@ -655,6 +782,19 @@ public readonly void EmitCoverageRows( } } + /// + /// Selects the profile data used to classify thin intervals in the next retained geometry. + /// + /// The geometry-space profile data, or an empty value. + /// The X translation from geometry space to destination space. + /// The Y translation from geometry space to destination space. + public void SetProfileTables(LinearGeometryProfiles profiles, float translateX, float translateY) + { + this.profiles = profiles; + this.columnProfileTranslate = translateX; + this.rowProfileTranslate = translateY; + } + /// /// Clears only rows touched during the previous rasterization pass. /// @@ -665,12 +805,18 @@ 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. + bool hasCrossings = !this.crossingCounts.IsEmpty; for (int i = 0; i < this.touchedRowCount; i++) { int row = this.touchedRows[i]; this.startCover[row] = 0; this.rowTouched[row] = 0; + if (hasCrossings) + { + this.crossingCounts[row] = 0; + } + if (this.rowHasBits[row] == 0) { continue; @@ -686,6 +832,15 @@ public void ResetTouchedRows() } this.touchedRowCount = 0; + + // Column crossing scratch is pooled alongside the row scratch, so the columns this + // pass touched must be cleared the same sparse way before the next pass reuses it. + for (int i = 0; i < this.touchedColumnCount; i++) + { + this.columnCrossingCounts[this.touchedColumns[i]] = 0; + } + + this.touchedColumnCount = 0; } /// @@ -843,6 +998,628 @@ private readonly void EmitRowCoverage( FlushBufferedRun(ref rowHandler, destinationY, destinationLeft, scanline, ref runStart, ref runEnd); } + /// + /// Emits an aliased band by setting each pixel whose centre the shape covers. + /// + /// The struct coverage handler type; constrained to a value type so calls devirtualize. + /// Absolute destination Y corresponding to row zero in this context. + /// Absolute destination X corresponding to column zero in this context. + /// Reusable scanline scratch buffer used to materialize emitted spans. + /// Coverage callback invoked for each emitted run. + private readonly void EmitCentreSampledRows( + int destinationTop, + int destinationLeft, + Span scanline, + ref TRowHandler rowHandler) + where TRowHandler : struct, IRasterizerCoverageRowHandler + { + // The main pass walks horizontal centre lines. It cannot see a horizontal feature that + // lies completely between two row centres. The column pass finds those features first. + // For each closed vertical interval that contains no row centre, it records the pixel + // containing the interval midpoint. Packing row and column into one integer lets one + // sort group the results by row and then by column. + Span columnDropouts = stackalloc int[MaxColumnDropoutsPerBand]; + int columnDropoutCount = this.CollectColumnDropouts(columnDropouts, destinationLeft); + columnDropouts = columnDropouts[..columnDropoutCount]; + columnDropouts.Sort(); + + for (int i = 0; i < this.touchedRowCount; i++) + { + int row = this.touchedRows[i]; + int count = this.crossingCounts[row]; + + // startCover contains signed vertical cover from edges left of this band. One full + // pixel of cover represents one crossing, so round it to the winding at the first + // pixel centre before processing the retained crossings. + int winding = NearestCrossingCount(this.startCover[row]); + if (this.intersectionRule == IntersectionRule.EvenOdd) + { + winding &= 1; + } + + if (count == 0) + { + if (winding != 0) + { + scanline[..this.width].Fill(1F); + rowHandler.Handle(destinationTop + row, destinationLeft, scanline[..this.width]); + } + + continue; + } + + Span rowCrossings = this.crossings.Slice(row * this.crossingStride, count); + rowCrossings.Sort(); + + // Binary searches select this row's range from the sorted column-pass results. + // Results covered by a normal row interval are discarded below. The remaining + // results are inserted as one-pixel intervals. + int rowKey = row << 16; + int dropoutStart = LowerBound(columnDropouts, rowKey); + int dropoutEnd = LowerBound(columnDropouts, rowKey + (1 << 16)); + + int intervalStart = 0; + long intervalStartCrossing = long.MinValue; + int runStart = -1; + int runEnd = -1; + + for (int c = 0; c <= count; c++) + { + int x; + long currentCrossing = long.MinValue; + int previousWinding = winding; + + if (c < count) + { + long crossing = rowCrossings[c]; + currentCrossing = crossing; + x = (int)(crossing >> CrossingShift); + winding = this.intersectionRule == IntersectionRule.NonZero + ? winding + (((crossing & CrossingDirectionBit) != 0) ? 1 : -1) + : winding ^ 1; + } + else + { + // A non-zero winding after the last retained crossing means the fill + // continues through the right clip edge. Close that interval at the edge. + if (previousWinding == 0) + { + break; + } + + x = this.width << FixedShift; + winding = 0; + } + + if (previousWinding == 0 && winding != 0) + { + intervalStart = x; + intervalStartCrossing = currentCrossing; + continue; + } + + if (previousWinding == 0 || winding != 0) + { + continue; + } + + // Coverage is closed at both ends. CeilingPixel finds the first centre at or + // after the start; FloorPixel finds the last centre at or before the end. + int first = CeilingPixel(intervalStart); + int last = FloorPixel(x); + if (last < first) + { + // This interval contains no pixel centre. Normally, preserve the thin + // feature by lighting the pixel that contains its midpoint. Do not light it + // when the two boundary profiles identify a terminating contour tip. + if (intervalStartCrossing != long.MinValue && currentCrossing != long.MinValue && IsStubInterval( + this.profiles, + xAxis: false, + this.rowProfileTranslate, + intervalStartCrossing, + currentCrossing, + ((destinationTop + row) << FixedShift) + FixedHalf, + x - intervalStart)) + { + continue; + } + + first = ((intervalStart >> 1) + (x >> 1)) >> FixedShift; + last = first; + } + + first = Math.Max(first, 0); + last = Math.Min(last, this.width - 1); + if (last < first) + { + continue; + } + + // Insert pending column results before this row interval. Discard a result + // inside the interval because the row pass already covers that pixel. + while (dropoutStart < dropoutEnd) + { + int dropColumn = columnDropouts[dropoutStart] & 0xFFFF; + if (dropColumn >= first) + { + if (dropColumn <= last) + { + dropoutStart++; + continue; + } + + break; + } + + dropoutStart++; + AccumulateRun(scanline, dropColumn, dropColumn, ref runStart, ref runEnd, destinationTop + row, destinationLeft, ref rowHandler); + } + + AccumulateRun(scanline, first, last, ref runStart, ref runEnd, destinationTop + row, destinationLeft, ref rowHandler); + } + + while (dropoutStart < dropoutEnd) + { + int dropColumn = columnDropouts[dropoutStart++] & 0xFFFF; + AccumulateRun(scanline, dropColumn, dropColumn, ref runStart, ref runEnd, destinationTop + row, destinationLeft, ref rowHandler); + } + + if (runStart >= 0) + { + scanline[..(runEnd - runStart)].Fill(1F); + rowHandler.Handle(destinationTop + row, destinationLeft + runStart, scanline[..(runEnd - runStart)]); + } + } + } + + /// + /// Adds one inclusive pixel interval to the row's run, coalescing with the open run or + /// flushing it when a gap intervenes. + /// + /// The struct coverage handler type; constrained to a value type so calls devirtualize. + /// Reusable scanline scratch buffer used to materialize emitted spans. + /// The inclusive first pixel of the interval. + /// The inclusive last pixel of the interval. + /// The open run's inclusive start pixel; negative when no run is open. + /// The open run's exclusive end pixel. + /// Absolute destination Y for the emitted row. + /// Absolute destination X corresponding to column zero in this context. + /// Coverage callback invoked for each flushed run. + private static void AccumulateRun( + Span scanline, + int first, + int last, + ref int runStart, + ref int runEnd, + int destinationY, + int destinationLeft, + ref TRowHandler rowHandler) + where TRowHandler : struct, IRasterizerCoverageRowHandler + { + if (runStart < 0) + { + runStart = first; + runEnd = last + 1; + return; + } + + if (first <= runEnd) + { + runEnd = Math.Max(runEnd, last + 1); + return; + } + + scanline[..(runEnd - runStart)].Fill(1F); + rowHandler.Handle(destinationY, destinationLeft + runStart, scanline[..(runEnd - runStart)]); + runStart = first; + runEnd = last + 1; + } + + /// + /// Finds the first index in a sorted span whose value is not less than the given key. + /// + /// The sorted values. + /// The search key. + /// The lower bound index. + private static int LowerBound(ReadOnlySpan values, int key) + { + int lo = 0; + int hi = values.Length; + while (lo < hi) + { + int mid = (lo + hi) >> 1; + if (values[mid] < key) + { + lo = mid + 1; + } + else + { + hi = mid; + } + } + + return lo; + } + + /// + /// Tests whether a centre-free interval ends at a terminating contour tip. + /// + /// + /// A centre-free interval is the closed span between two crossings when that span contains + /// no pixel centre. The normal thin-feature rule lights its midpoint pixel. It must not + /// extend a contour tip into the next empty pixel. The two boundary profiles identify that + /// case because they meet in one contour and both end inside the same pixel gap. + /// + /// The geometry-space profile data. + /// Whether to read the X-axis profile table. + /// The absolute translation applied to the profile extents. + /// The packed crossing that opened the interval. + /// The packed crossing that closed the interval. + /// The scanline's centre coordinate in 24.8 fixed-point. + /// The interval's length along the scanline in 24.8 fixed-point. + /// + /// when the interval must remain unlit because it is a terminating tip. + /// + private static bool IsStubInterval( + LinearGeometryProfiles profiles, + bool xAxis, + float translate, + long enterCrossing, + long exitCrossing, + int centre, + int intervalSpan) + { + int a = (int)(enterCrossing & ProfileIdMask); + int b = (int)(exitCrossing & ProfileIdMask); + + // The sentinel means that the segment has no usable profile. Without both profiles, + // the interval cannot be identified as a terminating tip and must remain visible. + if (a == ProfileIdMask || b == ProfileIdMask || profiles.IsEmpty) + { + return false; + } + + if (xAxis) + { + profiles.GetXProfile(a, out float minAFloat, out float maxAFloat, out int linkA); + profiles.GetXProfile(b, out float minBFloat, out float maxBFloat, out int linkB); + return IsStubIntervalCore( + minAFloat, + maxAFloat, + linkA, + minBFloat, + maxBFloat, + linkB, + a, + b, + translate, + centre, + intervalSpan); + } + + profiles.GetYProfile(a, out float rowMinA, out float rowMaxA, out int rowLinkA); + profiles.GetYProfile(b, out float rowMinB, out float rowMaxB, out int rowLinkB); + return IsStubIntervalCore( + rowMinA, + rowMaxA, + rowLinkA, + rowMinB, + rowMaxB, + rowLinkB, + a, + b, + translate, + centre, + intervalSpan); + } + + /// + /// Applies the terminating-tip test to two profile records. + /// + /// The first profile's minimum extent. + /// The first profile's maximum extent. + /// The first profile's adjacency link. + /// The second profile's minimum extent. + /// The second profile's maximum extent. + /// The second profile's adjacency link. + /// The first profile identifier. + /// The second profile identifier. + /// The absolute translation applied to the profile extents. + /// The scanline's centre coordinate in 24.8 fixed-point. + /// The interval's length along the scanline in 24.8 fixed-point. + /// when the interval must remain unlit. + private static bool IsStubIntervalCore( + float minAFloat, + float maxAFloat, + int linkA, + float minBFloat, + float maxBFloat, + int linkB, + int a, + int b, + float translate, + int centre, + int intervalSpan) + { + bool adjacent = (b == a + 1 && (linkB & 1) != 0) + || (a == b + 1 && (linkA & 1) != 0) + || (linkA >> 1) == b + 1 + || (linkB >> 1) == a + 1; + + // Only two profiles joined in the same contour can describe one terminating tip. + // Separate contours or non-adjacent profiles describe a continuing thin feature. + if (!adjacent) + { + return false; + } + + // Profiles are stored in geometry coordinates. Translate only the two records needed + // by this rare test. This avoids creating translated profile arrays for every draw. + int minA = TranslateProfileExtent(minAFloat, translate); + int maxA = TranslateProfileExtent(maxAFloat, translate); + int minB = TranslateProfileExtent(minBFloat, translate); + int maxB = TranslateProfileExtent(maxBFloat, translate); + + // Both profiles must turn around before the next centre on the same side of this + // scanline. That places the interval at the end of the contour feature. A tip still + // remains visible when it reaches at least halfway into that pixel and the interval + // itself is at least half a pixel long. + if (maxA < centre + FixedOne && maxB < centre + FixedOne) + { + int tip = Math.Max(maxA, maxB); + return !((tip & (FixedOne - 1)) >= FixedHalf && intervalSpan >= FixedHalf); + } + + if (minA > centre - FixedOne && minB > centre - FixedOne) + { + int tip = Math.Min(minA, minB); + int fraction = tip & (FixedOne - 1); + return !(fraction != 0 && fraction <= FixedHalf && intervalSpan >= FixedHalf); + } + + return false; + } + + /// + /// Converts one translated geometry-space profile extent to signed 24.8 fixed-point. + /// + /// The geometry-space extent. + /// The absolute device translation. + /// The translated extent in signed 24.8 fixed-point. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int TranslateProfileExtent(float extent, float translate) + => FloatToFixed24Dot8(Math.Clamp(extent + translate, -8388608F, 8388607F)); + + /// + /// Records where one segment crosses the vertical centre line of each affected column. + /// + /// The segment's start X in 24.8 fixed-point. + /// The segment's start Y in 24.8 fixed-point. + /// The segment's end X in 24.8 fixed-point. + /// The segment's end Y in 24.8 fixed-point. + /// The segment's x-profile identifier. + private void CaptureColumnCrossings(int x0, int y0, int x1, int y1, uint xProfileId) + { + if (x0 == x1) + { + return; + } + + int direction = x1 > x0 ? 1 : -1; + int left = direction > 0 ? x0 : x1; + int right = direction > 0 ? x1 : x0; + + int firstColumn = Math.Max(0, left >> FixedShift); + int lastColumn = Math.Min(this.width - 1, (right - 1) >> FixedShift); + + for (int column = firstColumn; column <= lastColumn; column++) + { + int sampleX = (column << FixedShift) + FixedHalf; + + // Use a half-open X range so a shared vertex is owned by one edge only. This is + // the transposed form of the row-crossing rule used below. + if (sampleX < left || sampleX >= right) + { + continue; + } + + int count = this.columnCrossingCounts[column]; + if (count >= ColumnCrossingCapacity) + { + continue; + } + + if (count == 0 && this.touchedColumnCount < this.touchedColumns.Length) + { + this.touchedColumns[this.touchedColumnCount++] = column; + } + + int y = y0 + (int)(((long)(y1 - y0) * (sampleX - x0)) / (x1 - x0)); + + // Column coordinates are band-local and need at most 13 bits in 24.8 form. + // The complete packed crossing therefore fits in one word, unlike row crossings + // whose X coordinate spans the full destination width. + this.columnCrossings[(column * ColumnCrossingCapacity) + count] = ((uint)y << CrossingShift) | (direction > 0 ? (uint)CrossingDirectionBit : 0U) | xProfileId; + this.columnCrossingCounts[column] = count + 1; + } + } + + /// + /// Finds closed vertical intervals that contain no row centre. + /// + /// The destination for packed midpoint pixel coordinates. + /// Absolute destination X corresponding to column zero, used to compare the column centre with absolute profile ranges. + /// + /// The number of midpoint pixels written. Each result is packed as + /// (row << 16) | column. + /// + private readonly int CollectColumnDropouts(Span dropouts, int destinationLeft) + { + int count = 0; + for (int i = 0; i < this.touchedColumnCount; i++) + { + int column = this.touchedColumns[i]; + int crossingCount = this.columnCrossingCounts[column]; + if (crossingCount < 2) + { + continue; + } + + Span crossings = this.columnCrossings.Slice(column * ColumnCrossingCapacity, crossingCount); + crossings.Sort(); + + int winding = 0; + int intervalStart = 0; + long intervalStartCrossing = long.MinValue; + for (int c = 0; c < crossingCount; c++) + { + long crossing = crossings[c]; + int y = (int)(crossing >> CrossingShift); + int previousWinding = winding; + winding = this.intersectionRule == IntersectionRule.NonZero + ? winding + (((crossing & CrossingDirectionBit) != 0) ? 1 : -1) + : winding ^ 1; + + if (previousWinding == 0 && winding != 0) + { + intervalStart = y; + intervalStartCrossing = crossing; + continue; + } + + if (previousWinding == 0 || winding != 0) + { + continue; + } + + if (FloorPixel(y) >= CeilingPixel(intervalStart)) + { + // The primary row pass sees this interval and needs no secondary result. + continue; + } + + if (IsStubInterval( + this.profiles, + xAxis: true, + this.columnProfileTranslate, + intervalStartCrossing, + crossing, + ((destinationLeft + column) << FixedShift) + FixedHalf, + y - intervalStart)) + { + // The interval ends at a contour tip and must remain unlit. + continue; + } + + int row = ((intervalStart >> 1) + (y >> 1)) >> FixedShift; + if (row < 0 || row >= this.height || count == dropouts.Length) + { + continue; + } + + dropouts[count++] = (row << 16) | column; + } + } + + return count; + } + + /// + /// Converts a row's signed 24.8 start cover to an integer winding count. + /// + /// + /// One complete cover unit represents one crossing to the left of the band. A partial unit + /// represents an edge that crosses only part of the row. Rounding to the nearest signed + /// unit determines whether that edge has crossed the row centre. + /// + /// The row's accumulated carry cover in 24.8 units. + /// The signed crossing count. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int NearestCrossingCount(int startCover) + => startCover >= 0 + ? (startCover + FixedHalf) >> FixedShift + : -((-startCover + FixedHalf) >> FixedShift); + + /// + /// Returns the first pixel whose centre is at or after the given 24.8 coordinate. + /// + /// The 24.8 fixed-point coordinate. + /// The pixel index. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int CeilingPixel(int value) + { + int shifted = value - FixedHalf; + return shifted >= 0 + ? (shifted + FixedOne - 1) >> FixedShift + : -((-shifted) >> FixedShift); + } + + /// + /// Returns the last pixel whose centre is at or before the given 24.8 coordinate. + /// + /// The 24.8 fixed-point coordinate. + /// The pixel index. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int FloorPixel(int value) + { + int shifted = value - FixedHalf; + return shifted >= 0 + ? shifted >> FixedShift + : -(((-shifted) + FixedOne - 1) >> FixedShift); + } + + /// + /// Records where one segment crosses the horizontal centre line of each affected row. + /// + /// + /// Aliased coverage depends on whether the shape contains the pixel centre. Area coverage + /// alone cannot answer this because equal covered areas can lie on opposite sides of the + /// centre. The sorted crossings define the exact filled intervals along the centre line. + /// + /// The segment's start X in 24.8 fixed-point. + /// The segment's start Y in 24.8 fixed-point. + /// The segment's end X in 24.8 fixed-point. + /// The segment's end Y in 24.8 fixed-point. + /// The segment's profile tag: the x-profile identifier in the low sixteen bits and the y-profile identifier in the high sixteen bits. + private void CaptureCrossings(int x0, int y0, int x1, int y1, uint tag) + { + this.CaptureColumnCrossings(x0, y0, x1, y1, tag & ProfileIdMask); + + if (y0 == y1) + { + return; + } + + int direction = y1 > y0 ? 1 : -1; + int top = direction > 0 ? y0 : y1; + int bottom = direction > 0 ? y1 : y0; + + int firstRow = Math.Max(0, top >> FixedShift); + int lastRow = Math.Min(this.height - 1, (bottom - 1) >> FixedShift); + + for (int row = firstRow; row <= lastRow; row++) + { + int sampleY = (row << FixedShift) + FixedHalf; + + // Use a half-open Y range so two edges meeting at a vertex do not both contribute + // that vertex to the winding count. + if (sampleY < top || sampleY >= bottom) + { + continue; + } + + int count = this.crossingCounts[row]; + int x = x0 + (int)(((long)(x1 - x0) * (sampleY - y0)) / (y1 - y0)); + + // Position, direction, and profile pack into one value ordered by position, so a + // row sorts with a single primitive sort. The lower bits only break ties between + // equal positions, where crossing order cannot change the winding result. + this.crossings[(row * this.crossingStride) + count] = ((long)x << CrossingShift) | (direction > 0 ? CrossingDirectionBit : 0L) | (tag >> 16); + this.crossingCounts[row] = count + 1; + this.MarkRowTouched(row); + } + } + /// /// Converts accumulated signed area to normalized coverage under the selected fill rule. /// @@ -882,13 +1659,6 @@ private readonly float AreaToCoverage(int area) coverage = wrapped >= CoverageStepCount ? 1F : wrapped * CoverageScale; } - if (this.rasterizationMode == RasterizationMode.Aliased) - { - // Aliased mode quantizes final coverage to hard 0/1 per pixel - // using the configurable threshold from GraphicsOptions.AntialiasThreshold. - return coverage >= this.antialiasThreshold ? 1F : 0F; - } - if (this.coverageBoost != 0F) { // Perceptual contrast boost for text: an S-curve that darkens coverage above @@ -1723,8 +2493,21 @@ private void LineUpL(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y /// The starting Y coordinate in 24.8 fixed-point band-local space. /// The ending X coordinate in 24.8 fixed-point band-local space. /// The ending Y coordinate in 24.8 fixed-point band-local space. - private void RasterizeLine(int x0, int y0, int x1, int y1) + /// The segment's profile tag. + private void RasterizeLine(int x0, int y0, int x1, int y1, uint tag) { + if (this.rasterizationMode == RasterizationMode.Aliased) + { + this.CaptureCrossings(x0, y0, x1, y1, tag); + } + + // Horizontal segments are retained only so the aliased column pass can find features + // between row centres. They add no area, and the row walker cannot divide by zero Y. + if (y0 == y1) + { + return; + } + if (x0 == x1) { // Vertical edges need ownership adjustment to avoid double counting at cell seams. @@ -1822,6 +2605,8 @@ internal sealed class WorkerScratch : IDisposable private readonly IMemoryOwner touchedRowsOwner; private readonly IMemoryOwner scanlineOwner; private IMemoryOwner? strokeBandCoverageOwner; + private IMemoryOwner? crossingsOwner; + private IMemoryOwner? aliasedScratchOwner; /// /// Initializes a new instance of the class taking ownership of the supplied buffers. @@ -1885,6 +2670,88 @@ public Span StrokeBandCoverage this.allocator.Allocate(checked(this.width * this.tileCapacity * DirectStrokeVerticalSampleCount))) .Memory.Span; + /// + /// Gets reusable per-row crossing count scratch, created on first aliased use. + /// + public Span CrossingCounts + => MemoryMarshal.Cast(this.GetAliasedScratch()[..this.tileCapacity]); + + /// + /// Gets reusable per-column centre line crossing scratch, created on first aliased use. + /// + public Span ColumnCrossings + => this.GetAliasedScratch().Slice(this.tileCapacity, checked(this.width * ColumnCrossingCapacity)); + + /// + /// Gets reusable per-column crossing count scratch, created on first aliased use. + /// + public Span ColumnCrossingCounts + { + get + { + int offset = checked(this.tileCapacity + (this.width * ColumnCrossingCapacity)); + return MemoryMarshal.Cast(this.GetAliasedScratch().Slice(offset, this.width)); + } + } + + /// + /// Gets the reusable touched column index list, created on first aliased use. + /// + public Span TouchedColumns + { + get + { + int offset = checked(this.tileCapacity + (this.width * (ColumnCrossingCapacity + 1))); + return MemoryMarshal.Cast(this.GetAliasedScratch().Slice(offset, this.width)); + } + } + + /// + /// Gets the worker's fixed-size aliased scratch rental, creating it on first use. + /// + /// The packed row-count, column-crossing, column-count, and touched-column storage. + private Span GetAliasedScratch() + { + if (this.aliasedScratchOwner is null) + { + // This allocation belongs to WorkerScratch, not to a geometry. The worker reuses it + // for every aliased geometry until the worker is disposed. + int columnCrossingLength = checked(this.width * ColumnCrossingCapacity); + int length = checked(this.tileCapacity + columnCrossingLength + this.width + this.width); + this.aliasedScratchOwner = this.allocator.Allocate(length); + + // Only counts require an initial zero. Crossing values and touched indices are + // overwritten before use, so clearing their much larger slices would be wasted work. + Span scratch = this.aliasedScratchOwner.Memory.Span; + scratch[..this.tileCapacity].Clear(); + scratch.Slice(this.tileCapacity + columnCrossingLength, this.width).Clear(); + } + + return this.aliasedScratchOwner.Memory.Span; + } + + /// + /// Gets the worker's growable row-crossing rental. Each row reserves one slot per retained + /// line because one line can cross a row centre no more than once. + /// + /// The required crossing slots for each row. + /// The rented crossing storage. + private Span GetCrossings(int crossingStride) + { + int requiredLength = checked(this.tileCapacity * crossingStride); + if (this.crossingsOwner is null || this.crossingsOwner.Memory.Length < requiredLength) + { + // Keep the largest rental used by this worker. Later geometries at or below that + // complexity reuse it without another allocation. + IMemoryOwner replacement = this.allocator.Allocate(requiredLength); + + this.crossingsOwner?.Dispose(); + this.crossingsOwner = replacement; + } + + return this.crossingsOwner.Memory.Span; + } + /// /// Returns when this scratch has compatible dimensions and sufficient /// capacity for the requested parameters, making it safe to reuse without reallocation. @@ -1979,13 +2846,13 @@ public static WorkerScratch Create(MemoryAllocator allocator, int wordsPerRow, i /// Creates a context view over a compatible prefix of this scratch for the requested geometry width. /// /// The fill rule used when converting accumulated winding/coverage into final alpha. - /// The rasterization mode that controls how antialiasing thresholds are interpreted. - /// The threshold used when antialiasing is conditionally reduced or disabled. + /// The rasterization mode that selects continuous or centre-sampled coverage. + /// The retained line count, which is the exact maximum crossing count for one row. /// A backed by this scratch; the caller must not use two contexts over the same scratch concurrently. public Context CreateContext( IntersectionRule intersectionRule, RasterizationMode rasterizationMode, - float antialiasThreshold) + int lineCount) => new( this.bitVectorsOwner.Memory.Span, this.coverAreaOwner.Memory.Span, @@ -1995,9 +2862,14 @@ public Context CreateContext( this.rowHasBitsOwner.Memory.Span, this.rowTouchedOwner.Memory.Span, this.touchedRowsOwner.Memory.Span, + rasterizationMode == RasterizationMode.Aliased && lineCount > 0 ? this.GetCrossings(lineCount) : default, + rasterizationMode == RasterizationMode.Aliased ? this.CrossingCounts : default, + rasterizationMode == RasterizationMode.Aliased ? this.ColumnCrossings : default, + rasterizationMode == RasterizationMode.Aliased ? this.ColumnCrossingCounts : default, + rasterizationMode == RasterizationMode.Aliased ? this.TouchedColumns : default, + rasterizationMode == RasterizationMode.Aliased ? lineCount : 0, intersectionRule, - rasterizationMode, - antialiasThreshold); + rasterizationMode); /// /// Releases worker-local scratch buffers back to the allocator. @@ -2014,6 +2886,8 @@ public void Dispose() this.touchedRowsOwner.Dispose(); this.scanlineOwner.Dispose(); this.strokeBandCoverageOwner?.Dispose(); + this.crossingsOwner?.Dispose(); + this.aliasedScratchOwner?.Dispose(); } } } diff --git a/src/ImageSharp.Drawing/Processing/Backends/FlushScene.cs b/src/ImageSharp.Drawing/Processing/Backends/FlushScene.cs index a0ac19f38..04218f1bd 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/FlushScene.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/FlushScene.cs @@ -31,7 +31,8 @@ internal sealed partial class FlushScene : IDisposable controlItems: [], hasApply: false, rows: [], - segments: []); + segments: [], + profileStorages: []); /// /// Initializes a new instance of the class. @@ -51,6 +52,7 @@ internal sealed partial class FlushScene : IDisposable /// Whether the scene contains apply barriers. /// The retained row lists. /// The retained target-wide execution segments for scenes containing apply barriers. + /// The partition-owned aliased profile arenas. private FlushScene( int fillItemCount, int strokeItemCount, @@ -66,7 +68,8 @@ private FlushScene( SceneControlItem?[] controlItems, bool hasApply, SceneRow[] rows, - SceneSegment[] segments) + SceneSegment[] segments, + LinearGeometryProfileStorage[] profileStorages) { this.FillItemCount = fillItemCount; this.StrokeItemCount = strokeItemCount; @@ -83,6 +86,7 @@ private FlushScene( this.HasApply = hasApply; this.Rows = rows; this.Segments = segments; + this.ProfileStorages = profileStorages; } /// @@ -140,6 +144,11 @@ private FlushScene( /// public SceneSegment[] Segments { get; } + /// + /// Gets the partition-owned profile arenas retained for the scene lifetime. + /// + private LinearGeometryProfileStorage[] ProfileStorages { get; } + /// /// Gets the total number of row items retained by the scene. /// @@ -204,6 +213,7 @@ public static FlushScene Create( SceneControlItem?[] controlItems = scene.HasApply ? new SceneControlItem?[commandCount] : []; int partitionCount = ParallelExecutionHelper.GetPartitionCount(maxDegreeOfParallelism, commandCount, targetRowCount); PartitionState[] partitions = new PartitionState[partitionCount]; + LinearGeometryProfileStorage[] profileStorages = new LinearGeometryProfileStorage[partitionCount]; // The ordered begin/end-clip command stream is the single source of truth for clipping. // A single sequential prescan resolves the clip scopes open at each partition boundary so @@ -230,7 +240,7 @@ public static FlushScene Create( int partitionCommandStart = (partitionIndex * commandCount) / partitionCount; int partitionCommandEnd = ((partitionIndex + 1) * commandCount) / partitionCount; - partitions[partitionIndex] = ProcessPartition( + PartitionState partition = ProcessPartition( scene.Commands, partitionCommandStart, partitionCommandEnd, @@ -243,6 +253,9 @@ public static FlushScene Create( strokeItems, layers, controlItems); + + partitions[partitionIndex] = partition; + profileStorages[partitionIndex] = partition.ProfileStorage; }); int fillItemCount = 0; @@ -296,6 +309,7 @@ public static FlushScene Create( // geometry and clip state; the empty singleton carries nothing, so release them here. DisposeRetainedItems(fillItems, strokeItems, controlItems); DisposeRows(rowBuilders); + DisposeProfileStorages(profileStorages); return Empty(); } @@ -343,7 +357,8 @@ public static FlushScene Create( controlItems, scene.HasApply, sceneRows, - segments); + segments, + profileStorages); } catch { @@ -375,6 +390,7 @@ public static FlushScene Create( } DisposeRetainedItems(fillItems, strokeItems, segments.Length == 0 ? controlItems : []); + DisposeProfileStorages(profileStorages); throw; } } @@ -402,6 +418,19 @@ public void Dispose() } DisposeRetainedItems(this.FillItems, this.StrokeItems, this.ControlItems); + DisposeProfileStorages(this.ProfileStorages); + } + + /// + /// Releases all initialized partition profile arenas. + /// + /// The profile arenas to release. + private static void DisposeProfileStorages(LinearGeometryProfileStorage[] profileStorages) + { + for (int i = 0; i < profileStorages.Length; i++) + { + profileStorages[i]?.Dispose(); + } } /// @@ -904,112 +933,126 @@ private static PartitionState ProcessPartition( int currentLayerDepth = 0; int maxLayerDepth = 0; ClipStreamTracker clipTracker = ClipStreamTracker.CreateFromSeed(clipSeed); + LinearGeometryProfileStorage profileStorage = new(allocator); - for (int commandIndex = commandStart; commandIndex < commandEnd; commandIndex++) + try { - CompositionSceneCommand command = commands[commandIndex]; - if (command is PathCompositionSceneCommand pathCommand) + for (int commandIndex = commandStart; commandIndex < commandEnd; commandIndex++) { - CompositionCommand composition = pathCommand.Command; + CompositionSceneCommand command = commands[commandIndex]; + if (command is PathCompositionSceneCommand pathCommand) + { + CompositionCommand composition = pathCommand.Command; - // Clip commands only mutate the tracker; they retain no scene items. Every draw - // command that follows captures the composed clip state and anchor from the tracker, - // keeping the ordered clip stream the single source of truth for clipping. - if (composition.Kind == CompositionCommandKind.BeginClip) + // Clip commands only mutate the tracker; they retain no scene items. Every draw + // command that follows captures the composed clip state and anchor from the tracker, + // keeping the ordered clip stream the single source of truth for clipping. + if (composition.Kind == CompositionCommandKind.BeginClip) + { + clipTracker.Push(composition.ClipDescriptor, composition.DestinationOffset); + continue; + } + + if (composition.Kind == CompositionCommandKind.EndClip) + { + clipTracker.Pop(); + continue; + } + + ProcessPathCommand( + composition, + commandIndex, + clipTracker.Current, + clipTracker.Anchor, + targetBounds, + firstTargetRowBandIndex, + rowBuilders, + allocator, + profileStorage, + fillItems, + strokeItems, + layers, + controlItems, + ref fillItemCount, + ref strokeItemCount, + ref totalEdgeCount, + ref singleBandItemCount, + ref smallEdgeItemCount, + ref currentLayerDepth, + ref maxLayerDepth); + } + else if (command is StrokePathCompositionSceneCommand strokePathCommand) { - clipTracker.Push(composition.ClipDescriptor, composition.DestinationOffset); - continue; + ProcessStrokePathCommand( + strokePathCommand.Command, + commandIndex, + clipTracker.Current, + clipTracker.Anchor, + targetRowCount, + firstTargetRowBandIndex, + rowBuilders, + allocator, + profileStorage, + strokeItems, + ref strokeItemCount, + ref totalEdgeCount, + ref singleBandItemCount, + ref smallEdgeItemCount); } - - if (composition.Kind == CompositionCommandKind.EndClip) + else if (command is LineSegmentCompositionSceneCommand lineSegmentCommand) { - clipTracker.Pop(); - continue; + ProcessLineSegmentCommand( + lineSegmentCommand.Command, + commandIndex, + clipTracker.Current, + clipTracker.Anchor, + targetRowCount, + firstTargetRowBandIndex, + rowBuilders, + allocator, + profileStorage, + strokeItems, + ref strokeItemCount, + ref totalEdgeCount, + ref singleBandItemCount, + ref smallEdgeItemCount); + } + else if (command is PolylineCompositionSceneCommand polylineCommand) + { + ProcessPolylineCommand( + polylineCommand.Command, + commandIndex, + clipTracker.Current, + clipTracker.Anchor, + targetRowCount, + firstTargetRowBandIndex, + rowBuilders, + allocator, + profileStorage, + strokeItems, + ref strokeItemCount, + ref totalEdgeCount, + ref singleBandItemCount, + ref smallEdgeItemCount); } - - ProcessPathCommand( - composition, - commandIndex, - clipTracker.Current, - clipTracker.Anchor, - targetBounds, - firstTargetRowBandIndex, - rowBuilders, - allocator, - fillItems, - strokeItems, - layers, - controlItems, - ref fillItemCount, - ref strokeItemCount, - ref totalEdgeCount, - ref singleBandItemCount, - ref smallEdgeItemCount, - ref currentLayerDepth, - ref maxLayerDepth); - } - else if (command is StrokePathCompositionSceneCommand strokePathCommand) - { - ProcessStrokePathCommand( - strokePathCommand.Command, - commandIndex, - clipTracker.Current, - clipTracker.Anchor, - targetRowCount, - firstTargetRowBandIndex, - rowBuilders, - allocator, - strokeItems, - ref strokeItemCount, - ref totalEdgeCount, - ref singleBandItemCount, - ref smallEdgeItemCount); - } - else if (command is LineSegmentCompositionSceneCommand lineSegmentCommand) - { - ProcessLineSegmentCommand( - lineSegmentCommand.Command, - commandIndex, - clipTracker.Current, - clipTracker.Anchor, - targetRowCount, - firstTargetRowBandIndex, - rowBuilders, - allocator, - strokeItems, - ref strokeItemCount, - ref totalEdgeCount, - ref singleBandItemCount, - ref smallEdgeItemCount); - } - else if (command is PolylineCompositionSceneCommand polylineCommand) - { - ProcessPolylineCommand( - polylineCommand.Command, - commandIndex, - clipTracker.Current, - clipTracker.Anchor, - targetRowCount, - firstTargetRowBandIndex, - rowBuilders, - allocator, - strokeItems, - ref strokeItemCount, - ref totalEdgeCount, - ref singleBandItemCount, - ref smallEdgeItemCount); } - } - return new PartitionState( - fillItemCount, - strokeItemCount, - totalEdgeCount, - singleBandItemCount, - smallEdgeItemCount, - currentLayerDepth, - maxLayerDepth, - rowBuilders); + return new PartitionState( + fillItemCount, + strokeItemCount, + totalEdgeCount, + singleBandItemCount, + smallEdgeItemCount, + currentLayerDepth, + maxLayerDepth, + rowBuilders, + profileStorage); + } + catch + { + profileStorage.Dispose(); + throw; + } } /// @@ -1024,6 +1067,7 @@ private static PartitionState ProcessPartition( /// The first row-band index covered by the target. /// The partition-owned row builders, one slot per target row band. /// The allocator used for retained row storage. + /// The partition-owned aliased profile arena. /// The shared retained fill items indexed by original command index. /// The shared retained stroke items indexed by original command index. /// The shared retained layer state indexed by begin-layer command index. @@ -1044,6 +1088,7 @@ private static void ProcessPathCommand( int firstTargetRowBandIndex, RowBuilder[] rowBuilders, MemoryAllocator allocator, + LinearGeometryProfileStorage profileStorage, FillSceneItem?[] fillItems, StrokeSceneItem?[] strokeItems, DrawingCanvasLayer?[] layers, @@ -1074,6 +1119,7 @@ private static void ProcessPathCommand( command.DestinationOffset, command.SubPixelOffset, allocator, + profileStorage, out PreparedFillItem preparedApply)) { return; @@ -1084,6 +1130,7 @@ private static void ProcessPathCommand( command.TargetBounds, clipAnchor, allocator, + profileStorage, out PreparedPathClipState? preparedApplyPathClips)) { preparedApply.Rasterizable.Dispose(); @@ -1152,7 +1199,7 @@ private static void ProcessPathCommand( return; } - if (!TryPrepareFillPath(command, allocator, out PreparedFillItem preparedFill) || + if (!TryPrepareFillPath(command, allocator, profileStorage, out PreparedFillItem preparedFill) || preparedFill.Rasterizable.RowBandCount == 0) { return; @@ -1163,6 +1210,7 @@ private static void ProcessPathCommand( command.TargetBounds, clipAnchor, allocator, + profileStorage, out PreparedPathClipState? preparedPathClips)) { preparedFill.Rasterizable.Dispose(); @@ -1196,6 +1244,7 @@ private static void ProcessPathCommand( /// The first row-band index covered by the target. /// The partition-owned row builders, one slot per target row band. /// The allocator used for retained row storage. + /// The partition-owned aliased profile arena. /// The shared retained stroke items indexed by original command index. /// The running partition stroke item count. /// The running total of encoded raster edges. @@ -1210,6 +1259,7 @@ private static void ProcessStrokePathCommand( int firstTargetRowBandIndex, RowBuilder[] rowBuilders, MemoryAllocator allocator, + LinearGeometryProfileStorage profileStorage, StrokeSceneItem?[] strokeItems, ref int strokeItemCount, ref long totalEdgeCount, @@ -1227,6 +1277,7 @@ private static void ProcessStrokePathCommand( command.TargetBounds, clipAnchor, allocator, + profileStorage, out PreparedPathClipState? preparedPathClips)) { preparedStroke.Rasterizable.Dispose(); @@ -1260,6 +1311,7 @@ private static void ProcessStrokePathCommand( /// The first row-band index covered by the target. /// The partition-owned row builders, one slot per target row band. /// The allocator used for retained row storage. + /// The partition-owned aliased profile arena. /// The shared retained stroke items indexed by original command index. /// The running partition stroke item count. /// The running total of encoded raster edges. @@ -1274,6 +1326,7 @@ private static void ProcessLineSegmentCommand( int firstTargetRowBandIndex, RowBuilder[] rowBuilders, MemoryAllocator allocator, + LinearGeometryProfileStorage profileStorage, StrokeSceneItem?[] strokeItems, ref int strokeItemCount, ref long totalEdgeCount, @@ -1291,6 +1344,7 @@ private static void ProcessLineSegmentCommand( command.TargetBounds, clipAnchor, allocator, + profileStorage, out PreparedPathClipState? preparedPathClips)) { preparedStroke.Rasterizable.Dispose(); @@ -1324,6 +1378,7 @@ private static void ProcessLineSegmentCommand( /// The first row-band index covered by the target. /// The partition-owned row builders, one slot per target row band. /// The allocator used for retained row storage. + /// The partition-owned aliased profile arena. /// The shared retained stroke items indexed by original command index. /// The running partition stroke item count. /// The running total of encoded raster edges. @@ -1338,6 +1393,7 @@ private static void ProcessPolylineCommand( int firstTargetRowBandIndex, RowBuilder[] rowBuilders, MemoryAllocator allocator, + LinearGeometryProfileStorage profileStorage, StrokeSceneItem?[] strokeItems, ref int strokeItemCount, ref long totalEdgeCount, @@ -1355,6 +1411,7 @@ private static void ProcessPolylineCommand( command.TargetBounds, clipAnchor, allocator, + profileStorage, out PreparedPathClipState? preparedPathClips)) { preparedStroke.Rasterizable.Dispose(); @@ -1418,11 +1475,13 @@ private static void AppendLayerOperations( /// /// The command supplying the path, brush, and options. /// The allocator used for retained raster storage. + /// The partition-owned aliased profile arena. /// The prepared fill payload when successful. /// when the fill intersects the target and rasterizable geometry was created; otherwise . private static bool TryPrepareFillPath( in CompositionCommand command, MemoryAllocator allocator, + LinearGeometryProfileStorage profileStorage, out PreparedFillItem prepared) => TryPrepareFillPath( command.SourcePath, @@ -1433,6 +1492,7 @@ private static bool TryPrepareFillPath( command.DestinationOffset, command.SubPixelOffset, allocator, + profileStorage, out prepared); /// @@ -1448,6 +1508,7 @@ private static bool TryPrepareFillPath( /// The offset from path-local to destination coordinates. /// The fractional translation applied after the drawing options transform. /// The allocator used for retained raster storage. + /// The partition-owned aliased profile arena. /// The prepared fill payload when successful. /// when the fill intersects the target and rasterizable geometry was created; otherwise . internal static bool TryPrepareFillPath( @@ -1459,6 +1520,7 @@ internal static bool TryPrepareFillPath( Point destinationOffset, Vector2 subPixelOffset, MemoryAllocator allocator, + LinearGeometryProfileStorage profileStorage, out PreparedFillItem prepared) { Matrix4x4 transform = drawingOptions.Transform; @@ -1497,7 +1559,8 @@ internal static bool TryPrepareFillPath( destinationOffset.X, destinationOffset.Y, rasterizerOptions, - allocator); + allocator, + profileStorage); if (rasterizable is null) { @@ -1732,7 +1795,6 @@ private static bool TryResolveRasterization( clippedDestination, options.IntersectionRule, options.RasterizationMode, - options.AntialiasThreshold, options.CoverageBoost); brushBounds = absoluteInterest; @@ -1905,6 +1967,7 @@ private readonly struct PartitionState /// The net begin minus end layer count across the partition. /// The maximum layer depth reached, relative to the partition start. /// The partition-owned row builders, one slot per target row band. + /// The partition-owned aliased profile arena. public PartitionState( int fillItemCount, int strokeItemCount, @@ -1913,7 +1976,8 @@ public PartitionState( int smallEdgeItemCount, int layerDepthDelta, int maxLayerDepth, - RowBuilder[] rowBuilders) + RowBuilder[] rowBuilders, + LinearGeometryProfileStorage profileStorage) { this.FillItemCount = fillItemCount; this.StrokeItemCount = strokeItemCount; @@ -1923,6 +1987,7 @@ public PartitionState( this.LayerDepthDelta = layerDepthDelta; this.MaxLayerDepth = maxLayerDepth; this.RowBuilders = rowBuilders; + this.ProfileStorage = profileStorage; } /// @@ -1964,6 +2029,11 @@ public PartitionState( /// Gets the partition-owned row builders, one slot per target row band. /// public RowBuilder[] RowBuilders { get; } + + /// + /// Gets the aliased profile arena built by this partition. + /// + public LinearGeometryProfileStorage ProfileStorage { get; } } /// diff --git a/src/ImageSharp.Drawing/Processing/Backends/PreparedPathClipState.cs b/src/ImageSharp.Drawing/Processing/Backends/PreparedPathClipState.cs index 8f7422f8a..9a0e14e3b 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/PreparedPathClipState.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/PreparedPathClipState.cs @@ -35,6 +35,7 @@ private PreparedPathClipState(DefaultRasterizer.RasterizableGeometry?[] rasteriz /// The absolute command target bounds. /// The destination offset used to place clip descriptors. /// The allocator used for retained raster storage. + /// The partition-owned aliased profile arena. /// The prepared path clip state, or when the clip state has no path clips. /// when the command can still draw after preparing clips; otherwise . public static bool TryCreate( @@ -42,6 +43,7 @@ public static bool TryCreate( Rectangle targetBounds, Point destinationOffset, MemoryAllocator allocator, + LinearGeometryProfileStorage profileStorage, out PreparedPathClipState? prepared) { prepared = null; @@ -79,8 +81,7 @@ public static bool TryCreate( RasterizerOptions options = new( targetBounds, descriptor.IntersectionRule, - mode, - descriptor.AntialiasThreshold); + mode); DefaultRasterizer.RasterizableGeometry? rasterizable = DefaultRasterizer.CreateRasterizableGeometry( geometry, @@ -88,7 +89,8 @@ public static bool TryCreate( destinationOffset.X, destinationOffset.Y, options, - allocator); + allocator, + profileStorage); // An empty intersecting path clip makes the whole command empty. An empty // difference clip is a no-op, so it remains represented by a null rasterizable. diff --git a/src/ImageSharp.Drawing/Processing/Backends/RasterizerOptions.cs b/src/ImageSharp.Drawing/Processing/Backends/RasterizerOptions.cs index 33667b284..d869bcf27 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/RasterizerOptions.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/RasterizerOptions.cs @@ -30,19 +30,16 @@ public readonly struct RasterizerOptions /// Destination bounds to rasterize into. /// Polygon intersection rule. /// Rasterization coverage mode. - /// Coverage threshold for aliased mode (0 to 1). /// Perceptual coverage boost for antialiased mode (0 disables). public RasterizerOptions( Rectangle interest, IntersectionRule intersectionRule, RasterizationMode rasterizationMode, - float antialiasThreshold, float coverageBoost = 0F) { this.Interest = interest; this.IntersectionRule = intersectionRule; this.RasterizationMode = rasterizationMode; - this.AntialiasThreshold = antialiasThreshold; this.CoverageBoost = coverageBoost; } @@ -61,12 +58,6 @@ public RasterizerOptions( /// public RasterizationMode RasterizationMode { get; } - /// - /// Gets the coverage threshold used when is . - /// Pixels whose coverage is greater than or equal to this value are rendered as fully opaque; pixels below it are discarded. - /// - public float AntialiasThreshold { get; } - /// /// Gets the perceptual coverage boost used when is /// . Partial coverage values are remapped by @@ -83,5 +74,5 @@ public RasterizerOptions( /// The replacement interest rectangle. /// A new value. public RasterizerOptions WithInterest(Rectangle interest) - => new(interest, this.IntersectionRule, this.RasterizationMode, this.AntialiasThreshold, this.CoverageBoost); + => new(interest, this.IntersectionRule, this.RasterizationMode, this.CoverageBoost); } diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index 962363f91..52c45b38f 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -247,8 +247,7 @@ private DrawingCanvas( DrawingClipState.FromPaths( clipPaths, ClipOperation.Intersection, - options.GraphicsOptions.Antialias ? DrawingClipEdgeMode.Antialiased : DrawingClipEdgeMode.Hard, - options.GraphicsOptions.AntialiasThreshold), + options.GraphicsOptions.Antialias ? DrawingClipEdgeMode.Antialiased : DrawingClipEdgeMode.Hard), targetFrame.Bounds, targetFrame.Bounds.Location), true, @@ -617,8 +616,7 @@ public override void Clip(ClipOperation operation, params IPath[] clipPaths) clipPaths, transform, operation, - state.Options.GraphicsOptions.Antialias ? DrawingClipEdgeMode.Antialiased : DrawingClipEdgeMode.Hard, - state.Options.GraphicsOptions.AntialiasThreshold); + state.Options.GraphicsOptions.Antialias ? DrawingClipEdgeMode.Antialiased : DrawingClipEdgeMode.Hard); this.AppendBeginClips(incomingState, state.DestinationOffset); @@ -1437,8 +1435,7 @@ private void PrepareCompositionCore( RasterizerOptions rasterizerOptions = new( interest, options.IntersectionRule, - rasterizationMode, - graphicsOptions.AntialiasThreshold); + rasterizationMode); DrawingCanvasState state = this.ResolveState(); @@ -1504,8 +1501,7 @@ private void PrepareStrokeLineSegmentCompositionCore( RasterizerOptions rasterizerOptions = new( interest, options.IntersectionRule, - rasterizationMode, - graphicsOptions.AntialiasThreshold); + rasterizationMode); DrawingCanvasState state = this.ResolveState(); this.batcher.EnsureClipAnchors(state.DestinationOffset); @@ -1550,8 +1546,7 @@ private void PrepareStrokePolylineCompositionCore( RasterizerOptions rasterizerOptions = new( interest, options.IntersectionRule, - rasterizationMode, - graphicsOptions.AntialiasThreshold); + rasterizationMode); DrawingCanvasState state = this.ResolveState(); this.batcher.EnsureClipAnchors(state.DestinationOffset); @@ -2209,16 +2204,14 @@ private CompositionSceneCommand CreateTextCompositionCommand(DrawingOperation op Rectangle interest = ToRasterizerInterest(bounds); // Text opts into the perceptual coverage boost here; generic vector fills and strokes - // never carry one. The boost only applies to antialiased coverage. - float coverageBoost = rasterizationMode == RasterizationMode.Antialiased - ? Math.Clamp(drawingOptions.TextContrast, 0F, 1F) - : 0F; + // never carry one. Only antialiased coverage uses this remap. Aliased coverage comes + // from the independent centre-sampled scan conversion. + float coverageBoost = Math.Clamp(drawingOptions.TextContrast, 0F, 1F); RasterizerOptions rasterizerOptions = new( interest, intersectionRule, rasterizationMode, - graphicsOptions.AntialiasThreshold, coverageBoost); // Glyph paths arrive pre-laid-out, so the queued command carries identity-transform @@ -2492,20 +2485,17 @@ private static RectangleF MapSourceClipToDestination( /// Canvas transform to apply to the clip state. /// The operation used to combine the paths with the existing clip. /// The clip edge mode. - /// The coverage threshold used for hard clip edges. /// The transformed clip state. private static DrawingClipState CreateClipState( IPath[] clipPaths, Matrix4x4 transform, ClipOperation operation, - DrawingClipEdgeMode edgeMode, - float antialiasThreshold) + DrawingClipEdgeMode edgeMode) { DrawingClipState clipState = DrawingClipState.FromPaths( clipPaths, operation, - edgeMode, - antialiasThreshold); + edgeMode); // Transform after descriptor creation. DrawingClipDescriptor.Transform has the // rectangle/region-specific logic needed to preserve cheap clip primitives. diff --git a/src/ImageSharp.Drawing/Processing/DrawingClipDescriptor.cs b/src/ImageSharp.Drawing/Processing/DrawingClipDescriptor.cs index acbd76c7a..3a1fe607a 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingClipDescriptor.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingClipDescriptor.cs @@ -35,7 +35,6 @@ public sealed class DrawingClipDescriptor /// The operation used to combine this descriptor with the existing clip. /// The fill rule used for path geometry. /// The clip edge mode. - /// The coverage threshold used for hard clip edges. private DrawingClipDescriptor( DrawingClipKind kind, RectangleF rectangle, @@ -46,8 +45,7 @@ private DrawingClipDescriptor( Matrix4x4 pathTransform, ClipOperation operation, IntersectionRule intersectionRule, - DrawingClipEdgeMode edgeMode, - float antialiasThreshold) + DrawingClipEdgeMode edgeMode) { this.Kind = kind; this.Rectangle = rectangle; @@ -59,7 +57,6 @@ private DrawingClipDescriptor( this.Operation = operation; this.IntersectionRule = intersectionRule; this.EdgeMode = edgeMode; - this.AntialiasThreshold = antialiasThreshold; } /// @@ -82,11 +79,6 @@ private DrawingClipDescriptor( /// public DrawingClipEdgeMode EdgeMode { get; } - /// - /// Gets the coverage threshold used when the clip edge mode is hard. - /// - public float AntialiasThreshold { get; } - /// /// Gets the rectangle for a rectangle clip. /// @@ -123,13 +115,11 @@ private DrawingClipDescriptor( /// The rectangle in canvas-local coordinates. /// The operation used to combine the rectangle with the existing clip. /// The clip edge mode. - /// The coverage threshold used for hard clip edges. /// The descriptor. public static DrawingClipDescriptor CreateRectangle( RectangleF rectangle, ClipOperation operation, - DrawingClipEdgeMode edgeMode, - float antialiasThreshold) + DrawingClipEdgeMode edgeMode) => new( DrawingClipKind.Rectangle, rectangle, @@ -140,20 +130,17 @@ public static DrawingClipDescriptor CreateRectangle( Matrix4x4.Identity, operation, IntersectionRule.NonZero, - edgeMode, - antialiasThreshold); + edgeMode); /// /// Creates an integer region clip descriptor. /// /// The region rectangles in canvas-local coordinates. /// The operation used to combine the region with the existing clip. - /// The coverage threshold used for hard clip edges. /// The descriptor. public static DrawingClipDescriptor CreateIntegerRegion( IReadOnlyList rectangles, - ClipOperation operation, - float antialiasThreshold) + ClipOperation operation) => new( DrawingClipKind.IntegerRegion, default, @@ -164,8 +151,7 @@ public static DrawingClipDescriptor CreateIntegerRegion( Matrix4x4.Identity, operation, IntersectionRule.NonZero, - DrawingClipEdgeMode.Hard, - antialiasThreshold); + DrawingClipEdgeMode.Hard); /// /// Creates a floating-point region clip descriptor. @@ -173,13 +159,11 @@ public static DrawingClipDescriptor CreateIntegerRegion( /// The region rectangles in canvas-local coordinates. /// The operation used to combine the region with the existing clip. /// The clip edge mode. - /// The coverage threshold used for hard clip edges. /// The descriptor. public static DrawingClipDescriptor CreateRegion( IReadOnlyList rectangles, ClipOperation operation, - DrawingClipEdgeMode edgeMode, - float antialiasThreshold) + DrawingClipEdgeMode edgeMode) => new( DrawingClipKind.Region, default, @@ -190,8 +174,7 @@ public static DrawingClipDescriptor CreateRegion( Matrix4x4.Identity, operation, IntersectionRule.NonZero, - edgeMode, - antialiasThreshold); + edgeMode); /// /// Creates a path clip descriptor. @@ -199,14 +182,12 @@ public static DrawingClipDescriptor CreateRegion( /// The path operands interpreted as one clip region. /// The operation used to combine the paths with the existing clip. /// The clip edge mode. - /// The coverage threshold used for hard clip edges. /// The descriptor. public static DrawingClipDescriptor CreatePath( IReadOnlyList paths, ClipOperation operation, - DrawingClipEdgeMode edgeMode, - float antialiasThreshold) - => CreatePath(paths, Vector2.One, Matrix4x4.Identity, operation, IntersectionRule.NonZero, edgeMode, antialiasThreshold); + DrawingClipEdgeMode edgeMode) + => CreatePath(paths, Vector2.One, Matrix4x4.Identity, operation, IntersectionRule.NonZero, edgeMode); /// /// Creates a path descriptor from operands plus the transform split used by path lowering. @@ -217,7 +198,6 @@ public static DrawingClipDescriptor CreatePath( /// The operation used to combine the paths with the existing clip. /// The fill rule used for the path geometry. /// The clip edge mode. - /// The coverage threshold used for hard clip edges. /// The descriptor. private static DrawingClipDescriptor CreatePath( IReadOnlyList paths, @@ -225,8 +205,7 @@ private static DrawingClipDescriptor CreatePath( Matrix4x4 transform, ClipOperation operation, IntersectionRule intersectionRule, - DrawingClipEdgeMode edgeMode, - float antialiasThreshold) + DrawingClipEdgeMode edgeMode) => new( DrawingClipKind.Path, default, @@ -237,8 +216,7 @@ private static DrawingClipDescriptor CreatePath( transform, operation, intersectionRule, - edgeMode, - antialiasThreshold); + edgeMode); /// /// Converts one incoming path to a clip descriptor. @@ -246,25 +224,23 @@ private static DrawingClipDescriptor CreatePath( /// The incoming path. /// The operation used to combine the path with the existing clip. /// The clip edge mode. - /// The coverage threshold used for hard clip edges. /// The descriptor. public static DrawingClipDescriptor FromPath( IPath path, ClipOperation operation, - DrawingClipEdgeMode edgeMode, - float antialiasThreshold) + DrawingClipEdgeMode edgeMode) { if (path is IRegionPath regionPath) { - return CreateIntegerRegion(regionPath.Rectangles, operation, antialiasThreshold); + return CreateIntegerRegion(regionPath.Rectangles, operation); } if (TryGetRectangle(path, out RectangleF rectangle)) { - return CreateRectangle(rectangle, operation, edgeMode, antialiasThreshold); + return CreateRectangle(rectangle, operation, edgeMode); } - return CreatePath([path], operation, edgeMode, antialiasThreshold); + return CreatePath([path], operation, edgeMode); } /// @@ -285,7 +261,7 @@ public DrawingClipDescriptor Translate(Vector2 offset) { RectangleF translated = this.Rectangle; translated.Offset(offset.X, offset.Y); - return CreateRectangle(translated, this.Operation, this.EdgeMode, this.AntialiasThreshold); + return CreateRectangle(translated, this.Operation, this.EdgeMode); } case DrawingClipKind.IntegerRegion: @@ -301,7 +277,7 @@ public DrawingClipDescriptor Translate(Vector2 offset) translated[i].Offset(pointOffset); } - return CreateIntegerRegion(translated, this.Operation, this.AntialiasThreshold); + return CreateIntegerRegion(translated, this.Operation); } RectangleF[] translatedRegion = new RectangleF[source.Count]; @@ -311,7 +287,7 @@ public DrawingClipDescriptor Translate(Vector2 offset) translatedRegion[i] = new RectangleF(rectangle.X + offset.X, rectangle.Y + offset.Y, rectangle.Width, rectangle.Height); } - return CreateRegion(translatedRegion, this.Operation, DrawingClipEdgeMode.Hard, this.AntialiasThreshold); + return CreateRegion(translatedRegion, this.Operation, DrawingClipEdgeMode.Hard); } case DrawingClipKind.Region: @@ -324,7 +300,7 @@ public DrawingClipDescriptor Translate(Vector2 offset) translated[i].Offset(offset.X, offset.Y); } - return CreateRegion(translated, this.Operation, this.EdgeMode, this.AntialiasThreshold); + return CreateRegion(translated, this.Operation, this.EdgeMode); } default: @@ -336,7 +312,7 @@ public DrawingClipDescriptor Translate(Vector2 offset) Vector2 scale = MatrixUtilities.GetScale(translated); Matrix4x4 residual = MatrixUtilities.GetResidual(scale, translated); - return CreatePath(this.Paths, scale, residual, this.Operation, this.IntersectionRule, this.EdgeMode, this.AntialiasThreshold); + return CreatePath(this.Paths, scale, residual, this.Operation, this.IntersectionRule, this.EdgeMode); } } } @@ -357,7 +333,7 @@ public DrawingClipDescriptor Transform(Matrix4x4 matrix) { if (this.Kind == DrawingClipKind.Rectangle) { - return CreateRectangle(TransformRectangle(this.Rectangle, matrix), this.Operation, this.EdgeMode, this.AntialiasThreshold); + return CreateRectangle(TransformRectangle(this.Rectangle, matrix), this.Operation, this.EdgeMode); } if (this.Kind == DrawingClipKind.IntegerRegion) @@ -373,7 +349,7 @@ public DrawingClipDescriptor Transform(Matrix4x4 matrix) } SortRegionRectangles(transformed); - return CreateRegion(transformed, this.Operation, DrawingClipEdgeMode.Hard, this.AntialiasThreshold); + return CreateRegion(transformed, this.Operation, DrawingClipEdgeMode.Hard); } if (this.Kind == DrawingClipKind.Region) @@ -386,7 +362,7 @@ public DrawingClipDescriptor Transform(Matrix4x4 matrix) } SortRegionRectangles(transformed); - return CreateRegion(transformed, this.Operation, this.EdgeMode, this.AntialiasThreshold); + return CreateRegion(transformed, this.Operation, this.EdgeMode); } } @@ -397,7 +373,7 @@ public DrawingClipDescriptor Transform(Matrix4x4 matrix) Matrix4x4 residual = MatrixUtilities.GetResidual(scale, pathMatrix); IReadOnlyList sourcePaths = this.Kind == DrawingClipKind.Path ? this.Paths : [this.ToPath()]; - return CreatePath(sourcePaths, scale, residual, this.Operation, this.IntersectionRule, this.EdgeMode, this.AntialiasThreshold); + return CreatePath(sourcePaths, scale, residual, this.Operation, this.IntersectionRule, this.EdgeMode); } /// diff --git a/src/ImageSharp.Drawing/Processing/DrawingClipState.cs b/src/ImageSharp.Drawing/Processing/DrawingClipState.cs index d35d72492..b33567955 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingClipState.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingClipState.cs @@ -15,8 +15,7 @@ public readonly struct DrawingClipState private static readonly DrawingClipDescriptor EmptyDescriptor = DrawingClipDescriptor.CreateRectangle( default, ClipOperation.Intersection, - DrawingClipEdgeMode.Hard, - 0F); + DrawingClipEdgeMode.Hard); // Most clip stacks in UI redraws are shallow: a dirty region plus one or two control clips. // Keep that path allocation-free and only spill to an array for unusually deep nesting. @@ -127,13 +126,11 @@ private DrawingClipState(DrawingClipDescriptor[] overflowDescriptors) /// The clip paths. /// The operation used to combine the paths with the existing clip. /// The clip edge mode. - /// The coverage threshold used for hard clip edges. /// The normalized clip state. public static DrawingClipState FromPaths( IReadOnlyList paths, ClipOperation operation, - DrawingClipEdgeMode edgeMode, - float antialiasThreshold) + DrawingClipEdgeMode edgeMode) { if (paths.Count == 0) { @@ -141,8 +138,8 @@ public static DrawingClipState FromPaths( } DrawingClipDescriptor descriptor = paths.Count == 1 - ? DrawingClipDescriptor.FromPath(paths[0], operation, edgeMode, antialiasThreshold) - : DrawingClipDescriptor.CreatePath(paths, operation, edgeMode, antialiasThreshold); + ? DrawingClipDescriptor.FromPath(paths[0], operation, edgeMode) + : DrawingClipDescriptor.CreatePath(paths, operation, edgeMode); // Multiple paths supplied in one Clip call are one clip operand. They are not separate // stack entries because the operation applies to the combined incoming shape. diff --git a/src/ImageSharp.Drawing/Processing/DrawingOptions.cs b/src/ImageSharp.Drawing/Processing/DrawingOptions.cs index 2212aa21f..7cb149d23 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingOptions.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingOptions.cs @@ -49,8 +49,8 @@ internal DrawingOptions( } /// - /// Gets or sets the graphics rendering options that control antialiasing, blending, alpha composition, - /// and coverage thresholding for the drawing operation. + /// Gets or sets the graphics rendering options that control antialiasing, blending, and alpha composition + /// for the drawing operation. /// public GraphicsOptions GraphicsOptions { diff --git a/src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.cs b/src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.cs index ed7ff87c5..e3422f506 100644 --- a/src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.cs +++ b/src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.cs @@ -7,7 +7,6 @@ using SixLabors.Fonts.Rendering; using SixLabors.Fonts.Unicode; using SixLabors.ImageSharp.Drawing.Helpers; -using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Text; namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Text; @@ -99,9 +98,11 @@ internal sealed partial class RichTextGlyphRenderer : BaseGlyphBuilder // for repeated characters. // // Position is intentionally absent from the key: cached paths are anchored at their exact - // outline origin, and each emitted operation carries the pixel-snapped location plus the + // outline origin, and each emitted operation carries the integer location plus the // fractional remainder (DrawingOperation.SubPixelOffset). The backends apply the remainder - // as a residual translation, so one cache entry renders exactly at every position. + // as a residual translation. Fonts supplies metric bounds at the same resolved origin used + // for outline emission, including format-specific grid placement, so BoundsOffset maps every + // cache hit back to the exact position a fresh outline would have produced. // The key's size component stays quantized (1/SizeAccuracyMultiple px) because transformed // bounds sizes pick up float noise under translation. @@ -492,7 +493,7 @@ protected override void EndGlyph() // Otherwise, on a cache miss the built path is anchored at its exact outline origin, // stored for future hits, and emitted as fill and/or outline DrawingOperations. // On a cache hit the stored path is reused; this instance's own outline origin - // (snapped location + fractional remainder) positions it exactly. + // positions it exactly. if (this.hasLayer) { // The layer has already been rendered. @@ -573,13 +574,14 @@ protected override void EndGlyph() { // Cache hit: the base class skipped building the decoded outline, so derive the // position estimate from the anchor stored with the cached entry, exactly as the - // fast path does. The stored path is anchored at its exact origin, so the snapped - // estimate plus its fractional remainder positions it exactly. The entries were + // fast path does. The stored path is anchored at its exact origin, so the integer + // component plus its fractional remainder positions it exactly. The entries were // assigned by the hit in BeginGlyph. renderData = this.currentCacheEntries![this.cacheReadIndex++]; PointF estimatedPathLocation = new( this.currentTransformedBoundsLocation.X + renderData.BoundsOffset.X, this.currentTransformedBoundsLocation.Y + renderData.BoundsOffset.Y); + renderLocation = ClampToPixel(estimatedPathLocation); subPixelOffset = (Vector2)(estimatedPathLocation - renderLocation); @@ -640,11 +642,12 @@ private void EmitCachedGlyphOperations(GlyphRenderData renderData, PointF curren { // Estimate the outline bounds location using the stored offset between // the outline bounds and the font metric bounds from the original glyph. - // The cached path is anchored at its exact outline origin, so the snapped - // estimate plus its fractional remainder positions it exactly. + // The cached path is anchored at its exact outline origin, so the integer + // component plus its fractional remainder positions it exactly. PointF estimatedPathLocation = new( currentBoundsLocation.X + renderData.BoundsOffset.X, currentBoundsLocation.Y + renderData.BoundsOffset.Y); + Point renderLocation = ClampToPixel(estimatedPathLocation); Vector2 subPixelOffset = (Vector2)(estimatedPathLocation - renderLocation); @@ -707,10 +710,7 @@ private void EmitCachedGlyphOperations(GlyphRenderData renderData, PointF curren /// current key. Creates the cache list on first insertion for a given key. /// /// The render data to append to the current key's entry list. - private void UpdateCache(GlyphRenderData renderData) - { - this.glyphCache.GetOrAdd(this.currentCacheKey).Add(renderData); - } + private void UpdateCache(GlyphRenderData renderData) => this.glyphCache.GetOrAdd(this.currentCacheKey).Add(renderData); /// /// Truncates a floating-point position to the nearest whole pixel toward negative infinity. @@ -738,7 +738,7 @@ protected override void Dispose(bool disposing) /// /// Per-layer cached data for a rasterized glyph. Stores the path anchored at its exact - /// outline origin; cache hits position it via their own snapped location and fractional + /// outline origin; cache hits position it via their own integer location and fractional /// remainder, so no per-hit compensation state is required. /// internal struct GlyphRenderData diff --git a/tests/ImageSharp.Drawing.Tests/GraphicsOptionsTests.cs b/tests/ImageSharp.Drawing.Tests/GraphicsOptionsTests.cs index 685fe348c..77bd27e49 100644 --- a/tests/ImageSharp.Drawing.Tests/GraphicsOptionsTests.cs +++ b/tests/ImageSharp.Drawing.Tests/GraphicsOptionsTests.cs @@ -38,14 +38,6 @@ public void DefaultGraphicsOptionsColorBlendingMode() Assert.Equal(expected, this.cloneGraphicsOptions.ColorBlendingMode); } - [Fact] - public void DefaultGraphicsOptionsAntialiasThreshold() - { - const float expected = 0.5F; - Assert.Equal(expected, this.newGraphicsOptions.AntialiasThreshold); - Assert.Equal(expected, this.cloneGraphicsOptions.AntialiasThreshold); - } - [Fact] public void DefaultGraphicsOptionsAlphaCompositionMode() { @@ -61,7 +53,6 @@ public void NonDefaultClone() { AlphaCompositionMode = PixelAlphaCompositionMode.DestAtop, Antialias = false, - AntialiasThreshold = .25F, BlendPercentage = .25F, ColorBlendingMode = PixelColorBlendingMode.HardLight, }; @@ -79,7 +70,6 @@ public void CloneIsDeep() actual.AlphaCompositionMode = PixelAlphaCompositionMode.DestAtop; actual.Antialias = false; - actual.AntialiasThreshold = .25F; actual.BlendPercentage = .25F; actual.ColorBlendingMode = PixelColorBlendingMode.HardLight; diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_134.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_134.cs index 3498b9191..849e3f460 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_134.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_134.cs @@ -9,10 +9,21 @@ namespace SixLabors.ImageSharp.Drawing.Tests.Issues; public class Issue_134 { + // The issue renders 8pt Tahoma at 100 dpi on a 128x64 white panel, wrapped at the image + // width and centred both ways about (0, 31) under the era's API, which centred every + // line within the wrap box anchored at the origin. The + // era measured the text wider than the panel and broke before "amet"; corrected metrics + // fit the whole string in the panel, so the wrap length here is the one that reproduces + // the issue's two line output rather than the literal value that no longer would. The + // modern API centres on the origin, so the equivalent origin is the box centre (64, 31). + // Full hinting grid fits the outlines and, without antialiasing, the renderer samples + // them at pixel centres, so the panel matches the classic bi-level clarity the issue + // compares against. [Theory] - [WithSolidFilledImages(128, 64, nameof(Color.White), PixelTypes.Rgba32, true)] - [WithSolidFilledImages(128, 64, nameof(Color.White), PixelTypes.Rgba32, false)] - public void LowFontSizeRenderOK(TestImageProvider provider, bool antialias) + [WithSolidFilledImages(128, 64, nameof(Color.White), PixelTypes.Rgb24, HintingMode.None, true)] + [WithSolidFilledImages(128, 64, nameof(Color.White), PixelTypes.Rgb24, HintingMode.Full, true)] + [WithSolidFilledImages(128, 64, nameof(Color.White), PixelTypes.Rgb24, HintingMode.Full, false)] + public void LowFontSizeRenderOK(TestImageProvider provider, HintingMode hintingMode, bool antialias) where TPixel : unmanaged, IPixel { if (!TestEnvironment.IsWindows) @@ -23,12 +34,7 @@ public void LowFontSizeRenderOK(TestImageProvider provider, bool provider.RunValidatingProcessorTest( c => { - c.SetGraphicsOptions( - new GraphicsOptions - { - Antialias = antialias, - AntialiasThreshold = .33F - }); + c.SetGraphicsOptions(new GraphicsOptions { Antialias = antialias }); c.Paint(canvas => { @@ -36,16 +42,19 @@ public void LowFontSizeRenderOK(TestImageProvider provider, bool Font font = SystemFonts.Get("Tahoma").CreateFont(8); RichTextOptions options = new(font) { - WrappingLength = c.GetCurrentSize().Width / 2, + Dpi = 100, + HintingMode = hintingMode, + WrappingLength = 110, + TextAlignment = TextAlignment.Center, VerticalAlignment = VerticalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Center, - Origin = new PointF(c.GetCurrentSize().Width / 2, c.GetCurrentSize().Height / 2) + Origin = new PointF(c.GetCurrentSize().Width / 2, 31) }; canvas.DrawText(options, "Lorem ipsum dolor sit amet", brush, null); }); }, - testOutputDetails: $"{antialias}", + testOutputDetails: $"{hintingMode}_{(antialias ? "Antialiased" : "Aliased")}", appendSourceFileOrDescription: false); } } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index ce675e7a5..16d000a2b 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -116,7 +116,7 @@ public void FillPath_AliasedWithThreshold_MatchesDefaultOutput(TestImage { DrawingOptions drawingOptions = new() { - GraphicsOptions = new GraphicsOptions { Antialias = false, AntialiasThreshold = 0.25F } + GraphicsOptions = new GraphicsOptions { Antialias = false } }; EllipsePolygon ellipse = new(256, 256, 200, 150); @@ -1051,6 +1051,59 @@ public void DrawText_WithRepeatedGlyphs_UsesCoverageCache(TestImageProvi AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage); } + /// + /// Verifies exact CPU and GPU agreement for small, fully hinted, aliased text. + /// + /// The pixel type. + /// The test image provider. + // Small glyphs contain thin horizontal and vertical parts that can fall between pixel-centre + // lines. The varied origins also place glyph edges on tile boundaries. Use an opaque canvas so + // transparent pixels cannot hide colour-channel differences. + [WebGPUTheory] + [WithSolidFilledImages(560, 200, 255, 255, 255, PixelTypes.Rgba32)] + public void DrawText_AliasedFullHintingLadder_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = false } + }; + + Brush brush = Brushes.Solid(Color.Black); + void DrawAction(DrawingCanvas canvas) + { + float y = 3F; + for (int size = 6; size <= 16; size++) + { + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, size); + RichTextOptions textOptions = new(font) + { + Origin = new PointF(3F + (size % 3), y), + HintingMode = HintingMode.Full, + }; + + canvas.DrawText(textOptions, "The quick brown fox jumps over the lazy dog.", brush, null); + y += size + 5F; + } + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0F); + } + [WebGPUTheory] [WithBlankImage(1200, 280, PixelTypes.Rgba32)] public void DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath(TestImageProvider provider) diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs index dc36f66aa..15d432b76 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs @@ -282,8 +282,7 @@ private sealed class CapturingBackend : IDrawingBackend new RasterizerOptions( Rectangle.Empty, IntersectionRule.NonZero, - RasterizationMode.Aliased, - 0.5f), + RasterizationMode.Aliased), default, []); @@ -397,7 +396,7 @@ public CapturedCoverageDefinition( private readonly Rectangle interest; private readonly IntersectionRule intersectionRule; private readonly RasterizationMode rasterizationMode; - private readonly int antialiasThresholdBits; + private readonly int coverageBoostBits; public CoverageDefinitionKey(CompositionCommand command) { @@ -405,7 +404,7 @@ public CoverageDefinitionKey(CompositionCommand command) this.interest = command.RasterizerOptions.Interest; this.intersectionRule = command.RasterizerOptions.IntersectionRule; this.rasterizationMode = command.RasterizerOptions.RasterizationMode; - this.antialiasThresholdBits = BitConverter.SingleToInt32Bits(command.RasterizerOptions.AntialiasThreshold); + this.coverageBoostBits = BitConverter.SingleToInt32Bits(command.RasterizerOptions.CoverageBoost); } public bool Equals(CoverageDefinitionKey other) @@ -413,7 +412,7 @@ public bool Equals(CoverageDefinitionKey other) this.interest.Equals(other.interest) && this.intersectionRule == other.intersectionRule && this.rasterizationMode == other.rasterizationMode && - this.antialiasThresholdBits == other.antialiasThresholdBits; + this.coverageBoostBits == other.coverageBoostBits; public override bool Equals(object obj) => obj is CoverageDefinitionKey other && this.Equals(other); @@ -424,7 +423,7 @@ public override int GetHashCode() this.interest, (int)this.intersectionRule, (int)this.rasterizationMode, - this.antialiasThresholdBits); + this.coverageBoostBits); } private sealed class CapturedScene : DrawingBackendScene diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Primitives.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Primitives.cs index ea4ce8b8f..7b075b4dc 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Primitives.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Primitives.cs @@ -90,7 +90,7 @@ public void DrawRectangle_AliasedRendersFullCorners(TestImageProvider DrawingClipDescriptor.CreateRectangle( new RectangleF(x, y, width, height), ClipOperation.Intersection, - DrawingClipEdgeMode.Hard, - 0.5F); + DrawingClipEdgeMode.Hard); [Fact] public void Empty_HasNoClips() @@ -29,7 +28,7 @@ public void Empty_HasNoClips() [Fact] public void FromPaths_EmptyList_ReturnsEmpty() { - DrawingClipState state = DrawingClipState.FromPaths([], ClipOperation.Intersection, DrawingClipEdgeMode.Hard, 0.5F); + DrawingClipState state = DrawingClipState.FromPaths([], ClipOperation.Intersection, DrawingClipEdgeMode.Hard); Assert.False(state.HasClips); } @@ -43,7 +42,7 @@ public void FromPaths_MultiplePaths_FormOneOperand() new RectanglePolygon(20, 0, 10, 10), ]; - DrawingClipState state = DrawingClipState.FromPaths(paths, ClipOperation.Intersection, DrawingClipEdgeMode.Antialiased, 0.5F); + DrawingClipState state = DrawingClipState.FromPaths(paths, ClipOperation.Intersection, DrawingClipEdgeMode.Antialiased); Assert.Equal(1, state.Count); Assert.Equal(2, state.GetDescriptor(0).Paths.Count); @@ -183,8 +182,7 @@ public void TryGetConservativeBounds_IgnoresDifferenceDescriptors() DrawingClipDescriptor difference = DrawingClipDescriptor.CreateRectangle( new RectangleF(0, 0, 10, 10), ClipOperation.Difference, - DrawingClipEdgeMode.Hard, - 0.5F); + DrawingClipEdgeMode.Hard); DrawingClipState state = DrawingClipState.Empty.Append(difference); Assert.False(state.TryGetConservativeBounds(Point.Empty, out _)); @@ -223,8 +221,7 @@ public void TryGetTargetBoundsClip_DifferenceOperation_ReturnsFalse() DrawingClipDescriptor difference = DrawingClipDescriptor.CreateRectangle( new RectangleF(0, 0, 10, 10), ClipOperation.Difference, - DrawingClipEdgeMode.Hard, - 0.5F); + DrawingClipEdgeMode.Hard); DrawingClipState state = DrawingClipState.Empty.Append(difference); Assert.False(state.TryGetTargetBoundsClip(Point.Empty, out _)); @@ -235,8 +232,7 @@ public void TryGetTargetBoundsClip_SingleRectangleIntegerRegion_ReturnsOffsetBou { DrawingClipDescriptor region = DrawingClipDescriptor.CreateIntegerRegion( [new Rectangle(10, 20, 30, 40)], - ClipOperation.Intersection, - 0.5F); + ClipOperation.Intersection); DrawingClipState state = DrawingClipState.Empty.Append(region); Assert.True(state.TryGetTargetBoundsClip(new Point(1, 2), out Rectangle bounds)); @@ -248,8 +244,7 @@ public void TryGetTargetBoundsClip_MultiRectangleIntegerRegion_ReturnsFalse() { DrawingClipDescriptor region = DrawingClipDescriptor.CreateIntegerRegion( [new Rectangle(0, 0, 10, 10), new Rectangle(20, 0, 10, 10)], - ClipOperation.Intersection, - 0.5F); + ClipOperation.Intersection); DrawingClipState state = DrawingClipState.Empty.Append(region); Assert.False(state.TryGetTargetBoundsClip(Point.Empty, out _)); diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Aliased.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Aliased.cs new file mode 100644 index 000000000..731618542 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Aliased.cs @@ -0,0 +1,114 @@ +// 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 +{ + /// + /// Verifies that aliased fills produce only fully covered or uncovered pixels. + /// + /// The pixel type. + /// The test image provider. + [Theory] + [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32)] + public void Fill_Aliased_ProducesOnlyBinaryCoverage(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + EllipsePolygon circle = new(50, 50, 40); + DrawingOptions options = new() { GraphicsOptions = new GraphicsOptions { Antialias = false } }; + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.Paint(options, canvas => canvas.Fill(Brushes.Solid(Color.White), circle))); + + int whitePixels = CountPixelsAbove(image, 250); + int partialPixels = CountPixelsBetween(image, 1, 250); + + Assert.Equal(0, partialPixels); + Assert.True(whitePixels > 0, "Expected some white pixels from the filled circle."); + } + + /// + /// Verifies that antialiased fills produce partial edge coverage. + /// + /// The pixel type. + /// The test image provider. + [Theory] + [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32)] + public void Fill_Antialiased_ProducesPartialCoverage(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + EllipsePolygon circle = new(50, 50, 40); + DrawingOptions options = new() { GraphicsOptions = new GraphicsOptions { Antialias = true } }; + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.Paint(options, canvas => canvas.Fill(Brushes.Solid(Color.White), circle))); + + Assert.True(CountPixelsBetween(image, 1, 250) > 0, "Expected partially covered edge pixels."); + } + + /// + /// Counts pixels whose red channel is above the specified value. + /// + /// The pixel type. + /// The image to inspect. + /// The exclusive lower limit. + /// The matching pixel count. + private static int CountPixelsAbove(Image image, byte cutoff) + where TPixel : unmanaged, IPixel + { + int count = 0; + image.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + Span row = accessor.GetRowSpan(y); + for (int x = 0; x < row.Length; x++) + { + Rgba32 rgba = row[x].ToRgba32(); + if (rgba.R > cutoff) + { + count++; + } + } + } + }); + + return count; + } + + /// + /// Counts pixels whose red channel is within the specified half-open range. + /// + /// The pixel type. + /// The image to inspect. + /// The inclusive lower limit. + /// The exclusive upper limit. + /// The matching pixel count. + private static int CountPixelsBetween(Image image, byte low, byte high) + where TPixel : unmanaged, IPixel + { + int count = 0; + image.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + Span row = accessor.GetRowSpan(y); + for (int x = 0; x < row.Length; x++) + { + Rgba32 rgba = row[x].ToRgba32(); + if (rgba.R >= low && rgba.R < high) + { + count++; + } + } + } + }); + + return count; + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.AntialiasThreshold.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.AntialiasThreshold.cs deleted file mode 100644 index 5549faa30..000000000 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.AntialiasThreshold.cs +++ /dev/null @@ -1,133 +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.Processing; - -public partial class ProcessWithDrawingCanvasTests -{ - [Theory] - [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32)] - public void Fill_AliasedWithDefaultThreshold(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - EllipsePolygon circle = new(50, 50, 40); - DrawingOptions options = new() { GraphicsOptions = new GraphicsOptions { Antialias = false } }; - - using Image image = provider.GetImage(); - image.Mutate(ctx => ctx.Paint(options, canvas => canvas.Fill(Brushes.Solid(Color.White), circle))); - - int whitePixels = CountPixelsAbove(image, 250); - int partialPixels = CountPixelsBetween(image, 1, 250); - - // Aliased mode should produce no partial-coverage pixels. - Assert.Equal(0, partialPixels); - Assert.True(whitePixels > 0, "Expected some white pixels from the filled circle."); - } - - [Theory] - [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32)] - public void Fill_AliasedLowThreshold_ProducesMorePixelsThanHighThreshold(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - EllipsePolygon circle = new(50, 50, 40); - - DrawingOptions lowOptions = new() - { - GraphicsOptions = new GraphicsOptions { Antialias = false, AntialiasThreshold = 0.1F } - }; - - DrawingOptions highOptions = new() - { - GraphicsOptions = new GraphicsOptions { Antialias = false, AntialiasThreshold = 0.9F } - }; - - using Image lowImage = provider.GetImage(); - lowImage.Mutate(ctx => ctx.Paint(lowOptions, canvas => canvas.Fill(Brushes.Solid(Color.White), circle))); - int lowCount = CountPixelsAbove(lowImage, 250); - - using Image highImage = provider.GetImage(); - highImage.Mutate(ctx => ctx.Paint(highOptions, canvas => canvas.Fill(Brushes.Solid(Color.White), circle))); - int highCount = CountPixelsAbove(highImage, 250); - - // A lower threshold includes more edge pixels, so the fill area should be larger. - Assert.True(lowCount > highCount, $"Low threshold ({lowCount} pixels) should produce more pixels than high threshold ({highCount} pixels)."); - } - - [Theory] - [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32)] - public void Fill_AntialiasedIgnoresThreshold(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - EllipsePolygon circle = new(50, 50, 40); - - DrawingOptions options1 = new() - { - GraphicsOptions = new GraphicsOptions { Antialias = true, AntialiasThreshold = 0.1F } - }; - - DrawingOptions options2 = new() - { - GraphicsOptions = new GraphicsOptions { Antialias = true, AntialiasThreshold = 0.9F } - }; - - using Image image1 = provider.GetImage(); - image1.Mutate(ctx => ctx.Paint(options1, canvas => canvas.Fill(Brushes.Solid(Color.White), circle))); - - using Image image2 = provider.GetImage(); - image2.Mutate(ctx => ctx.Paint(options2, canvas => canvas.Fill(Brushes.Solid(Color.White), circle))); - - // In antialiased mode the threshold is irrelevant; images should be identical. - ImageComparer.Exact.VerifySimilarity(image1, image2); - } - - private static int CountPixelsAbove(Image image, byte threshold) - where TPixel : unmanaged, IPixel - { - int count = 0; - image.ProcessPixelRows(accessor => - { - for (int y = 0; y < accessor.Height; y++) - { - Span row = accessor.GetRowSpan(y); - for (int x = 0; x < row.Length; x++) - { - Rgba32 rgba = row[x].ToRgba32(); - if (rgba.R > threshold) - { - count++; - } - } - } - }); - - return count; - } - - private static int CountPixelsBetween(Image image, byte low, byte high) - where TPixel : unmanaged, IPixel - { - int count = 0; - image.ProcessPixelRows(accessor => - { - for (int y = 0; y < accessor.Height; y++) - { - Span row = accessor.GetRowSpan(y); - for (int x = 0; x < row.Length; x++) - { - Rgba32 rgba = row[x].ToRgba32(); - if (rgba.R >= low && rgba.R < high) - { - count++; - } - } - } - }); - - return count; - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs index 0e3c49a33..7b47740f0 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs @@ -127,8 +127,6 @@ public void DrawLines_Simple(TestImageProvider provider, string [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 { diff --git a/tests/ImageSharp.Drawing.Tests/Processing/RichTextGlyphRendererTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/RichTextGlyphRendererTests.cs index 519b394b9..57ae26bd9 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/RichTextGlyphRendererTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/RichTextGlyphRendererTests.cs @@ -51,6 +51,24 @@ public void SetDecoration_RunBoundary_FlushesSegment() Assert.Equal(2, underlinedCount - plainCount); } + [Fact] + public void FullHinting_CacheHitMatchesFreshRasterizationAtFractionalOrigin() + { + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 24); + DrawingTextCache sharedCache = new(); + + _ = RenderSingleGlyph(font, new PointF(10.3F, 10.7F), sharedCache); + Assert.Equal(1, sharedCache.Count); + + DrawingOperation cached = RenderSingleGlyph(font, new PointF(13.8F, 14.1F), sharedCache); + DrawingOperation fresh = RenderSingleGlyph(font, new PointF(13.8F, 14.1F), new DrawingTextCache()); + + // Fonts resolves the final hinted origin before BeginGlyph, so the ordinary bounds + // offset path must reproduce both components of the fresh operation's device position. + Assert.Equal(fresh.RenderLocation, cached.RenderLocation); + Assert.Equal(fresh.SubPixelOffset, cached.SubPixelOffset); + } + private static int CountOperations(Font font, string text, List? runs) { RichTextOptions options = new(font); @@ -73,4 +91,27 @@ private static int CountOperations(Font font, string text, List? ru // Dispose clears the caller-owned operation list, so count before leaving scope. return operations.Count; } + + private static DrawingOperation RenderSingleGlyph(Font font, PointF origin, DrawingTextCache cache) + { + RichTextOptions options = new(font) + { + HintingMode = HintingMode.Full, + Origin = origin, + }; + + List operations = []; + using RichTextGlyphRenderer renderer = new( + new DrawingOptions(), + path: null, + pen: null, + brush: Brushes.Solid(Color.Black), + cache, + operations); + + TextRenderer.RenderTo(renderer, "H", options); + + // Return the value copy before disposing the renderer, which clears the caller-owned list. + return Assert.Single(operations); + } } diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/TextBuilderTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/TextBuilderTests.cs index 3dd2f2746..6115855af 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/TextBuilderTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/TextBuilderTests.cs @@ -32,12 +32,16 @@ public void TextBuilder_Bounds_AreCorrect_Paths() Assert.Equal(measuredBounds.X, builderBounds.X); Assert.Equal(measuredBounds.Y, builderBounds.Y); - Assert.Equal(measuredBounds.Width, builderBounds.Width); + + // Outline geometry is quantized to the 26.6 fixed-point grid while the measurer works in + // unquantized metrics, so the widths agree only to within one grid step of 1/64 pixel. + Assert.True(MathF.Abs(measuredBounds.Width - builderBounds.Width) <= 1F / 64F); // TextMeasurer will measure the full lineheight of the string. // TextBuilder does not include line gaps following the descender since there - // is no path to include. - Assert.True(measuredBounds.Height >= builderBounds.Height); + // is no path to include; quantized outline geometry can still poke one 26.6 + // grid step past the unquantized measurement. + Assert.True(measuredBounds.Height >= builderBounds.Height - (1F / 64F)); } [Fact] @@ -62,12 +66,16 @@ public void TextBuilder_Bounds_AreCorrect_Glyphs() Assert.Equal(measuredBounds.X, builderBounds.X); Assert.Equal(measuredBounds.Y, builderBounds.Y); - Assert.Equal(measuredBounds.Width, builderBounds.Width); + + // Outline geometry is quantized to the 26.6 fixed-point grid while the measurer works in + // unquantized metrics, so the widths agree only to within one grid step of 1/64 pixel. + Assert.True(MathF.Abs(measuredBounds.Width - builderBounds.Width) <= 1F / 64F); // TextMeasurer will measure the full lineheight of the string. // TextBuilder does not include line gaps following the descender since there - // is no path to include. - Assert.True(measuredBounds.Height >= builderBounds.Height); + // is no path to include; quantized outline geometry can still poke one 26.6 + // grid step past the unquantized measurement. + Assert.True(measuredBounds.Height >= builderBounds.Height - (1F / 64F)); } [Fact] diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/GraphicsOptionsComparer.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/GraphicsOptionsComparer.cs index c58fafc40..fcdb3f9c2 100644 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/GraphicsOptionsComparer.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/GraphicsOptionsComparer.cs @@ -16,7 +16,6 @@ public bool Equals(GraphicsOptions x, GraphicsOptions y) return x.AlphaCompositionMode == y.AlphaCompositionMode && x.Antialias == y.Antialias - && x.AntialiasThreshold == y.AntialiasThreshold && x.BlendPercentage == y.BlendPercentage && x.ColorBlendingMode == y.ColorBlendingMode; } diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_RegionAndPath_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_RegionAndPath_MatchesReference_Rgba32.png index 84d1c06e3..d89824dec 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_RegionAndPath_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_RegionAndPath_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b255f0a986d5b33773a7af23cdb8411f3a2fbab02b64ae7cd29cbe4ff748b3fc -size 3632 +oid sha256:136af17eecc9605ce27a056aecdbab3cf432b64bdde357588cf534e8b318d3f7 +size 4358 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 7aff2bf2a..e4dc5d472 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:218011a820dab6702a1ca7bd0eefec1f35b430370732657d5ab32b481b36d3b7 -size 11000 +oid sha256:ff72a4a794fa4b726e7c64da3bcd8272858fb25d6e649e856445953357432de9 +size 10911 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 23996dd7b..f2329e876 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:b4a91e81f7708c71c8d1f3c20b2718b9f036c088b90e3890e2078bddacdf4de3 -size 8643 +oid sha256:b5d938f0ef0a5239a256f37a53e6e345893e6d958e5b62fe25ddb03ddc43e3ad +size 8721 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 eb52f831a..48a76c14b 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:4b9563a979fd3797426abf4e745e90fc0e9e58ff24afefb9ec00c33b55fa8a57 -size 11114 +oid sha256:64e045f409b6562d0b8f87a44457af2f7bf4921c7de9fe0de64bca9fb4167db4 +size 11263 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 eb52f831a..48a76c14b 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:4b9563a979fd3797426abf4e745e90fc0e9e58ff24afefb9ec00c33b55fa8a57 -size 11114 +oid sha256:64e045f409b6562d0b8f87a44457af2f7bf4921c7de9fe0de64bca9fb4167db4 +size 11263 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 0f7212f1d..1df7d5231 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:4600a4582640f739dcc7ef2533ca266e6303ba8d8e2fe08d935ee6b2645fb327 -size 11083 +oid sha256:9769b5b38d84cc4858289bbdff68fded5d92dd202599c0dbe2c3d6898ec0a141 +size 10893 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPieHelpers_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPieHelpers_MatchesReference_Rgba32.png index d493cfce4..6d4ec0307 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPieHelpers_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPieHelpers_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b0d3bbd305cac719874d45b11de59bd50f8fd2775f3a294f69b9001f41e12fc -size 4263 +oid sha256:b88aaf41358bb201f316100b7cd8a05bfc784222ea36cde4a871bd65827d9daa +size 4289 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPrimitiveHelpers_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPrimitiveHelpers_MatchesReference_Rgba32.png index d02b21d8f..5b1515dda 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:dd3d2a1795424ff40abf616e55b34cb5365a77fba90b6c0442b957312ac10dc2 -size 9092 +oid sha256:18be952d92b15fbb3f2c8493a7413b796530a5bf02b87313bd517f8c8b7203b6 +size 9130 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 0c2805844..114a18dee 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:c0b92fd87faa72e92dc89b2d9a97a71fde2c96c1b388e8bfd338e55fc182a358 -size 10861 +oid sha256:a588a61d73a617512678d6b0f7cf02b8d4b63a58668b141661b35ddedcad8f36 +size 10974 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 35d519994..344ab77ba 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:45fa8aa1a7e6bc272875a9fc746ca5d1781e6065cd6016a3114aacfac4945213 -size 19999 +oid sha256:2e9e57e1f7db0b35d863746bfe2cd311cbdf2394c70ea869fe5b1f9304bb23d1 +size 19712 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_LineLayoutsAlongDifferentPaths_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_LineLayoutsAlongDifferentPaths_MatchesReference_Rgba32.png index 40af29788..dcbf79ead 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_LineLayoutsAlongDifferentPaths_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_LineLayoutsAlongDifferentPaths_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9666d4c79f8cf6b126d838aa6bc61c02f7bb3da4f69cda60837ebf3edeb08c5 -size 29517 +oid sha256:66d6ee53c7cea7102d9525d12537880ddfe8aa5aee19b80f52caae492403ac91 +size 29396 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 18ef8c476..4d88b7d84 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:b0ddaef53d274d98eadf70eec0e725f2cc7b7d54f284e88b0e37386811a2b37d -size 37416 +oid sha256:d4e255dec02afb9aa6ea070a5c9fe517cdbc94dbe5ead253f7bc3ba710f70687 +size 24910 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 28a8ddc78..91402cabb 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:cabe7445ea3b518be4c35417bc9da9c248cc617f8ed0c2e7d683a71e219b9fe3 -size 3210 +oid sha256:c6600fe2a0a7a50d1c1fcbcab94d8134306f156e334e24e6836de14f85f1f4fd +size 3074 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_TextBlockAlongPath_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_TextBlockAlongPath_MatchesReference_Rgba32.png index 33858ae37..a48061966 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_TextBlockAlongPath_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_TextBlockAlongPath_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eff0d0d3162042cc45e534f3f1ed33233cf035726b1d6cffcf618665695e2dd0 -size 20912 +oid sha256:1a2a158c0420a49db1ccadab26fb6c3c2812850c6cf2485a92cb6474e7e4a929 +size 20956 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 65d24be66..02d8624a7 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:3ca3fa74665c9b52b624e4177b3700a285263454ef159f73a8f93e0c0ad6b06f -size 37626 +oid sha256:756cdb776cdd80223a046a4c95672abea632deac3fc6ebebb705c5adfc17594f +size 37672 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 550e824f4..238080d0b 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:fd51fce4f7bfd405d9f5965e6d97b0a87b1eb1986c8b4663586ddb418ee1dde1 -size 8512 +oid sha256:51dc1ecba5e6b67c76e40e3cb533731463998e63a1130753c8a5ae96e59d1f8f +size 8497 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/FillPrimitiveHelpers_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/FillPrimitiveHelpers_MatchesReference_Rgba32.png index 47899bc76..9fedc8533 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/FillPrimitiveHelpers_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/FillPrimitiveHelpers_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a16b1b47c9f8afa1d2accedd970177c1ac1018419f4b7b98ab4446cafb723cc0 -size 3298 +oid sha256:92bfc7bbfe9ecf0b78b600476a7fd7850b33002714748a8dd65eca051d5a1b5f +size 3301 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 ea9559b4b..e8b250dc2 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:4bd803e978fb02bfd75b7eb85f7ef555f1ae39171100a3b7098c31104849d94b -size 20859 +oid sha256:38942cd0c837b5b9209bb3f31f9f6f705a661503f51d9f771e193c1b0b40c4d6 +size 20862 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_PathBuilder_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_PathBuilder_MatchesReference_Rgba32.png index 5061a0b0d..b776db62e 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_PathBuilder_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_PathBuilder_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79746f0adf5db701238ebe0910228cdb73fd666b51addd7f46b4019aba78f7e6 -size 13049 +oid sha256:7afcbc3463eb2de9a40d8561c3a1167bfe21a88a5a7084d0c06c38526afe6e6f +size 13041 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 7769792b3..cfef59771 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:cba5e51955bc1fcc91f7a4d5a5b2e094653f8706879cdf8bf34d05492e4a2c22 -size 13041 +oid sha256:857786ae833c0fd7aa43fd2809eea6099afd0808a57b315d1a3416488290db34 +size 13008 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/RoundedRectanglePolygon_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/RoundedRectanglePolygon_MatchesReference_Rgba32.png index d1a2a81b5..fd8d21e79 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/RoundedRectanglePolygon_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/RoundedRectanglePolygon_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d83614918b91c5c4face452817c6387e1ee8ed39376e87e934ce54682e5c14b8 -size 5131 +oid sha256:1b67192c56a2c877af00f038f82ade14b156ca8db10084c981f2810ba15c22d6 +size 5138 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/TextMeasuring_RenderedMetrics_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/TextMeasuring_RenderedMetrics_MatchesReference_Rgba32.png index d9be1f2c4..ccbf31295 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/TextMeasuring_RenderedMetrics_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/TextMeasuring_RenderedMetrics_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e8f1daad7615b839878d8af547a42dda0cc6c51430a0284cff238f209e52fdc -size 26268 +oid sha256:e84c9db30fb5cd7bc89c9211be106af2b0d5a233d426208a523c02198e9056db +size 26194 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/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 index e1b76c676..0c3fc0ac3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6d120d307790eec54397bc818462668df94025c4d5d9bb4713e263f1c0b8b0f -size 4012 +oid sha256:9a9caa8f16a5058260e8f096effd786734bfdd2abb523164be8ab1e5436976fb +size 4034 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/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 index 2570b4eba..1d8fbc89b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:659faf60f57b436d3a7f8851869cdf6c972779cd38f80ad1068fed8ff14c1e40 +oid sha256:c79601843b12ad6051288935685b79b79e5b06f64087b123c27be1d316481d04 size 4122 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/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 index d7921e097..414642b5f 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dcab42cff919a8739e097127db58c70fe6dc80dfb02a3b46f714e1298d853c6c -size 5178 +oid sha256:9640783175f9d4ba29c2d5d08eaa829e5c08c32c19ae4af0cd0c480bc618818f +size 5192 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank100x100_type-spiral.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank100x100_type-spiral.png index 29a7e772a..29feb6304 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank100x100_type-spiral.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank100x100_type-spiral.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:73d63e3ad2afd0d1e7186a2304b95cc98623b97ce874e1e75649aeeb608946c5 -size 5311 +oid sha256:b0ba5ce072fff46c4888a7fab951044ca4fac68fb9b67c331e55194c83324235 +size 5340 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png index 707399ad2..9fb5449fe 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a039bf64e2449c129d967977874f081826d6694fa523a94d49d00194bcf76113 -size 4443 +oid sha256:d9edc9d7146031be2d5b72a00844f8787a51dec9705a7ec80c6a336bcac5d43d +size 4446 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank350x350_type-circle.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank350x350_type-circle.png index 07a8bf39b..4985b2507 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank350x350_type-circle.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank350x350_type-circle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d55fe15a80a6391fb95626684ec485e8a9c8d5e65a2fdf6ac2250e9969fe26c -size 9525 +oid sha256:6dd58bd67536b03372cee593c70050a3497464a4e3f8de840a8bc8ac32f9fb50 +size 9366 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank250x250_type-triangle.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank250x250_type-triangle.png index d266ded4a..f4b428aaa 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank250x250_type-triangle.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank250x250_type-triangle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f85f62fbe30119c52db614b634539ab0c2cd67796726d72092056200044c49c9 -size 5135 +oid sha256:8a161f059e7d1411db96bbefa7043ec87fd65be6cb73840b31d8a9ecb5f1ddd5 +size 5145 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank350x350_type-circle.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank350x350_type-circle.png index 8f607887c..b62248149 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank350x350_type-circle.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank350x350_type-circle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:057662f3c567cf3c36d6d001a42ea4b400a0a409c4e8bf26558de3ddce6d54d3 -size 7330 +oid sha256:0e4857b2947cd2973c012160cbd79b2c39d01152ba458cbc0956fe354dc1e1b8 +size 7388 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical2_Rgba32_Blank48x935.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical2_Rgba32_Blank48x935.png index 45d7f7ee6..de1dfb99a 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:8c722cd07c3ff50d269fe9f14b8a7ce3443a2ee1dc22796d5e691ec0f8338ab8 -size 4986 +oid sha256:bb8a42fb5e8b5efb7df60566ec52fcb7a9f1f52053e7480b9e1f6fed82089232 +size 5850 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed2_Rgba32_Blank48x839.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed2_Rgba32_Blank48x839.png index fb2688850..39044c38e 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:73b7471840d14e27b6892d3f84be6d1b3867981afda1524862cd3351cfce71a5 -size 4896 +oid sha256:6c28d6ae57e497d2aac41183333c55ae0b616e9046ec3911aa62013d2d748388 +size 5717 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed_Rgba32_Blank500x400.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed_Rgba32_Blank500x400.png index 8de44377f..da3f5a4d9 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:d4ab45d1241c6cd534bca395e0ef9f093444246ece8c4e2d649da87618986641 -size 14297 +oid sha256:2aeee05a8442a672e67d8cdbbcefff417ce04c8268004f7795b33701df9e8770 +size 14580 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical_Rgba32_Blank500x400.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical_Rgba32_Blank500x400.png index 0e6c7a306..426577a52 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:f8e88227709ea57a720a0c5012b0eb76dc8880113b9b4f3052089e98d310e4f8 -size 13394 +oid sha256:e70d07e3d244278f5c3ac96feb859a79c896db4b26ab55f1573358449f3a2b8f +size 13952 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVerticalMixed_Rgba32_Blank500x400.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVerticalMixed_Rgba32_Blank500x400.png index 9a20cef7d..9b990c8a1 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:dde20574ec8bd7beba3d8e3629657e0c3ae029ae7318473b6729413d391a6e45 -size 14326 +oid sha256:bb7fe8cb5e390e5c2ec77bfae735361e44126aaf96b744c2f8a1837e31ca81f2 +size 14580 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVertical_Rgba32_Blank500x400.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVertical_Rgba32_Blank500x400.png index 846429ef3..5c4c54253 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:d91ae45bd6e142e6efa60bbe9948f282dcb4791eed2ed1d1fb9132656de10d2e -size 4530 +oid sha256:622c99f19fb8d3f1157531688126aebd02eb8131d752f8616da1d856aec33fee +size 4558 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRenderTextOutOfBoundsIssue301.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRenderTextOutOfBoundsIssue301.png index 3501a4a38..656df18d2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRenderTextOutOfBoundsIssue301.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRenderTextOutOfBoundsIssue301.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4dba65ee80dfd10d3b37a4a87ebf390ecd928a405b9aece928c09b668627bedb -size 1125 +oid sha256:3ed27e863bd8cb503a3bc5d58cbf8c0f422ab6e35705c2399e41a2607f23c4ad +size 1122 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A150_T5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A150_T5.png index a8b7e434b..d86b59c66 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A150_T5.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A150_T5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:85efe3c0ea93bb31176a4c29d7e4d893ba460efed56e32ea72fce1c02371ffdf -size 4381 +oid sha256:513ea7a1dd5483598a9dcde257c130e0eafe8d2e406ccfe3f3adb5fb5fff627a +size 4279 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A255_T5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A255_T5.png index b3cf47e13..46d9bad8c 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A255_T5.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A255_T5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:527e9370db4fc11519499c58a771b3db3aa0d03a1960b2439437e9a1db5a654a -size 8182 +oid sha256:22f876bcc4c80727237bbbcd595dde6e4d2064ee49401006c14060b86094bc1b +size 4568 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_Red_A255_T3.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_Red_A255_T3.png index 8e47a82c4..04db5bcfe 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_Red_A255_T3.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_Red_A255_T3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e48ba2874190eea6b8b33fbfd206935285c9262ffccbc66a3de297c7f8193d80 -size 7405 +oid sha256:7a65449d14830e4749e3e1dbb3bd9ee2939c86222e09862feeed71b5a24886a4 +size 4568 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T1.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T1.5.png index fb78c559d..93f37555c 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T1.5.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T1.5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:30f7a5d02ae237e246068acdcce6ed5002a5f2753ca812df8be942da8b92a222 -size 4650 +oid sha256:aad215335a4eb4178422c387dd8622ef986d2e58d8eb2e048625e65848e08c37 +size 4564 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T15.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T15.png index fb78c559d..93f37555c 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T15.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T15.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:30f7a5d02ae237e246068acdcce6ed5002a5f2753ca812df8be942da8b92a222 -size 4650 +oid sha256:aad215335a4eb4178422c387dd8622ef986d2e58d8eb2e048625e65848e08c37 +size 4564 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 deleted file mode 100644 index 6cd79f23b..000000000 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1)_NoAntialias.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a2a405e64d39be85b3475f615f06eeef94c6f3f2ed577765a6fb557d3d1b4a07 -size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5)_NoAntialias.png deleted file mode 100644 index 6053918ea..000000000 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5)_NoAntialias.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f22b03e2ea6fcd6ad41d4509e53866d2fe6a7e8b2ee1f799bcdf73167973d32d -size 97 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDotDot_Rgba32_Black_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDotDot_Rgba32_Black_A(1)_T(5)_NoAntialias.png index effaddfb6..b690782fe 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDotDot_Rgba32_Black_A(1)_T(5)_NoAntialias.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDotDot_Rgba32_Black_A(1)_T(5)_NoAntialias.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe39f7f19f0ecac81e57094946db235c78f6c54a11946e014dcfd95940de8c74 -size 997 +oid sha256:48d710b7ba6a7d9ebc0c93ef30b9043e2bf3f12826b10d1ae9b7906c4d7c2ba9 +size 1049 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDot_Rgba32_Yellow_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDot_Rgba32_Yellow_A(1)_T(5)_NoAntialias.png index ff159bcbb..1b1be7ab3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDot_Rgba32_Yellow_A(1)_T(5)_NoAntialias.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDot_Rgba32_Yellow_A(1)_T(5)_NoAntialias.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:866c456f117c27429200ec127a2e806bb4015bda44fa781ee493016e3adfb93e -size 1020 +oid sha256:ed638eb9f9b61e8ac31a5a774cca7e6f9bc61438aef11bde269589952af703f6 +size 1069 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dash_Rgba32_White_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dash_Rgba32_White_A(1)_T(5)_NoAntialias.png index eaf97614e..2b37af982 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dash_Rgba32_White_A(1)_T(5)_NoAntialias.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dash_Rgba32_White_A(1)_T(5)_NoAntialias.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6940b1313031e529227c47d46a53468468557d0bedb9545082f370b92e018a65 -size 1080 +oid sha256:d47a2d049e89508818bc14f4f8be4546191b32e837526ee17dbe0e9846d7e0ff +size 1109 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dot_Rgba32_LightGreen_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dot_Rgba32_LightGreen_A(1)_T(5)_NoAntialias.png index d84fddeac..b91b863f3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dot_Rgba32_LightGreen_A(1)_T(5)_NoAntialias.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dot_Rgba32_LightGreen_A(1)_T(5)_NoAntialias.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea45e05097cb5fc0a2b570c13f2fd1cf32b7beaccb703db4f8b71e1edf5c81ef -size 901 +oid sha256:ffe1de6d76eace2a5a848956c3393caae4d7971dd3f7b4e6c47dbce00fe54a6e +size 976 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/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 index 9293df6ca..02308458c 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f2161b3d1ce4bafc8dc583aba65b1683637f5617e106bd75d9fbe3f012b2227 -size 1056 +oid sha256:ed27425d83cd46c93a76aaaa739c44c16b152190e815088ebba86a3f5c578ce7 +size 1061 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 bcf0c8925..624f0cd32 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:27bc906153430b09b38ce85b9903ced5944a1a5dca5eaf1c0086f8d4d00171b7 -size 8032 +oid sha256:f56171d86c48eb5e26f67ddcab9a747c7b49a07397af2536317790aa15ee4e0a +size 7679 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 83c3fd706..c44d40954 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:40f8b71d69bde2346d4634ad8de7c3922a8883517f905c989d4167cd209e355b -size 15588 +oid sha256:74cc168a086cb2ce379eaa50a60f400d343e34d761b59e530d9052dec9f8d97d +size 15586 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 669933b74..513118eab 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:baa261af1ab99e68d21cfaec8f16383ff29ce2dff9775c40fee5bf80428de8bf -size 15428 +oid sha256:a8be6bec64b2fdda2a41a03cb48634a10ebc20256118aa68295eba590d67b098 +size 15324 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 8530e239d..750b47c79 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:9c7cb7d6f904e55a7c6b81856b76e715a0a43675ebb135b0ba30f4a6f3222571 -size 7579 +oid sha256:dfb172649a61e42d7ced8c4c16bd7739c84ca761927c78a49a11521d5d600fa1 +size 7308 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 639b30c67..198ed4124 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:90298cafeec606095e5e6d0d150d3bcdf73cfd0b04d57eea87712e803954e6db -size 8076 +oid sha256:727d972f22630fedf2399162e410ca536a3474ec7a8a3df38db1560aeb527138 +size 7777 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x200_(0,0,0,255)_RichText-Arabic-F(32).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x200_(0,0,0,255)_RichText-Arabic-F(32).png index b2b2942b7..61f87f7df 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x200_(0,0,0,255)_RichText-Arabic-F(32).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x200_(0,0,0,255)_RichText-Arabic-F(32).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:203744697d038fd8764d6063792868f1c1948a41b78aa279eec59bc9e16c9a35 -size 3095 +oid sha256:5fc363b8f16e600c19e1ddc23de0e674de00762ec45491e0a5727db21030c778 +size 3189 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x300_(0,0,0,255)_RichText-Arabic-F(40).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x300_(0,0,0,255)_RichText-Arabic-F(40).png index ea5335ca6..d43a830cb 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x300_(0,0,0,255)_RichText-Arabic-F(40).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x300_(0,0,0,255)_RichText-Arabic-F(40).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e5af6ab8085f9c2abea85bc556ded1ae927ce46005a6b4455b188147b207ab1f -size 3974 +oid sha256:33cdf61cc52c95d1cb30997b7aab7bb0c7100955bb0aad30b7c149914603a524 +size 3988 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextRainbow_Solid500x300_(0,0,0,255)_RichText-Rainbow-F(40).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextRainbow_Solid500x300_(0,0,0,255)_RichText-Rainbow-F(40).png index e919531da..a535925e3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextRainbow_Solid500x300_(0,0,0,255)_RichText-Rainbow-F(40).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextRainbow_Solid500x300_(0,0,0,255)_RichText-Rainbow-F(40).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54ac67ccc86da32f48ce97ca349a8bba7f31ad03a4af2dd27ae9275380ea478a -size 7560 +oid sha256:fb439ac5f437d5aee831192213bcac65cc62d6f1604556c1f2031899aa8f6a59 +size 7519 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawTextStrikethroughIgnoresSkipInk_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawTextStrikethroughIgnoresSkipInk_Rgba32.png index b505e9a10..855c886af 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawTextStrikethroughIgnoresSkipInk_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawTextStrikethroughIgnoresSkipInk_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3eae426348b062957c574806ad0c7086cb6111047a0736ad08bfe1c68141ee0e -size 2275 +oid sha256:f9487d75122b840d79ade34b7d6f3ea4cdbde29634aafeaf45f7f1405c56ed05 +size 2273 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawTextUnderlineSkipsInk_Rgba32_auto.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawTextUnderlineSkipsInk_Rgba32_auto.png index 037838b5d..3de5011f6 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawTextUnderlineSkipsInk_Rgba32_auto.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawTextUnderlineSkipsInk_Rgba32_auto.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1fe614161403bce9db3906f89889a763efe598c6f5b32b6c1de497bd60f84689 -size 2314 +oid sha256:c271b1ed91a2e42871a35421b764fd1f7cf46101b154b62ee91c0f2760f12839 +size 2318 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawTextUnderlineSkipsInk_Rgba32_none.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawTextUnderlineSkipsInk_Rgba32_none.png index cd9abac7b..677cbe58c 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawTextUnderlineSkipsInk_Rgba32_none.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawTextUnderlineSkipsInk_Rgba32_none.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d6d8e876244992089268bfffad199323db2bd932dd06d972d59150455fe3b1f -size 2280 +oid sha256:74b190ea8c0912ebc786bf9b5f7d8e305dfc5442c9e41e61cdd807e78d923f3f +size 2283 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawText_EmojiGrid_NotoColorEmoji.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawText_EmojiGrid_NotoColorEmoji.png index edc2f3b08..469c0023e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawText_EmojiGrid_NotoColorEmoji.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawText_EmojiGrid_NotoColorEmoji.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eaf160b7ef8d87f813b2b92313c803c531ed39e5bf5d3b6d73730ce70d0b2aa8 -size 704710 +oid sha256:15c7c4c18a28d2d66b81b7f9c97eb366fa7eadc54a953876e1cec51795a9022e +size 705004 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushGradientsWithTransparencyOnExistingBackground_Rgba32_Blank200x200.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushGradientsWithTransparencyOnExistingBackground_Rgba32_Blank200x200.png index 36d6f18bc..72331a910 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushGradientsWithTransparencyOnExistingBackground_Rgba32_Blank200x200.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushGradientsWithTransparencyOnExistingBackground_Rgba32_Blank200x200.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:573d4a102d9bda9c6f6e89e4c1fc41dd14e08d2314091c41dea0395b4cd680e8 -size 1131 +oid sha256:3e7dcf0c0122f47c0705982fc423b4cbbda78755269a93e92af22a37946188a7 +size 1080 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathArcToAlternates.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathArcToAlternates.png index 6ec54cbbf..829e5a7c2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathArcToAlternates.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathArcToAlternates.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:07e3ae648be8c7b49bf02fbe55eca3dfe9e2ac9443c915c25c5abbaa8df55c51 -size 1775 +oid sha256:03310890b7ed30c1ebbaa2e31cd302f7694d94429a79d958e9a65d560e9fc51f +size 1720 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 424b0b440..b46243dae 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:6872a229f42638b130921a7f56f4960351cbc8fcc5abeece89925f1654700a0a -size 17499 +oid sha256:bde53587831f006609da784d8d28ca496949b65dbfdd7a4dbb555dccf3920cc6 +size 17436 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 6435a53b7..c86517ce7 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:acb6d7261e226a71372c60c8bf4c4c12dcbdf3e69a90880bad96a5e3b38bc8a0 -size 790 +oid sha256:ed9ad39560a0d5a35da734ac90c4f14d603af67a8a1de28ed16c342e644c8004 +size 775 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 2110478cb..ea2906949 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:34b8c497bd9cde44db04478c6deb92dbabcae2e14acdb3e05aa4c0bc265b61b6 -size 16965 +oid sha256:298b139184f696076eab19c9b2358134c85fcef98e750a75747765f13c8c1ea3 +size 16879 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 2b32f6930..ddd2ae516 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:22fcacdc7995676fed257d92af1c1967efcb22bbf7f560ac5bcf8fc77751553d -size 15065 +oid sha256:f1a0e12a4c0b00ff01701839e5522b3ab873c87ae31daadeac1c0ce6e02bfdf7 +size 15123 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 dfef8823f..4ad59dada 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:5d15066bbd3c490f258ae5be853974d11178888cfee73acd66e8d0ed36160f4f -size 789 +oid sha256:ba613a9757a77f3f37abb675e9192043a45cdd7e539a512052ebfa5aace31508 +size 712 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 8838f564f..4fcc2e4ad 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:b183072d61d42409bd9c1d5cae22a96a3eed6c159bbc2b5222d655d02d39afe6 -size 15386 +oid sha256:232220f3e09f8a5b1d40f4dd892cbb3e4228294209c52feacc04c38e43aa0457 +size 14637 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_LargeText.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_LargeText.png index b95bb51c3..6e20bb65f 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_LargeText.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_LargeText.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:46bc54948a8ce95c97da3eb909a6d2d3a1840d5c0efc5a2d5f665ba9257d400c -size 130784 +oid sha256:066a3db32a9b5372284a194f62cb4601b11e215852871e57293c4aebb2dbebae +size 127153 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid1100x200_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(150,50).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid1100x200_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(150,50).png index 55eed1ce9..a3ac8305e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid1100x200_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(150,50).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid1100x200_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(150,50).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9bcb292dde40781a191846cdeb8a6ce1ff771a4e86d2614c02d8d3d0c6cc032e -size 10972 +oid sha256:692311790af15fdd36b7f9e7b666ee7b4748ed1474aef27ab4ccee784270457f +size 10994 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/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 index 7c9634752..1684fb6df 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ee6d51df9002b3b11f6e64d2e61872bca07483bc897b4d727da3705df5a4b8cc +oid sha256:518129c83a3f4bca1ae4382b3d88a5f5273c7a2edc47d1f888ccca086763e328 size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/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 index 6e0f5fe13..33b3da1a3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cd6df4330dfbabd7474c98774a58e67ccafbe9b59d49f115814ae7580677e666 -size 3649 +oid sha256:569b9952524bd89d1afb34938c36f5e6e25291fae41ef61409623dfcf23ea3f4 +size 3484 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid900x150_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid900x150_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(0,0).png index b6e648e01..ecd8c30dd 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid900x150_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid900x150_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:15c50a278cb765d97662f1c4cb38ad70c30eccee33f59dc0b3a2a303db9164a1 -size 10361 +oid sha256:11d22809976a0a1ecb9a963568786f9051964cd6c8a050a418792f85964d12b4 +size 10453 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/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 index 5761f761b..929f48f28 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0695afbff866e73ead0df2c71d48c6a78925470d2c3ef79470fadea3e022e459 -size 2932 +oid sha256:445f392131fe5291b064df3bbfa9f7bfb9b5200a974ed22ddcd5c0089cfe6e56 +size 3081 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/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 index d3f97c96c..b37470cac 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56b4b12320241768e60102389a7c5bc2480bdaee3c5743b5dcb5fd82ef55d851 -size 29443 +oid sha256:faf8e480bf89044ca62b3cea7d6f1aa59ff3a4280474c3cd64b364e859b7f0b6 +size 29314 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_False.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_False.png index 230522e23..2b1e23833 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_False.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_False.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bfc831ad8f6aea39b2cf42ef6974cf417d8cfd40d0bd8d4889791098ef85d041 -size 2945 +oid sha256:5f4c9a0c5bbb6a8bd12e3bc93d1dee8784a8aab90a68b0feb6343a3c4be61735 +size 3084 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_True.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_True.png index 30cc5a54d..50d853732 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_True.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_True.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a336b86687d75001d8ca85d7d9a760f8ed848efcb0b47f7b2ff661dcdbca44e6 -size 47470 +oid sha256:5f8f60e3207818583072027b59c077fc39c4ff58be722642c41be2faa7c444ec +size 47263 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_False.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_False.png index e4d169808..428d8d9b8 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_False.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_False.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:847f9cc666ec6ba93bc2395748edea1f7b75a67aa2ff7265f2e145ee93e5e6bc -size 2910 +oid sha256:5717716d86e3022c94f2dfd3607bde574bc415c02046096a699d64e980e670bb +size 2830 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_True.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_True.png index 37c4036c5..d4bb4a54c 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_True.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_True.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:313497ac05e4f145fdb64d81be5261e4f4a449037335d846c7b262ec7a20fae8 -size 20300 +oid sha256:a0824ff9f8f3db421eb2557373f2ca32739a7686bd62e7aa9203932ec50c0d0e +size 20272 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(45)-Sphi-(200,200).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(45)-Sphi-(200,200).png index 0de8fe7d7..b302acfc3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(45)-Sphi-(200,200).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(45)-Sphi-(200,200).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:07d50fa6126f183117f143fdff8dfef1ca02a2a9ed798f31c230a41d475ecccf -size 4982 +oid sha256:57ee76530a6a5492fabb99f0f38cd35c7ed229b9bab59e1c6edc7461088454e8 +size 5037 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/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 index b3ae8fad4..96c8b92e7 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ca5fd285a2214a08f363a2a3ec040cdb81bacc2d55dad2b92f5393ef88ae7d8a -size 226 +oid sha256:cc5d53e3f852223cb85380522dea9ed4443d7d3dc7e660cf4055b4d978464e33 +size 229 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(0,-10)-Sphi-(200,200).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(0,-10)-Sphi-(200,200).png index dfefed36e..7c4087701 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(0,-10)-Sphi-(200,200).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(0,-10)-Sphi-(200,200).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be8bc6a30334f2c06acf4c1b1f5be6feb8307b4d6ed9da0e87f739bcd97fcdf6 -size 4849 +oid sha256:77daff981f04f0a4de7a72e6bfc42901765dcf050f84bfe5a2578ef0956e8c5b +size 4830 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(-12,0)-i-(25,25).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(-12,0)-i-(25,25).png index a15d84060..3e626a21d 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(-12,0)-i-(25,25).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(-12,0)-i-(25,25).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d357dbffb47720eba47c1904fcb5c1d071d4be44774db3b9fab754c86d9c093 -size 292 +oid sha256:f792e46d9f8f812edb4d0c746c0b48f9fcd608df7d38b6d04f878ac432aef856 +size 301 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SolidBezierFilledBezier_Rgba32_Blank500x500.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SolidBezierFilledBezier_Rgba32_Blank500x500.png index a5ea92305..d166e423e 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:6e6bef4f6bc88e6d736cd9b32f58550b2356269226934d0f932656ca8cf2b1f8 -size 7641 +oid sha256:566fa6ad81d0fd852877a33689030e09bc14b2073499062e016b8de48c6ef114 +size 3163 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SolidBezierOverlayByFilledPolygonOpacity_Rgba32_Blank500x500.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SolidBezierOverlayByFilledPolygonOpacity_Rgba32_Blank500x500.png index c61d9f16f..03ea57dbb 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:c54962c64afd4e10cb5bd36fa7f1daed2e67baa45db21ab338c6631be985b8db -size 3016 +oid sha256:2fc57603e6524cc2328b80360cc31aa59b3d5b1e5493288b676777854ffb9dd1 +size 2979 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 5a73450e4..60f59e232 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:ca78f4d28f84517ba314a2287699401559855fa2db0ce201eda3cba2a993a66e -size 4883 +oid sha256:0ceff800d714be52e0ed80ecf99a0e5111d723c3b947f85073f20a35f5a3b93f +size 4832 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 31be59c88..556cadba7 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:be6e7a562b928e1b3abc4bf8ef19c2bd8f385b3a2394d49dedd6e171c42542af -size 4902 +oid sha256:1097180d2ccdcb816268f9ea231e7d445a57e60673eb3c156890370808b24af1 +size 4864 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/BlurredBoxShadow_DifferenceClip_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/BlurredBoxShadow_DifferenceClip_MatchesDefaultOutput_Default.png index d4237742e..c917703a7 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/BlurredBoxShadow_DifferenceClip_MatchesDefaultOutput_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/BlurredBoxShadow_DifferenceClip_MatchesDefaultOutput_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:10068ba45692e023d4d5a809380c0ae99e2336d87828703a35887bb4f9685522 -size 1137 +oid sha256:369fd4bec260ebe8bea2ede6549fe5a8b66aa26a41167299eb82d67f34fa4eab +size 1012 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/BlurredBoxShadow_DifferenceClip_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/BlurredBoxShadow_DifferenceClip_MatchesDefaultOutput_WebGPU_NativeSurface.png index 0a220acb7..6f2e3e410 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/BlurredBoxShadow_DifferenceClip_MatchesDefaultOutput_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/BlurredBoxShadow_DifferenceClip_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b52cb6e0a5d10e888db9445b68e16cde9fb4b1c4b9db506dfe8ac41432d703c -size 1095 +oid sha256:7af4bcbe1ed53a037819786b509245d7859b82c1e44fa8b3538cd2256201e676 +size 982 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_Default.png index f11733333..196caecb9 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e840cf50433f85bf9f9f04046d114b232fd9888c290a3581c8eb0b627acdc68 -size 31558 +oid sha256:1363b9f154e82c1fe09941c687dc3fe6e66dbe46ed70142be8fe955286caddc4 +size 31651 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_WebGPU_NativeSurface.png index b0a75b741..3ce533693 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b54d12359554df5814d5f6abcc9a58fc7b704b6188dede8a94d563a86e1d7bd -size 31579 +oid sha256:e321ff9be5bf1cc3195a19d4df67eb8efcc5722dabb1e01b189e651688c6f2e0 +size 31623 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_acrylic_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_acrylic_Default.png index 07c675adc..d151a9b57 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_acrylic_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_acrylic_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a58cead7e52c6ea1829b82fa4f400fe32f91b8595202fd4477ea2c9078c19461 -size 66604 +oid sha256:b500298b467004907a1ad28829754f07becdae12cd3efbf5329b78bebbc527a6 +size 66512 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_acrylic_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_acrylic_WebGPU_NativeSurface.png index b870c19fd..4d3bab323 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_acrylic_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_acrylic_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ee23743376762e09ae283e99628dbce4b28a81c74cdc399b039ad6f9ae48cc5 -size 65249 +oid sha256:5d846917eb6c45c177dc13bc37f5744db329f3041941a37f0ba1e9a1141781ac +size 65215 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_brightness_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_brightness_Default.png index 4f7743068..df111cbde 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_brightness_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_brightness_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64f3e5d3cf177a0df472940873ad98b468f64f827d3d6c9e61dcf2ff49a1f642 -size 77722 +oid sha256:7881633a83a53980cf8a7bc7063813defb0153b075ef43e0c5d68ea36ccc2563 +size 77789 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_brightness_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_brightness_WebGPU_NativeSurface.png index ffb07805d..bf1b2d679 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_brightness_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_brightness_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6532b2e82142d5303a95db86be0bd64ad25ea5fcfc6d9e01d1a005f082e11dbe -size 77829 +oid sha256:2b638dad3cb2a692656e02b5e4f5fcb60709944273922d17970ca93a39a00822 +size 77795 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_contrast_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_contrast_Default.png index e024ed48d..9f69d8f78 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_contrast_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_contrast_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:17322cc550d237e05c534c91c641cd976f7cd836cde6229134a0ad0a71f27020 -size 72160 +oid sha256:cdad3092d651da5a72fef42a1b85c0732f9d0b147f1b962d650df17cf17ab323 +size 72090 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_contrast_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_contrast_WebGPU_NativeSurface.png index d393c7da7..73a97319e 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_contrast_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_contrast_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d38a0027007b860181c632bb9e1119df23de333f5810db7ed6535113eb8210c -size 72263 +oid sha256:f0043c6e7d9043e2460fca709a2596cb3dd9e9f1449e9a840939bddb59e522ec +size 72380 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_drop-shadow_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_drop-shadow_Default.png index cc7588fe4..d943bda90 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_drop-shadow_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_drop-shadow_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb34006faceb18b4fbd3961205973764b6036af538b3fee020cd8cff493153f0 -size 34267 +oid sha256:855227d2fccaf8c2c730e9f3a4c05b023f15147fe848697b5ea4beb830e48380 +size 34353 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_drop-shadow_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_drop-shadow_WebGPU_NativeSurface.png index 510ef5d73..c7ca11bce 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_drop-shadow_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_drop-shadow_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f495e08d10d7053b6cc5aa0f621796ccfcd510a8fb03f3b9c205087c151ddf6d -size 31625 +oid sha256:ff085a1474cb8f9f27684b723ca2b688e2c1eaa8ce64af67b5b061d71bc0be25 +size 31636 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_grayscale_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_grayscale_Default.png index d106f1f7a..449f90cf3 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_grayscale_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_grayscale_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:578b052de461c3a0084ea4b1e3b97801db01f4ec95e9f8adc89cfa74dc421b8b -size 82590 +oid sha256:bfc9c91944c6a854fd923e8771b476ae8032053fa933be453b30f83ec20cafe3 +size 82540 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_grayscale_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_grayscale_WebGPU_NativeSurface.png index 3e411d0d9..fffe0b2ad 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_grayscale_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_grayscale_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f177e925938dce7bd2c7192419c20793abdb4a7bcb46525b4b74916761e3e922 -size 82875 +oid sha256:a8156384bed358b48ce5a307aa9198ea1068781851e3ed4c6724d8ac9e4db072 +size 82807 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_hue-rotate_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_hue-rotate_Default.png index 316f373c8..622a7a247 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_hue-rotate_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_hue-rotate_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:701391f9c868691025c96857cfa9eb0a3df590a95169bafba0b19bc17ea4b47b -size 84664 +oid sha256:061fe1aca0b17c7b4929c598ca0198a41f937ce15afe9a87a28f7fbdbe412bac +size 84615 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_hue-rotate_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_hue-rotate_WebGPU_NativeSurface.png index 64f17c702..7782ee820 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_hue-rotate_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_hue-rotate_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:812085a35695ebece97fe85d3e18cfb8baded84f24534e6270ab9dd848d98ccd -size 84889 +oid sha256:48bc5500707276dd1ca7d4af99b9f32cfe97046f44d1cf11b9f7c9153c9fdb2b +size 84836 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_invert_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_invert_Default.png index 90fd5bb05..0c9f301b3 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_invert_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_invert_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a2537b4d7c153b80f89a0538deb327aed7d12c82b1175f9f2bbfc33ab27a69ca -size 71742 +oid sha256:13c754bcb01f9ef353afac411345bb650586a5a6cdad0e3a0ec1180a79c577e9 +size 71609 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_invert_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_invert_WebGPU_NativeSurface.png index 5979e0c0a..f40ad4ec9 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_invert_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_invert_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:319cf3dca8898dee0dd006894d50d69092a29fab7d50f6b67bd726c3754e7134 -size 71628 +oid sha256:baa93fcf8efc74ffe910e46da53b084af84bea2eed879a96513aad6327e8d61e +size 71603 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_opacity_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_opacity_Default.png index 4f8fa50dd..5bbe080e5 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_opacity_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_opacity_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1b6058ac4dfece7e49bd39f185fe747d223413d0a406114d50f3c36e31e4b0ff -size 99356 +oid sha256:3b970b4fd5fe846e8832bfa76583d9798591c6735894bc3cc34d9da5a464d860 +size 92129 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_opacity_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_opacity_WebGPU_NativeSurface.png index 27789bd26..1fd8aeaef 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_opacity_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_opacity_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0f22a5329320dee31be8b6c95ac5d66483eee92d73d15c6f1291ef6ee293700 -size 99323 +oid sha256:fee95e6799aa9926d8f5ddd8ec0b9c1f7cd657627c00ada2237bb59e09dbd85f +size 92109 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_saturate_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_saturate_Default.png index 17b47fe7b..20387e6c3 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_saturate_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_saturate_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9da62ab6f283d632f4363eb46b3e1cd10826d7c516e9ad68fd1316ade708ddfa -size 82618 +oid sha256:15618715f052814d47e2542d6c5ce0e9571871fb1db0008c70921f996573f4c9 +size 82633 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_saturate_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_saturate_WebGPU_NativeSurface.png index 84000d3d7..d7e13191b 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_saturate_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_saturate_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:59fd15a088d928105d9345b153d6cf85c0e421b5fad474903e80a793e224b7b1 -size 82952 +oid sha256:27eefa621885494c77a41e7a96533641dd815f3a3a25bc94fbfce508b1862c84 +size 82865 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_sepia_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_sepia_Default.png index f60f6a9cd..b52c49811 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_sepia_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_sepia_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ec9ba79f2d8c59040a7ecd4a9a0676094572b21932ef34b47311b65f45a5fad -size 80237 +oid sha256:f12f6ed209f769dd22f60bbd5d00e6f5ea6cbaba4bd290931a72f0e9a9bafd3e +size 80194 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_sepia_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_sepia_WebGPU_NativeSurface.png index 203c190a2..7ea4f2bcc 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_sepia_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_sepia_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d9382f596dc28c6d00b47f9c0caee5a2320c9b32cf0f989ecdfd8907b33957ab -size 79964 +oid sha256:8e1a5c966f8f2c53564ae481983656a8f4f90b524141d047f4395c74e9f7d50a +size 79948 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBlurLayerEffect_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBlurLayerEffect_MatchesDefaultOutput_Default.png index cf3561f86..fc725a002 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBlurLayerEffect_MatchesDefaultOutput_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBlurLayerEffect_MatchesDefaultOutput_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:600d8528434533716f60e0794ee7abd2b84d6c71f1e5a3079fa0083ec4273614 -size 5368 +oid sha256:b60f2a99fc7c9933ce715377e35fe8164f47a763bdf587a6cec275cfc0e2b162 +size 5398 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBlurLayerEffect_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBlurLayerEffect_MatchesDefaultOutput_WebGPU_NativeSurface.png index 74348e7cc..566951491 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBlurLayerEffect_MatchesDefaultOutput_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBlurLayerEffect_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:26dd0fb62b6e07c839dbf964afa152d26f4ed5e4faca43701bba8ab9cbb9684b -size 5402 +oid sha256:52f98faaf87e9072fd1ec7528889fe53abc6b6697d495403f964e02dc6848232 +size 5473 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithColorMatrixLayerEffect_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithColorMatrixLayerEffect_MatchesDefaultOutput_Default.png index 7bc31727e..9444d2f94 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithColorMatrixLayerEffect_MatchesDefaultOutput_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithColorMatrixLayerEffect_MatchesDefaultOutput_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2a19abb7f0e241ccbe0df04025c4b4ef3af66dc93979d5441b5d3e8bd3d753b9 -size 4199 +oid sha256:d8541428564bb068ed2dbc7e918d3f7f3f20ce1e68527e141d65f8cbe12ee76f +size 4175 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithColorMatrixLayerEffect_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithColorMatrixLayerEffect_MatchesDefaultOutput_WebGPU_NativeSurface.png index a7c8108d8..d6899d771 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithColorMatrixLayerEffect_MatchesDefaultOutput_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithColorMatrixLayerEffect_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:277eb95d415d1482fa9a565d1b7f8d201eb6b95d92631fe69649a989e1b192e2 -size 4274 +oid sha256:1fd332092d59baf7b2109e90ee59face210ec4c0e15c131dd8dfad62b2d03507 +size 4226 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithDropShadowWriteBack_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithDropShadowWriteBack_MatchesDefaultOutput_Default.png index 34bfe2631..0b490eea1 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithDropShadowWriteBack_MatchesDefaultOutput_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithDropShadowWriteBack_MatchesDefaultOutput_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a3b0508ede2f0c4aabbce9458921c68c13b4bab2e482b296cf8a55e016fffd4 -size 14597 +oid sha256:253e1e577594d73f6422526d3fde07ea2d5ff87639d0734c03a399c30a20bbaa +size 14625 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithDropShadowWriteBack_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithDropShadowWriteBack_MatchesDefaultOutput_WebGPU_NativeSurface.png index 3f0c84e20..e41071712 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithDropShadowWriteBack_MatchesDefaultOutput_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithDropShadowWriteBack_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9314e8e2dec92b5ec414fb00d4107b362e765c31047c5da100ac57eb90990fa1 -size 14576 +oid sha256:920f8e159fc85305e24a8a491e76de102a170d6ad91f6afccd69c6292853cef6 +size 14567 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithGlowLayerEffect_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithGlowLayerEffect_MatchesDefaultOutput_Default.png index efcedc97e..7eea26de9 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithGlowLayerEffect_MatchesDefaultOutput_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithGlowLayerEffect_MatchesDefaultOutput_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e41e7aebef1a3df5c4c0266ad31a58cc08f84740fbaaefe8dbf8762ae3b34647 -size 9946 +oid sha256:2c7ede84da9cc95a2e5d23f249ead0c859ea066374edf4548bb4659c4865c9f6 +size 10018 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithGlowLayerEffect_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithGlowLayerEffect_MatchesDefaultOutput_WebGPU_NativeSurface.png index 1b355fba5..009b2c014 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithGlowLayerEffect_MatchesDefaultOutput_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithGlowLayerEffect_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5344dbf11c23c15fd803f11aa71b1342341b2ebeb2000dee60939d3153fdbe18 -size 9833 +oid sha256:dc9171e009800d7fb19fbcfee6049885917f581baf012c75bf5557891de7ed37 +size 9981 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithInnerShadowLayerEffect_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithInnerShadowLayerEffect_MatchesDefaultOutput_Default.png index b10afe350..86760559f 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithInnerShadowLayerEffect_MatchesDefaultOutput_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithInnerShadowLayerEffect_MatchesDefaultOutput_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:07d653b76945b0e3d568877896ab07e80ef588751486877acfe0076228370527 -size 7266 +oid sha256:0a123d2277aec0153b2953acc14ff54faf67e175860ea4aae914fefad7283467 +size 7189 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithInnerShadowLayerEffect_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithInnerShadowLayerEffect_MatchesDefaultOutput_WebGPU_NativeSurface.png index 5bb89c1d6..78956efa0 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithInnerShadowLayerEffect_MatchesDefaultOutput_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithInnerShadowLayerEffect_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d1fe6cd31f550af4b0dbdfa1ed8c7407ca55e4b9b9448f589b4fcb3d28be815 -size 7292 +oid sha256:6a56cb4a2a0d7fe9b3eb866b22c2ef75ae7b0c4d0e38b1f1d715e997d1a167ae +size 7306 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithPolygonLayerEffectRegion_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithPolygonLayerEffectRegion_MatchesDefaultOutput_Default.png index 7e4576c9f..49eb798c6 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithPolygonLayerEffectRegion_MatchesDefaultOutput_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithPolygonLayerEffectRegion_MatchesDefaultOutput_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d5c3f2278a3e6871320d503656aa034f2c68ba62f8e7a320590a987aedd8e1d -size 6147 +oid sha256:aef0eba9adf34c6a97d78f4aefc91132be3e82c1dfeadf79d263c1014e6cddc0 +size 6140 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithPolygonLayerEffectRegion_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithPolygonLayerEffectRegion_MatchesDefaultOutput_WebGPU_NativeSurface.png index da14f8469..4d07648ee 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithPolygonLayerEffectRegion_MatchesDefaultOutput_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithPolygonLayerEffectRegion_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f4cc46382ced75767332523251d2950f66f895262b57dc259f4002be7ca24ed6 -size 6199 +oid sha256:b81b9edb4ddab9dc4262b96182d69e27dd810cf590dc1056b7704cc2eb8c81b8 +size 6115 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_Default.png index c4b6f0d95..1b5d146f2 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c61a178a6908cd03a7a436d1e9795449f12f69a2ef27fabea2f799c9e5921e6 -size 10534 +oid sha256:9826c1acc73a0cb357cbe3d6f44e43e0c7ab0fe708d619815656afdf573db03e +size 10431 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_WebGPU_NativeSurface.png index 2bc25c365..c03860f32 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c56ce62d0faf825ffeec92e622e5ba74225f009b35a5257eb079911220e9290 -size 10942 +oid sha256:1fe5eb34980858c883651ac120af1a6810e4bf4aef1a76a94c7b1a88245c2cca +size 10752 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_Default.png index f2e676e84..2835d2423 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a8e36e9f56a6cd19d7f55e42326533a94ec0cfdd577d4d58883bf4474dd6b56b -size 6681 +oid sha256:bbf6734278b7ec25affc9a6f78a9e98948fd2625c61bd402cf91f8982aef13d9 +size 6762 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_WebGPU_NativeSurface.png index 2137643b2..1f79413a3 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f85633558d6e7fd5be3a72ec98eb646e51a28f239fce087bb36330f197f68bdf -size 6713 +oid sha256:eb664bed7ef759a111e214ea685566732c72733a4707f5d791635d8fa344c5e2 +size 6720 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_Default.png index 16b10d5c4..675f97443 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a6b4c172e4dc15d2fdc687c6a350e01750a065bf543a81ad4e09883c6f2b263e -size 32781 +oid sha256:10e82bddab88aa1fcc7f8700cfc3fee54d7c39b5981a1d82df246cce750f6676 +size 32799 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_WebGPU_NativeSurface.png index e1bc818d5..bdc36a98f 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d73f68ae8e559c5fa39f27b4a15466a7e01d802a3cc755b58f062fc8442fc4de -size 33235 +oid sha256:2b3fd5577ab3c9e7ae6c797633437be586db961fb9d544d98dc45c5051fe3473 +size 33189 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_AliasedWithThreshold_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_AliasedWithThreshold_MatchesDefaultOutput_Default.png index 71fe4495e..16f5e05b5 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_AliasedWithThreshold_MatchesDefaultOutput_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_AliasedWithThreshold_MatchesDefaultOutput_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:11a4ff84a5a8a142f076a26b9e0066410ac96aba34bc1916abbdb487ad9eb989 -size 523 +oid sha256:15ac9cac9b631644cb1c12271d5d1c9b9b39b4e582dba43313b7d8e9fb2a783c +size 527 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_AliasedWithThreshold_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_AliasedWithThreshold_MatchesDefaultOutput_WebGPU_NativeSurface.png index 71fe4495e..16f5e05b5 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_AliasedWithThreshold_MatchesDefaultOutput_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_AliasedWithThreshold_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:11a4ff84a5a8a142f076a26b9e0066410ac96aba34bc1916abbdb487ad9eb989 -size 523 +oid sha256:15ac9cac9b631644cb1c12271d5d1c9b9b39b4e582dba43313b7d8e9fb2a783c +size 527 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_LargeTileCount_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_LargeTileCount_MatchesDefaultOutput_Default.png index f580f7344..52886cee9 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_LargeTileCount_MatchesDefaultOutput_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_LargeTileCount_MatchesDefaultOutput_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d6ceb2309554fef908479fb5b2ec5bdcd0c804c0044d770ce97d4e07226ae1c -size 6102 +oid sha256:8346d700f5b8459a6a8b6de9339cd7dfbeb76df391a23d2a333bc46d71fcd015 +size 6075 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_LargeTileCount_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_LargeTileCount_MatchesDefaultOutput_WebGPU_NativeSurface.png index ca1b8301b..3717cdcac 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_LargeTileCount_MatchesDefaultOutput_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_LargeTileCount_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c325ac506f80265d8bd4471327ef3755a2887a7ffd2b856056d4fcd06228c00 -size 5855 +oid sha256:f911a50e488b97b63a91f1fa7d9506ce8dc0263a43095caffa6d7879df40ea56 +size 5832 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_MatchesDefaultOutput_Default.png index c78b9e926..76ea95599 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_MatchesDefaultOutput_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_MatchesDefaultOutput_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e525bc68eba86c53cec4a3539308ba7792738003e754752deb9ac66e2c85777e -size 5461 +oid sha256:fbf9cb4f465ba9daa4775c9ddef85af48095675f332ffa8f33decec4707bd5d5 +size 5429 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_MatchesDefaultOutput_WebGPU_NativeSurface.png index 40a025d84..74e323223 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_MatchesDefaultOutput_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e40c933b0ed69579760861f20277ebb3ede47feb331ba7b7d562f8b6c5a4698 -size 5361 +oid sha256:99dc4c868bdb60b0e6b49822a34f9befd8f436b250b1d0ed98632105e18fa939 +size 5368 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_Default.png index e641bc8ed..c03c56322 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ced44a832fcce053dfd118d7959a7468823c8fb083573bb7d68930f505b0a0dc -size 2085 +oid sha256:1ce686ac157accc5af370ab2f6c2ea6fad9cd9a0e7b2aa443289e80dbbce2422 +size 1977 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_WebGPU_NativeSurface.png index 2d7628f51..d0d40586a 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c5eaef4c95aa86e0655b87d82504b23aa99268501af4603f8ed46cda1b4e5c44 -size 5999 +oid sha256:05539cdc30b7c34109f41f009121e298d8fe1d6cd85c2e57104ece06e2c4987b +size 1952 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_Default.png index e31dc6594..85c90b532 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d53d5e12d37b636245c4950bcd8f451ceda00942786c0d8523ced3d1d1998b66 -size 15267 +oid sha256:a434bc0afff981af2e45f63e21bb96b7833f1adbf329f8cc1133496f625b850f +size 15298 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_WebGPU_NativeSurface.png index 1332891e0..49bd3e15b 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c593a8659089d650f3a2684b2cd8008eedee73d46ac4531647320cfeddc3d72c -size 15827 +oid sha256:37859c68257acc9b1c8254fb0fefb01b574130790e55c0993149fd2f7dc00639 +size 15831 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Default.png index 7769792b3..cfef59771 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cba5e51955bc1fcc91f7a4d5a5b2e094653f8706879cdf8bf34d05492e4a2c22 -size 13041 +oid sha256:857786ae833c0fd7aa43fd2809eea6099afd0808a57b315d1a3416488290db34 +size 13008 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_WebGPU_NativeSurface.png index a83fe538a..78bf28f59 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f0c56dd2fb41eeff013cc8b1ee953583916a0d2b7e2954fe709bc439d998c368 -size 13021 +oid sha256:7baa5c30a0f231ebe17c647dde6e7b2ea5fe3e6cf11d87b5cc23ffa731a1cd5a +size 13090 diff --git a/tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgb24_Full_Aliased.png b/tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgb24_Full_Aliased.png new file mode 100644 index 000000000..1563d8524 --- /dev/null +++ b/tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgb24_Full_Aliased.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b90452455b5829114ff3ba84e2ce5de1f70fa2953a1743718525fb6d3d3c3fe +size 217 diff --git a/tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgb24_Full_Antialiased.png b/tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgb24_Full_Antialiased.png new file mode 100644 index 000000000..06e760463 --- /dev/null +++ b/tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgb24_Full_Antialiased.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05626c19e4ce48da6aea6d92a1ad029b29fbf78c93e26b7bcb26300eb6a269e0 +size 526 diff --git a/tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgb24_None_Antialiased.png b/tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgb24_None_Antialiased.png new file mode 100644 index 000000000..ca87157c6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgb24_None_Antialiased.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:52238dcf60b64d01c672e20eb0b52abaa0cb14a76562cc877396f8f6783d2ba3 +size 943 diff --git a/tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgba32_False.png b/tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgba32_False.png deleted file mode 100644 index adef79dcd..000000000 --- a/tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgba32_False.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:67515a55bc0bed27054d344d430e0ea0ab0ff15a5038b6b33e8f6118f2d84d42 -size 176 diff --git a/tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgba32_True.png b/tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgba32_True.png deleted file mode 100644 index 2217a347a..000000000 --- a/tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgba32_True.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b8e351dc56c0aad3f007c66e0a945934872bed3cbb88da5c60bc05d7e9a11c40 -size 669 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 6c1e976db..9afca6047 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:dd99a270bd7c7a26cbcdee1484604c3e5684addcd299462f745a1f822ad07c1d -size 132521 +oid sha256:08b3c17d2a334bafbdaf614c7c46ac93f4fcb9b5eeb9fe605a048dd93f7dfafe +size 132091 diff --git a/tests/Images/ReferenceOutput/Issue_367/BrushAndTextAlign_Rgba32.png b/tests/Images/ReferenceOutput/Issue_367/BrushAndTextAlign_Rgba32.png index e03ef7bfe..f90517ab9 100644 --- a/tests/Images/ReferenceOutput/Issue_367/BrushAndTextAlign_Rgba32.png +++ b/tests/Images/ReferenceOutput/Issue_367/BrushAndTextAlign_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:793b73e516798b16d39fd81b6b66ffc779b75bc813f3921a41ade715a21950bf -size 8171 +oid sha256:e98c9c4463af4d0fd9c067f5cabad7fb8e2789e4cdd8f78b2887cc102b5fdb76 +size 8216 diff --git a/tests/Images/ReferenceOutput/Issue_397/DrawTextWithIntersectingClip_Difference_IntersectingClip.png b/tests/Images/ReferenceOutput/Issue_397/DrawTextWithIntersectingClip_Difference_IntersectingClip.png index aaf5e25c3..58b315c85 100644 --- a/tests/Images/ReferenceOutput/Issue_397/DrawTextWithIntersectingClip_Difference_IntersectingClip.png +++ b/tests/Images/ReferenceOutput/Issue_397/DrawTextWithIntersectingClip_Difference_IntersectingClip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9bf110aa83053a3849a78e8a633abcb0863572ce2fef3262a3611565c0ce8367 -size 4505 +oid sha256:8ad2cc6db3c251696f6cc0fb5bc93113d74b734fcad67dd1b4f2e3d804bc9b50 +size 4528 diff --git a/tests/Images/ReferenceOutput/Issue_397/DrawTextWithIntersectingClip_Intersection_IntersectingClip.png b/tests/Images/ReferenceOutput/Issue_397/DrawTextWithIntersectingClip_Intersection_IntersectingClip.png index 51021d523..1e38ccd9c 100644 --- a/tests/Images/ReferenceOutput/Issue_397/DrawTextWithIntersectingClip_Intersection_IntersectingClip.png +++ b/tests/Images/ReferenceOutput/Issue_397/DrawTextWithIntersectingClip_Intersection_IntersectingClip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4164bdfd56128859895310bf64638e2822e7f6a8f35ae644a1a2ea1041baafd1 -size 4243 +oid sha256:47292cb9c07898302dbdd5687a412bcbf13a7c5f8cbb5ee9461b754a1daed3e6 +size 4260 diff --git a/tests/Images/ReferenceOutput/Issue_397/DrawTextWithNonIntersectingClip_Difference_NonIntersectingClip.png b/tests/Images/ReferenceOutput/Issue_397/DrawTextWithNonIntersectingClip_Difference_NonIntersectingClip.png index 7e7394fa3..ccdabf93e 100644 --- a/tests/Images/ReferenceOutput/Issue_397/DrawTextWithNonIntersectingClip_Difference_NonIntersectingClip.png +++ b/tests/Images/ReferenceOutput/Issue_397/DrawTextWithNonIntersectingClip_Difference_NonIntersectingClip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e4699a59315ecdbb0994e30876c87b154b046d3caaca92aec5955476b0717417 -size 4614 +oid sha256:a094b1e054ee115fb4442da7b4bb70790be0cedef7a644e809eadb517fc155b4 +size 4619 diff --git a/tests/Images/ReferenceOutput/Issue_397/DrawTextWithNonIntersectingClip_Intersection_NonIntersectingClip.png b/tests/Images/ReferenceOutput/Issue_397/DrawTextWithNonIntersectingClip_Intersection_NonIntersectingClip.png index 69f6fe3ee..567ef2b5a 100644 --- a/tests/Images/ReferenceOutput/Issue_397/DrawTextWithNonIntersectingClip_Intersection_NonIntersectingClip.png +++ b/tests/Images/ReferenceOutput/Issue_397/DrawTextWithNonIntersectingClip_Intersection_NonIntersectingClip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a1c12cf5ed192345e6f43b890c77f5d0c817b2376972874845c0ac1499ba4c82 -size 2713 +oid sha256:c203d177d846b0d2ed5096d3d38c0d04185a2e97d1d65f9f2ed0486214b658ff +size 2684 diff --git a/tests/Images/ReferenceOutput/Issue_405/UnderlineWithTracking_SpansLetterSpacing_Rgba32_Solid800x200_(255,255,255,255).png b/tests/Images/ReferenceOutput/Issue_405/UnderlineWithTracking_SpansLetterSpacing_Rgba32_Solid800x200_(255,255,255,255).png index 3763af40b..27f0fc37c 100644 --- a/tests/Images/ReferenceOutput/Issue_405/UnderlineWithTracking_SpansLetterSpacing_Rgba32_Solid800x200_(255,255,255,255).png +++ b/tests/Images/ReferenceOutput/Issue_405/UnderlineWithTracking_SpansLetterSpacing_Rgba32_Solid800x200_(255,255,255,255).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a40d6dbf9e15baf6b89dd1ec149d0b19523f8c37bcf32acf8e018aaec90a460 -size 1833 +oid sha256:8a02599ac567e880c4ad28cd7c3454813e6672818cb675b4650ca2fc0b5cfb82 +size 1837 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 317ddfe8d..502f73f18 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:e26e47ca1ebf58e474b0d17af56730219e818127bb8b15a3ed9a4a2bb0330eea -size 33413 +oid sha256:da8bff3af82c6d1149bea640e90708da354ab4062dc9e895d882d093c9fe71be +size 33481 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 2a9bce567..ec77bc3a8 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:8879be8652d558996081a543bba5e5f8a5d99ca078d8e5f55950309f36572631 -size 10624 +oid sha256:e72630fdbaa894bc88afad18d1c71fa8dfac0a8b47bae90f06049a37d7ae9e80 +size 10710 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 36372a259..4650ff472 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:9ae0c505ede502283dee17e525a291d4ab555c0bc93263e6d883c39d6acf2aa1 -size 33476 +oid sha256:a2182ed34ca7ccefe6c117302e90b55b1f751857596acbda7e801e223424c994 +size 33498 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 2a9bce567..ec77bc3a8 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:8879be8652d558996081a543bba5e5f8a5d99ca078d8e5f55950309f36572631 -size 10624 +oid sha256:e72630fdbaa894bc88afad18d1c71fa8dfac0a8b47bae90f06049a37d7ae9e80 +size 10710 From f1fa7c7bd1c3960ba00eab0478c5485d7e56b5b6 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 6 Aug 2026 18:29:25 +1000 Subject: [PATCH 2/2] Update WebGPUDrawingBackendTests.cs --- .../Processing/Backends/WebGPUDrawingBackendTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index 16d000a2b..b207070a5 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -973,7 +973,7 @@ void DrawAction(DrawingCanvas canvas) // Reference outputs are rendered on one adapter; other conforming adapters round // within one LSB across a small fraction of pixels. - AssertBackendPairReferenceOutputs(provider, filter, defaultImage, nativeSurfaceImage, 0.0043F); + AssertBackendPairReferenceOutputs(provider, filter, defaultImage, nativeSurfaceImage, 0.0049F); } [WebGPUTheory]