Skip to content

Commit bd8d170

Browse files
Add aliased fine-pass threshold shader and plumbing
1 parent b29a440 commit bd8d170

5 files changed

Lines changed: 275 additions & 14 deletions

File tree

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using System.Text;
5+
using Silk.NET.WebGPU;
6+
7+
namespace SixLabors.ImageSharp.Drawing.Processing.Backends;
8+
9+
/// <summary>
10+
/// Final staged-scene fine pass for thresholded aliased rasterization.
11+
/// The WGSL stays shared with the analytic fine pass, but coverage is quantized
12+
/// immediately after path evaluation so brush evaluation and composition still
13+
/// reuse the same scene encoding and blend logic.
14+
/// </summary>
15+
internal static class FineAliasedThresholdComputeShader
16+
{
17+
private const string OutputBindingMarker = "var output: texture_storage_2d<rgba8unorm, write>;";
18+
private const string OutputStoreMarker = "textureStore(output, vec2<i32>(coords), rgba_sep);";
19+
private const string PremulAlphaMarker = "fn premul_alpha(rgba: vec4<f32>) -> vec4<f32> {";
20+
private const string WorkgroupSizeMarker = "// The X size should be 16 / PIXELS_PER_THREAD";
21+
private const string AnalyticFillCallMarker = " fill_path(fill, local_xy, &area);";
22+
private const string MsaaFillCallMarker = " fill_path_ms(fill, local_id.xy, &area);";
23+
private const string ThresholdHelper =
24+
"""
25+
fn apply_aliased_threshold(result: ptr<function, array<f32, PIXELS_PER_THREAD>>) {
26+
let threshold = config.fine_coverage_threshold;
27+
for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) {
28+
(*result)[i] = select(0.0, 1.0, (*result)[i] >= threshold);
29+
}
30+
}
31+
32+
""";
33+
34+
private static readonly object CacheSync = new();
35+
private static readonly Dictionary<TextureFormat, byte[]> ShaderCache = [];
36+
37+
/// <summary>
38+
/// Gets the compute entry point exported by the aliased fine shader.
39+
/// </summary>
40+
public static ReadOnlySpan<byte> EntryPoint => "main\0"u8;
41+
42+
/// <summary>
43+
/// Gets the texture-format-specialized WGSL for the aliased threshold fine pass.
44+
/// </summary>
45+
public static bool TryGetCode(TextureFormat textureFormat, out byte[] code, out string? error)
46+
{
47+
if (!TryGetTraits(textureFormat, out ShaderTraits traits))
48+
{
49+
code = [];
50+
error = $"Scene aliased-threshold fine shader does not support texture format '{textureFormat}'.";
51+
return false;
52+
}
53+
54+
lock (CacheSync)
55+
{
56+
if (ShaderCache.TryGetValue(textureFormat, out byte[]? cachedCode))
57+
{
58+
code = cachedCode;
59+
error = null;
60+
return true;
61+
}
62+
63+
string source = GeneratedWgslShaderSources.FineText;
64+
source = source.Replace(OutputBindingMarker, $"var output: texture_storage_2d<{traits.OutputFormat}, write>;", StringComparison.Ordinal);
65+
source = source.Replace(OutputStoreMarker, traits.StoreOutputStatement, StringComparison.Ordinal);
66+
source = source.Replace(PremulAlphaMarker, $"{traits.EncodeOutputFunction}\n\n{PremulAlphaMarker}", StringComparison.Ordinal);
67+
source = source.Replace(WorkgroupSizeMarker, $"{ThresholdHelper}{WorkgroupSizeMarker}", StringComparison.Ordinal);
68+
source = source.Replace(AnalyticFillCallMarker, $"{AnalyticFillCallMarker}\n apply_aliased_threshold(&area);", StringComparison.Ordinal);
69+
source = source.Replace(MsaaFillCallMarker, $"{MsaaFillCallMarker}\n apply_aliased_threshold(&area);", StringComparison.Ordinal);
70+
71+
int byteCount = Encoding.UTF8.GetByteCount(source);
72+
code = new byte[byteCount + 1];
73+
_ = Encoding.UTF8.GetBytes(source, code);
74+
code[^1] = 0;
75+
ShaderCache[textureFormat] = code;
76+
}
77+
78+
error = null;
79+
return true;
80+
}
81+
82+
/// <summary>
83+
/// Creates the bind-group layout consumed by the aliased threshold fine pass.
84+
/// </summary>
85+
public static unsafe bool TryCreateBindGroupLayout(
86+
WebGPU api,
87+
Device* device,
88+
TextureFormat outputTextureFormat,
89+
out BindGroupLayout* layout,
90+
out string? error)
91+
=> FineAreaComputeShader.TryCreateBindGroupLayout(api, device, outputTextureFormat, out layout, out error);
92+
93+
private static bool TryGetTraits(TextureFormat textureFormat, out ShaderTraits traits)
94+
{
95+
if (!WebGPUDrawingBackend.TryGetCompositeTextureShaderTraits(textureFormat, out WebGPUDrawingBackend.CompositeTextureShaderTraits compositeTraits))
96+
{
97+
traits = default;
98+
return false;
99+
}
100+
101+
traits = compositeTraits.EncodingKind switch
102+
{
103+
WebGPUDrawingBackend.CompositeTextureEncodingKind.Float => CreateFloatTraits(compositeTraits.OutputFormat),
104+
WebGPUDrawingBackend.CompositeTextureEncodingKind.Snorm => CreateSnormTraits(compositeTraits.OutputFormat),
105+
WebGPUDrawingBackend.CompositeTextureEncodingKind.Uint8 => CreateUintTraits(compositeTraits.OutputFormat, 255F),
106+
WebGPUDrawingBackend.CompositeTextureEncodingKind.Uint16 => CreateUintTraits(compositeTraits.OutputFormat, 65535F),
107+
WebGPUDrawingBackend.CompositeTextureEncodingKind.Sint16 => CreateSintTraits(compositeTraits.OutputFormat, -32768F, 32767F),
108+
_ => default
109+
};
110+
111+
return true;
112+
}
113+
114+
private static ShaderTraits CreateFloatTraits(string outputFormat)
115+
{
116+
const string encodeOutput =
117+
"""
118+
fn encode_output(color: vec4<f32>) -> vec4<f32> {
119+
return color;
120+
}
121+
""";
122+
123+
return new ShaderTraits(
124+
outputFormat,
125+
encodeOutput,
126+
"textureStore(output, vec2<i32>(coords), encode_output(rgba_sep));");
127+
}
128+
129+
private static ShaderTraits CreateSnormTraits(string outputFormat)
130+
{
131+
const string encodeOutput =
132+
"""
133+
fn encode_output(color: vec4<f32>) -> vec4<f32> {
134+
let clamped = clamp(color, vec4<f32>(0.0), vec4<f32>(1.0));
135+
return (clamped * 2.0) - vec4<f32>(1.0);
136+
}
137+
""";
138+
139+
return new ShaderTraits(
140+
outputFormat,
141+
encodeOutput,
142+
"textureStore(output, vec2<i32>(coords), encode_output(rgba_sep));");
143+
}
144+
145+
private static ShaderTraits CreateUintTraits(string outputFormat, float maxValue)
146+
{
147+
string maxVector = $"vec4<f32>({maxValue:F1}, {maxValue:F1}, {maxValue:F1}, {maxValue:F1})";
148+
const string encodeOutput =
149+
"""
150+
const UINT_TEXEL_MAX: vec4<f32> = __UINT_TEXEL_MAX__;
151+
fn encode_output(color: vec4<f32>) -> vec4<u32> {
152+
let clamped = clamp(color, vec4<f32>(0.0), vec4<f32>(1.0));
153+
return vec4<u32>(round(clamped * UINT_TEXEL_MAX));
154+
}
155+
""";
156+
157+
return new ShaderTraits(
158+
outputFormat,
159+
encodeOutput.Replace("__UINT_TEXEL_MAX__", maxVector, StringComparison.Ordinal),
160+
"textureStore(output, vec2<i32>(coords), encode_output(rgba_sep));");
161+
}
162+
163+
private static ShaderTraits CreateSintTraits(string outputFormat, float minValue, float maxValue)
164+
{
165+
string minVector = $"vec4<f32>({minValue:F1}, {minValue:F1}, {minValue:F1}, {minValue:F1})";
166+
string maxVector = $"vec4<f32>({maxValue:F1}, {maxValue:F1}, {maxValue:F1}, {maxValue:F1})";
167+
string encodeOutput =
168+
$$"""
169+
const SINT_TEXEL_MIN: vec4<f32> = {{minVector}};
170+
const SINT_TEXEL_MAX: vec4<f32> = {{maxVector}};
171+
const SINT_TEXEL_RANGE: vec4<f32> = SINT_TEXEL_MAX - SINT_TEXEL_MIN;
172+
fn encode_output(color: vec4<f32>) -> vec4<i32> {
173+
let clamped = clamp(color, vec4<f32>(0.0), vec4<f32>(1.0));
174+
return vec4<i32>(round((clamped * SINT_TEXEL_RANGE) + SINT_TEXEL_MIN));
175+
}
176+
""";
177+
178+
return new ShaderTraits(
179+
outputFormat,
180+
encodeOutput,
181+
"textureStore(output, vec2<i32>(coords), encode_output(rgba_sep));");
182+
}
183+
184+
private readonly struct ShaderTraits(
185+
string outputFormat,
186+
string encodeOutputFunction,
187+
string storeOutputStatement)
188+
{
189+
public string OutputFormat { get; } = outputFormat;
190+
191+
public string EncodeOutputFunction { get; } = encodeOutputFunction;
192+
193+
public string StoreOutputStatement { get; } = storeOutputStatement;
194+
}
195+
}

