Skip to content

Commit cde991c

Browse files
Add AntialiasThreshold and aliased mode
1 parent a95d910 commit cde991c

27 files changed

Lines changed: 368 additions & 110 deletions

src/ImageSharp.Drawing.WebGPU/Shaders/CompositeComputeShader.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ struct Params {
5151
solid_g: u32,
5252
solid_b: u32,
5353
solid_a: u32,
54+
rasterization_mode: u32,
55+
antialias_threshold: u32,
5456
};
5557
5658
struct DispatchConfig {
@@ -766,7 +768,7 @@ fn rasterize_edge(edge: Edge, band_top: i32, band_left_fixed: i32, clip_top_fixe
766768
rasterize_line(clipped.x0, clipped.y0 - band_top_fixed, clipped.x1, clipped.y1 - band_top_fixed);
767769
}
768770
769-
fn area_to_coverage(area_val: i32, fill_rule: u32) -> f32 {
771+
fn area_to_coverage(area_val: i32, fill_rule: u32, rasterization_mode: u32, antialias_threshold: f32) -> f32 {
770772
let signed_area = area_val >> AREA_SHIFT;
771773
var abs_area: i32;
772774
if signed_area < 0 {
@@ -794,6 +796,14 @@ fn area_to_coverage(area_val: i32, fill_rule: u32) -> f32 {
794796
coverage = f32(wrapped) * COV_SCALE;
795797
}
796798
}
799+
// Aliased mode: snap to binary coverage using threshold
800+
if rasterization_mode == 1u {
801+
if coverage >= antialias_threshold {
802+
coverage = 1.0;
803+
} else {
804+
coverage = 0.0;
805+
}
806+
}
797807
return coverage;
798808
}
799809
@@ -923,7 +933,7 @@ fn cs_main(
923933
cover += atomicLoad(&tile_cover[py * 16u + col]);
924934
}
925935
let area_val = atomicLoad(&tile_area[py * 16u + px]) + (cover << AREA_SHIFT);
926-
let coverage_value = area_to_coverage(area_val, command.fill_rule_value);
936+
let coverage_value = area_to_coverage(area_val, command.fill_rule_value, command.rasterization_mode, u32_to_f32(command.antialias_threshold));
927937
928938
if coverage_value > 0.0 {
929939
let blend_percentage = u32_to_f32(command.blend_percentage);

src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ DrawingCanvasBatcher.Flush()
2323
-> use target texture view directly as backdrop source (no copy)
2424
-> allocate transient output texture for composition bounds
2525
-> deduplicate coverage definitions across batches via CoverageDefinitionIdentity
26+
(keyed by path, interest, intersection rule, rasterization mode, sampling origin, antialias threshold)
2627
-> TryCreateEdgeBuffer (CPU-side edge preparation)
2728
-> for each unique coverage definition:
2829
-> path.Flatten() to iterate flattened vertices
@@ -52,6 +53,7 @@ DrawingCanvasBatcher.Flush()
5253
-> X-range spatial filter: edges left of tile only update start_cover
5354
-> barrier, then each thread accumulates its coverage from shared memory
5455
-> applies fill rule (non-zero or even-odd)
56+
-> if aliased mode: snaps coverage to binary using antialias threshold
5557
-> samples brush (solid color or image texture)
5658
-> composes pixel using Porter-Duff alpha composition + color blend mode
5759
-> writes final pixel to output texture
@@ -87,7 +89,7 @@ Each edge is a 32-byte `GpuEdge` struct (sequential layout):
8789

8890
### Command Parameters
8991

90-
Each `PreparedCompositeParameters` struct contains destination rectangle, edge placement (start, fill rule, CSR offsets start, band count), brush configuration, blend/composition mode, and blend percentage.
92+
Each `PreparedCompositeParameters` struct (26 × u32 = 104 bytes) contains destination rectangle, edge placement (start, fill rule, CSR offsets start, band count), brush configuration, blend/composition mode, blend percentage, rasterization mode (0 = antialiased, 1 = aliased), and antialias threshold (float as u32 bitcast).
9193

9294
### Dispatch Config
9395

src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -162,14 +162,6 @@ public void FillPath<TPixel>(
162162
target.Bounds.Location));
163163
}
164164

165-
/// <inheritdoc />
166-
public bool IsCompositionBrushSupported<TPixel>(Brush brush)
167-
where TPixel : unmanaged, IPixel<TPixel>
168-
{
169-
this.ThrowIfDisposed();
170-
return IsSupportedCompositionBrush(brush);
171-
}
172-
173165
/// <inheritdoc />
174166
public void FlushCompositions<TPixel>(
175167
Configuration configuration,
@@ -733,7 +725,9 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out
733725
(uint)command.GraphicsOptions.ColorBlendingMode,
734726
(uint)command.GraphicsOptions.AlphaCompositionMode,
735727
command.GraphicsOptions.BlendPercentage,
736-
solidColor);
728+
solidColor,
729+
command.GraphicsOptions.Antialias ? 0u : 1u,
730+
command.GraphicsOptions.AntialiasThreshold);
737731