src/ImageSharp.Drawing.WebGPU/Shaders/Shared/config.wgsl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ struct Config {
3939
segments_size: u32,
4040
blend_size: u32,
4141
ptcl_size: u32,
42+
fine_coverage_threshold: f32,
4243
}
4344

4445
// Geometry of tiles and bins

src/ImageSharp.Drawing.WebGPU/WebGPUSceneDispatch.cs

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ internal static class WebGPUSceneDispatch
4040
private const string PathTilingSetupPipelineKey = "scene/path-tiling-setup";
4141
private const string PathTilingPipelineKey = "scene/path-tiling";
4242
private const string FineAreaPipelineKey = "scene/fine-area";
43+
private const string FineAliasedThresholdPipelineKey = "scene/fine-aliased-threshold";
4344

4445
/// <summary>
4546
/// Builds the flush-scoped encoded scene and uploads its GPU resources.
@@ -1192,23 +1193,39 @@ private static unsafe bool TryDispatchFineArea(
11921193
uint groupCountY,
11931194
out string? error)
11941195
{
1195-
if (!FineAreaComputeShader.TryGetCode(flushContext.TextureFormat, out byte[] shaderCode, out error))
1196+
bool useAliasedThreshold = encodedScene.FineRasterizationMode == RasterizationMode.Aliased;
1197+
byte[] shaderCode;
1198+
if (useAliasedThreshold)
1199+
{
1200+
if (!FineAliasedThresholdComputeShader.TryGetCode(flushContext.TextureFormat, out shaderCode, out error))
1201+
{
1202+
return false;
1203+
}
1204+
}
1205+
else if (!FineAreaComputeShader.TryGetCode(flushContext.TextureFormat, out shaderCode, out error))
11961206
{
11971207
return false;
11981208
}
11991209

12001210
bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out string? layoutError)
1201-
=> FineAreaComputeShader.TryCreateBindGroupLayout(
1202-
api,
1203-
device,
1204-
flushContext.TextureFormat,
1205-
out layout,
1206-
out layoutError);
1211+
=> useAliasedThreshold
1212+
? FineAliasedThresholdComputeShader.TryCreateBindGroupLayout(
1213+
api,
1214+
device,
1215+
flushContext.TextureFormat,
1216+
out layout,
1217+
out layoutError)
1218+
: FineAreaComputeShader.TryCreateBindGroupLayout(
1219+
api,
1220+
device,
1221+
flushContext.TextureFormat,
1222+
out layout,
1223+
out layoutError);
12071224