738732
parameters[commandIndex] = commandParameters;
739733
commandIndex++;
@@ -1623,6 +1617,7 @@ private static bool WaitForSignal(WebGPUFlushContext flushContext, ManualResetEv
16231617
private readonly IntersectionRule intersectionRule;
16241618
private readonly RasterizationMode rasterizationMode;
16251619
private readonly RasterizerSamplingOrigin samplingOrigin;
1620+
private readonly float antialiasThreshold;
16261621

16271622
public CoverageDefinitionIdentity(in CompositionCoverageDefinition definition)
16281623
{
@@ -1632,6 +1627,7 @@ public CoverageDefinitionIdentity(in CompositionCoverageDefinition definition)
16321627
this.intersectionRule = definition.RasterizerOptions.IntersectionRule;
16331628
this.rasterizationMode = definition.RasterizerOptions.RasterizationMode;
16341629
this.samplingOrigin = definition.RasterizerOptions.SamplingOrigin;
1630+
this.antialiasThreshold = definition.RasterizerOptions.AntialiasThreshold;
16351631
}
16361632

16371633
/// <summary>
@@ -1645,7 +1641,8 @@ public bool Equals(CoverageDefinitionIdentity other)
16451641
this.interest.Equals(other.interest) &&
16461642
this.intersectionRule == other.intersectionRule &&
16471643
this.rasterizationMode == other.rasterizationMode &&
1648-
this.samplingOrigin == other.samplingOrigin;
1644+
this.samplingOrigin == other.samplingOrigin &&
1645+
this.antialiasThreshold == other.antialiasThreshold;
16491646

16501647
/// <inheritdoc/>
16511648
public override bool Equals(object? obj)
@@ -1659,7 +1656,8 @@ public override int GetHashCode()
16591656
this.interest,
16601657
(int)this.intersectionRule,
16611658
(int)this.rasterizationMode,
1662-
(int)this.samplingOrigin);
1659+
(int)this.samplingOrigin,
1660+
this.antialiasThreshold);
16631661
}
16641662

16651663
private readonly struct EdgePlacement
@@ -1746,7 +1744,7 @@ public PreparedCompositeDispatchConfig(
17461744

17471745
/// <summary>
17481746
/// Prepared composite command parameters consumed by <see cref="CompositeComputeShader"/>.
1749-
/// Layout matches the WGSL <c>Params</c> struct exactly (24 u32 fields = 96 bytes).
1747+
/// Layout matches the WGSL <c>Params</c> struct exactly (26 u32 fields = 104 bytes).
17501748
/// </summary>
17511749
[StructLayout(LayoutKind.Sequential)]
17521750
private readonly struct PreparedCompositeParameters
@@ -1775,6 +1773,8 @@ private readonly struct PreparedCompositeParameters
17751773
public readonly uint SolidG;
17761774
public readonly uint SolidB;
17771775
public readonly uint SolidA;
1776+
public readonly uint RasterizationMode;
1777+
public readonly uint AntialiasThreshold;
17781778

17791779
public PreparedCompositeParameters(
17801780
int destinationX,
@@ -1797,7 +1797,9 @@ public PreparedCompositeParameters(
17971797
uint colorBlendMode,
17981798
uint alphaCompositionMode,
17991799
float blendPercentage,
1800-
Vector4 solidColor)
1800+
Vector4 solidColor,
1801+
uint rasterizationMode,
1802+
float antialiasThreshold)
18011803
{
18021804
this.DestinationX = (uint)destinationX;
18031805
this.DestinationY = (uint)destinationY;
@@ -1823,6 +1825,8 @@ public PreparedCompositeParameters(
18231825
this.SolidG = FloatToUInt32Bits(solidColor.Y);
18241826
this.SolidB = FloatToUInt32Bits(solidColor.Z);
18251827
this.SolidA = FloatToUInt32Bits(solidColor.W);
1828+
this.RasterizationMode = rasterizationMode;
1829+
this.AntialiasThreshold = FloatToUInt32Bits(antialiasThreshold);
18261830
}
18271831
}
18281832
}