12081225
if (!flushContext.DeviceState.TryGetOrCreateCompositeComputePipeline(
1209-
$"{FineAreaPipelineKey}/{flushContext.TextureFormat}",
1226+
$"{(useAliasedThreshold ? FineAliasedThresholdPipelineKey : FineAreaPipelineKey)}/{flushContext.TextureFormat}",
12101227
shaderCode,
1211-
FineAreaComputeShader.EntryPoint,
1228+
useAliasedThreshold ? FineAliasedThresholdComputeShader.EntryPoint : FineAreaComputeShader.EntryPoint,
12121229
LayoutFactory,
12131230
out BindGroupLayout* bindGroupLayout,
12141231
out ComputePipeline* pipeline,

src/ImageSharp.Drawing.WebGPU/WebGPUSceneEncoder.cs

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ private ref struct SupportedSubsetSceneEncoding
100100
private bool gradientPixelsDetached;
101101
private uint lastStyle0;
102102
private uint lastStyle1;
103+
private bool hasFineRasterizationMode;
103104
private readonly Rectangle rootTargetBounds;
104105
private List<Rectangle>? openLayerBounds;
105106

@@ -130,8 +131,11 @@ private SupportedSubsetSceneEncoding(MemoryAllocator allocator, int commandCount
130131
this.gradientPixelsDetached = false;
131132
this.lastStyle0 = 0;
132133
this.lastStyle1 = 0;
134+
this.hasFineRasterizationMode = false;
133135
this.rootTargetBounds = rootTargetBounds;
134136
this.openLayerBounds = null;
137+
this.FineRasterizationMode = RasterizationMode.Antialiased;
138+
this.FineCoverageThreshold = 0F;
135139

136140
this.PathTags.Add(PathTagTransform);
137141
AppendIdentityTransform(ref this.Transforms);
@@ -212,6 +216,16 @@ private SupportedSubsetSceneEncoding(MemoryAllocator allocator, int commandCount
212216
/// </summary>
213217
public int GradientRowCount { get; private set; }
214218

219+
/// <summary>
220+
/// Gets the flush-wide fine rasterization mode selected while encoding visible fills.
221+
/// </summary>
222+
public RasterizationMode FineRasterizationMode { get; private set; }
223+
224+
/// <summary>
225+
/// Gets the aliased coverage threshold consumed by the fine pass when aliased mode is selected.
226+
/// </summary>
227+
public float FineCoverageThreshold { get; private set; }
228+
215229
/// <summary>
216230
/// Gets a value indicating whether the encoding produced no fill work.
217231
/// </summary>
@@ -277,6 +291,14 @@ private void Append(in CompositionCommand command)
277291
return;
278292
}
279293

294+
RasterizerOptions options = command.RasterizerOptions;
295+
if (!this.hasFineRasterizationMode)
296+
{
297+
this.hasFineRasterizationMode = true;
298+
this.FineRasterizationMode = options.RasterizationMode;
299+
this.FineCoverageThreshold = options.AntialiasThreshold;
300+
}
301+
280302
IPath preparedPath = command.PreparedPath!;
281303
this.AppendPlainFill(command, preparedPath);
282304
return;
@@ -515,7 +537,9 @@ public static WebGPUEncodedScene Resolve(
515537
encoding.TotalTileMembershipCount,
516538
0,
517539
DivideRoundUp(targetBounds.Width, TileWidth),
518-
DivideRoundUp(targetBounds.Height, TileHeight));
540+
DivideRoundUp(targetBounds.Height, TileHeight),
541+
encoding.FineRasterizationMode,
542+
encoding.FineCoverageThreshold);
519543
}
520544
catch
521545
{
@@ -1308,7 +1332,9 @@ internal sealed class WebGPUEncodedScene : IDisposable
13081332
0,
13091333
0,
13101334
0,
1311-
0);
1335+
0,
1336+
RasterizationMode.Antialiased,
1337+
0F);
13121338