src/ImageSharp.Drawing/ImageSharp.Drawing.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,11 @@
5050
<None Include="..\..\shared-infrastructure\branding\icons\imagesharp.drawing\sixlabors.imagesharp.drawing.128.png" Pack="true" PackagePath="" />
5151
</ItemGroup>
5252
<ItemGroup>
53-
<PackageReference Include="SixLabors.Fonts" Version="3.0.0-alpha.0.19" />
54-
<PackageReference Include="SixLabors.ImageSharp" Version="4.0.0-alpha.0.72" />
53+
<PackageReference Include="SixLabors.Fonts" Version="3.0.0-alpha.0.23" />
54+
<PackageReference Include="SixLabors.ImageSharp" Version="4.0.0-alpha.0.78" />
5555
<PackageReference Include="SixLabors.PolygonClipper" Version="1.0.0-alpha.0.52" />
5656
</ItemGroup>
57-
57+
5858
<Import Project="..\..\shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems" Label="Shared" />
5959

6060
</Project>

src/ImageSharp.Drawing/PathBuilder.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,7 @@ public PathBuilder Reset()
465465
/// <summary>
466466
/// Clears all drawn paths, Leaving any applied transforms.
467467
/// </summary>
468-
[MemberNotNull(nameof(this.currentFigure))]
468+
[MemberNotNull(nameof(currentFigure))]
469469
public void Clear()
470470
{
471471
this.currentFigure = new Figure();
@@ -485,7 +485,7 @@ private class Figure
485485

486486
public IPath Build()
487487
=> this.IsClosed
488-
? new Polygon(this.segments.ToArray(), true)
488+
? new Polygon([.. this.segments], true)
489489
: new Path(this.segments.ToArray());
490490
}
491491
}

src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,6 @@ internal sealed class DefaultDrawingBackend : IDrawingBackend
3838
/// </summary>
3939
public static DefaultDrawingBackend Instance { get; } = new();
4040

41-
/// <inheritdoc />
42-
public bool IsCompositionBrushSupported<TPixel>(Brush brush)
43-
where TPixel : unmanaged, IPixel<TPixel>
44-
=> true;
45-
4641
/// <inheritdoc />
4742
public void FillPath<TPixel>(
4843
ICanvasFrame<TPixel> target,

src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ private static void RasterizeCoreRows(
182182
maxBandRows,
183183
options.IntersectionRule,
184184
options.RasterizationMode,
185+
options.AntialiasThreshold,
185186
allocator,
186187
rowHandler,
187188
ref reusableScratch))
@@ -199,6 +200,7 @@ private static void RasterizeCoreRows(
199200
maxBandRows,
200201
options.IntersectionRule,
201202
options.RasterizationMode,
203+
options.AntialiasThreshold,
202204
allocator,
203205
rowHandler,
204206
ref reusableScratch);
@@ -216,6 +218,9 @@ private static void RasterizeCoreRows(
216218
/// <param name="maxBandRows">Maximum rows per reusable scratch band.</param>
217219
/// <param name="intersectionRule">Fill rule.</param>
218220
/// <param name="rasterizationMode">Coverage mode (AA or aliased).</param>
221+
/// <param name="antialiasThreshold">
222+
/// Antialiasing threshold in [0, 1] when <paramref name="rasterizationMode"/> is AA.
223+
/// </param>
219224
/// <param name="allocator">Temporary buffer allocator.</param>
220225
/// <param name="rowHandler">Coverage row callback invoked once per emitted row.</param>
221226
/// <param name="reusableScratch">
@@ -232,6 +237,7 @@ private static void RasterizeSequentialBands(
232237
int maxBandRows,
233238
IntersectionRule intersectionRule,
234239
RasterizationMode rasterizationMode,
240+
float antialiasThreshold,
235241
MemoryAllocator allocator,
236242
RasterizerCoverageRowHandler rowHandler,
237243
ref WorkerScratch? reusableScratch)
@@ -280,7 +286,7 @@ private static void RasterizeSequentialBands(
280286
continue;
281287
}
282288

283-
Context context = scratch.CreateContext(currentBandHeight, intersectionRule, rasterizationMode);
289+
Context context = scratch.CreateContext(currentBandHeight, intersectionRule, rasterizationMode, antialiasThreshold);
284290
context.RasterizeEdgeTable(sortedEdges.Slice(start, length), bandTop);
285291
context.EmitCoverageRows(interestTop + bandTop, scratch.Scanline, rowHandler);
286292
context.ResetTouchedRows();
@@ -301,6 +307,9 @@ private static void RasterizeSequentialBands(
301307
/// <param name="maxBandRows">Maximum rows per worker scratch context.</param>
302308
/// <param name="intersectionRule">Fill rule.</param>
303309
/// <param name="rasterizationMode">Coverage mode (AA or aliased).</param>
310+
/// <param name="antialiasThreshold">
311+
/// Antialiasing threshold in [0, 1] when <paramref name="rasterizationMode"/> is AA.
312+
/// </param>
304313
/// <param name="allocator">Temporary buffer allocator.</param>
305314
/// <param name="rowHandler">Coverage row callback invoked once per emitted row.</param>
306315
/// <param name="reusableScratch">Caller-managed scratch. Reused when compatible; replaced and updated in place otherwise.</param>
@@ -319,6 +328,7 @@ private static bool TryRasterizeParallel(
319328
int maxBandRows,
320329
IntersectionRule intersectionRule,
321330
RasterizationMode rasterizationMode,
331+
float antialiasThreshold,
322332
MemoryAllocator allocator,
323333
RasterizerCoverageRowHandler rowHandler,
324334
ref WorkerScratch? reusableScratch)
@@ -344,6 +354,7 @@ private static bool TryRasterizeParallel(
344354
coverStride,
345355
intersectionRule,
346356
rasterizationMode,
357+
antialiasThreshold,
347358
allocator,
348359
rowHandler,
349360
ref reusableScratch);
@@ -398,7 +409,7 @@ private static bool TryRasterizeParallel(
398409
if (length > 0)
399410
{
400411
ReadOnlySpan<EdgeData> tileEdges = sortedEdgesMemory.Span.Slice(start, length);
401-
context = worker.CreateContext(bandHeight, intersectionRule, rasterizationMode);
412+
context = worker.CreateContext(bandHeight, intersectionRule, rasterizationMode, antialiasThreshold);
402413
context.RasterizeEdgeTable(tileEdges, bandTop);
403414
hasCoverage = true;
404415
context.EmitCoverageRows(interestTop + bandTop, worker.Scanline, rowHandler);
@@ -435,6 +446,9 @@ private static bool TryRasterizeParallel(
435446
/// <param name="coverStride">Cover-area stride in ints.</param>
436447
/// <param name="intersectionRule">Fill rule.</param>
437448
/// <param name="rasterizationMode">Coverage mode (AA or aliased).</param>
449+
/// <param name="antialiasThreshold">
450+
/// Antialiasing threshold in [0, 1] when <paramref name="rasterizationMode"/> is AA.
451+
/// </param>
438452
/// <param name="allocator">Temporary buffer allocator.</param>
439453
/// <param name="rowHandler">Coverage row callback invoked once per emitted row.</param>
440454
/// <param name="reusableScratch">
@@ -450,6 +464,7 @@ private static void RasterizeSingleTileDirect(
450464
int coverStride,
451465
IntersectionRule intersectionRule,
452466
RasterizationMode rasterizationMode,
467+
float antialiasThreshold,
453468
MemoryAllocator allocator,
454469
RasterizerCoverageRowHandler rowHandler,
455470
ref WorkerScratch? reusableScratch)
@@ -462,7 +477,7 @@ private static void RasterizeSingleTileDirect(
462477
}
463478

464479
WorkerScratch scratch = reusableScratch;
465-
Context context = scratch.CreateContext(height, intersectionRule, rasterizationMode);
480+
Context context = scratch.CreateContext(height, intersectionRule, rasterizationMode, antialiasThreshold);
466481
context.RasterizeEdgeTable(edges, bandTop: 0);
467482
context.EmitCoverageRows(interestTop, scratch.Scanline, rowHandler);
468483
context.ResetTouchedRows();
@@ -865,6 +880,7 @@ internal ref struct Context
865880
private readonly int coverStride;
866881
private readonly IntersectionRule intersectionRule;
867882
private readonly RasterizationMode rasterizationMode;
883+
private readonly float antialiasThreshold;
868884
private int touchedRowCount;
869885

870886
/// <summary>
@@ -884,7 +900,8 @@ public Context(
884900
int wordsPerRow,
885901
int coverStride,
886902
IntersectionRule intersectionRule,
887-
RasterizationMode rasterizationMode)
903+
RasterizationMode rasterizationMode,
904+
float antialiasThreshold)
888905
{
889906
this.bitVectors = bitVectors;
890907
this.coverArea = coverArea;
@@ -900,6 +917,7 @@ public Context(
900917
this.coverStride = coverStride;
901918
this.intersectionRule = intersectionRule;
902919
this.rasterizationMode = rasterizationMode;
920+
this.antialiasThreshold = antialiasThreshold;
903921
this.touchedRowCount = 0;
904922
}
905923

@@ -1252,8 +1270,9 @@ private readonly float AreaToCoverage(int area)
12521270

12531271
if (this.rasterizationMode == RasterizationMode.Aliased)
12541272
{
1255-
// Aliased mode quantizes final coverage to hard 0/1 per pixel.
1256-
return coverage >= 0.5F ? 1F : 0F;
1273+
// Aliased mode quantizes final coverage to hard 0/1 per pixel
1274+
// using the configurable threshold from GraphicsOptions.AntialiasThreshold.
1275+
return coverage >= this.antialiasThreshold ? 1F : 0F;
12571276
}
12581277

12591278
return coverage;
@@ -2156,7 +2175,7 @@ public static WorkerScratch Create(MemoryAllocator allocator, int wordsPerRow, i
21562175
/// <summary>
21572176
/// Creates a context view over this scratch for the requested band height.
21582177
/// </summary>
2159-
public Context CreateContext(int bandHeight, IntersectionRule intersectionRule, RasterizationMode rasterizationMode)
2178+
public Context CreateContext(int bandHeight, IntersectionRule intersectionRule, RasterizationMode rasterizationMode, float antialiasThreshold)
21602179
{
21612180
if ((uint)bandHeight > (uint)this.tileCapacity)
21622181
{
@@ -2179,7 +2198,8 @@ public Context CreateContext(int bandHeight, IntersectionRule intersectionRule,
21792198
this.wordsPerRow,
21802199
this.coverStride,
21812200
intersectionRule,
2182-
rasterizationMode);
2201+
rasterizationMode,
2202+
antialiasThreshold);
21832203
}
21842204

21852205
/// <summary>

src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,6 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends;
1414
/// </remarks>
1515
internal interface IDrawingBackend
1616
{
17-
/// <summary>
18-
/// Determines whether the backend can compose the provided brush type directly for <typeparamref name="TPixel"/>.
19-
/// </summary>
20-
/// <typeparam name="TPixel">The destination pixel format.</typeparam>
21-
/// <param name="brush">The brush used by a pending composition command.</param>
22-
/// <returns>
23-
/// <see langword="true"/> when the backend can compose the brush directly; otherwise <see langword="false"/>.
24-
/// </returns>
25-
public bool IsCompositionBrushSupported<TPixel>(Brush brush)
26-
where TPixel : unmanaged, IPixel<TPixel>;
27-
2817
/// <summary>
2918
/// Fills a path into a destination target region.
3019
/// </summary>

0 commit comments

Comments
 (0)