13131339
private readonly IMemoryOwner<uint>? sceneDataOwner;
13141340
private readonly IMemoryOwner<uint>? gradientPixelsOwner;
@@ -1343,7 +1369,9 @@ public WebGPUEncodedScene(
13431369
int totalTileMembershipCount,
13441370
int totalLineSliceCount,
13451371
int tileCountX,
1346-
int tileCountY)
1372+
int tileCountY,
1373+
RasterizationMode fineRasterizationMode,
1374+
float fineCoverageThreshold)
13471375
{
13481376
this.TargetSize = targetSize;
13491377
this.InfoWordCount = infoWordCount;
@@ -1370,6 +1398,8 @@ public WebGPUEncodedScene(
13701398
this.TotalLineSliceCount = totalLineSliceCount;
13711399
this.TileCountX = tileCountX;
13721400
this.TileCountY = tileCountY;
1401+
this.FineRasterizationMode = fineRasterizationMode;
1402+
this.FineCoverageThreshold = fineCoverageThreshold;
13731403
}
13741404

13751405
/// <summary>
@@ -1494,6 +1524,16 @@ public ReadOnlyMemory<uint> GradientPixels
14941524
/// </summary>
14951525
public int TileCountY { get; }
14961526

1527+
/// <summary>
1528+
/// Gets the fine-pass rasterization mode selected for this flush.
1529+
/// </summary>
1530+
public RasterizationMode FineRasterizationMode { get; }
1531+
1532+
/// <summary>
1533+
/// Gets the scene-wide aliased coverage threshold consumed by the fine pass.
1534+
/// </summary>
1535+
public float FineCoverageThreshold { get; }
1536+
14971537
/// <summary>
14981538
/// Gets the total tile count.
14991539
/// </summary>

0 commit comments

Comments
 (0)