Skip to content

Commit c3d5012

Browse files
Add WebGPU parity for advanced blending
Port the remaining CPU blend and Porter-Duff composition behavior to the WebGPU fine shader, including precise target-format round-tripping, half-float quantization, lossless clip backdrop storage, and support for additional blend/composition modes such as ColorDodge, ColorBurn, SoftLight, Hue, Saturation, Color, Luminosity, and Plus. The scene encoder now defers and specializes solid colors and gradient ramps for the bound target pixel type so GPU brush inputs match CPU snapping. Text rendering also gains proper isolated COLR group lowering, glyph clip propagation, and layered glyph cache replay fixes. Tests and reference outputs were expanded to cover the new blending parity and COLR behavior.
1 parent 1f6ad7e commit c3d5012

535 files changed

Lines changed: 3618 additions & 574 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

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

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -160,15 +160,21 @@ private static ShaderTraits GetTraits(
160160
// The CPU RecolorBrush observes a TPixel already written by earlier draws. A staged GPU
161161
// scene keeps those draws in f32 registers, so Recolor alone must reproduce the target's
162162
// physical storage conversion before comparing without reducing normal composition precision.
163+
// Binary16 quantization must round to nearest even like the CPU renderer's
164+
// float-to-half conversion; pack2x16float and the hardware store may truncate
165+
// toward zero, so the round trip routes through the explicit RTNE helper.
163166
string targetFormatRoundTripBody = textureFormat switch
164167
{
165168
WGPUTextureFormat.RGBA8Unorm or WGPUTextureFormat.BGRA8Unorm => "return unpack4x8unorm(pack4x8unorm(color));",
166169
WGPUTextureFormat.RGBA8Snorm => "return unpack4x8snorm(pack4x8snorm(color));",
167-
WGPUTextureFormat.RGBA16Float => "return vec4<f32>(unpack2x16float(pack2x16float(color.rg)), unpack2x16float(pack2x16float(color.ba)));"
170+
WGPUTextureFormat.RGBA16Float => "return vec4<f32>(quantize_f16_rtne(color.x), quantize_f16_rtne(color.y), quantize_f16_rtne(color.z), quantize_f16_rtne(color.w));"
168171
};
169172

170-
// Fine shading uses associated colors internally. Recolor explicitly crosses the target
171-
// TPixel storage boundary, including associated-alpha rescaling when stored alpha quantizes.
173+
// Fine shading uses associated colors internally. Unassociated targets cross the storage
174+
// boundary with Numerics.UnPremultiply's exact semantics: zero alpha preserves RGB, and a
175+
// true division (not a reciprocal multiply) rounds identically to the CPU renderer.
176+
// Recolor additionally crosses the target TPixel storage boundary, including
177+
// associated-alpha rescaling when stored alpha quantizes.
172178
(string Decode, string Encode, string RecolorNativeToInternal, string RecolorStoreTarget) alphaBodies = alphaRepresentation switch
173179
{
174180
PixelAlphaRepresentation.Associated =>
@@ -180,14 +186,40 @@ private static ShaderTraits GetTraits(
180186
PixelAlphaRepresentation.Unassociated =>
181187
(
182188
"return premul_alpha(decode_numeric(color));",
183-
"let a_inv = select(0.0, 1.0 / color.a, color.a > 0.0);\n return encode_numeric(vec4<f32>(color.rgb * a_inv, color.a));",
189+
"if color.a == 0.0 {\n return encode_numeric(color);\n }\n\n return encode_numeric(vec4<f32>(color.rgb / color.a, color.a));",
184190
"return premul_alpha(color);",
185-
"let a_inv = select(0.0, 1.0 / color.a, color.a > 0.0);\n let native = vec4<f32>(color.rgb * a_inv, color.a);\n return decode_numeric(round_trip_target_format(encode_numeric(native)));")
191+
"if color.a == 0.0 {\n return decode_numeric(round_trip_target_format(encode_numeric(color)));\n }\n\n let native = vec4<f32>(color.rgb / color.a, color.a);\n return decode_numeric(round_trip_target_format(encode_numeric(native)));")
186192
};
187193
#pragma warning restore CS8509, CS8524
188194

189195
string targetConversionFunctions =
190196
$$"""
197+
fn quantize_f16_rtne(value: f32) -> f32 {
198+
// Binary16 quantization with IEEE round-to-nearest-even. pack2x16float and the
199+
// hardware f32-to-f16 store conversion may truncate toward zero, so the two
200+
// bracketing half values are recovered by bit stepping (monotonic for one sign)
201+
// and the nearest is chosen, ties to the even mantissa, matching the CPU
202+
// renderer's float-to-half conversion exactly.
203+
let magnitude = abs(value);
204+
let packed = pack2x16float(vec2(magnitude, 0.0)) & 0xffffu;
205+
let snapped = unpack2x16float(packed).x;
206+
if snapped == magnitude {
207+
return select(snapped, -snapped, value < 0.0);
208+
}
209+
210+
let lower_bits = select(packed, packed - 1u, snapped > magnitude);
211+
let lower = unpack2x16float(lower_bits).x;
212+
let upper = unpack2x16float(lower_bits + 1u).x;
213+
let below = magnitude - lower;
214+
let above = upper - magnitude;
215+
var rounded = lower;
216+
if above < below || (above == below && (lower_bits & 1u) == 1u) {
217+
rounded = upper;
218+
}
219+
220+
return select(rounded, -rounded, value < 0.0);
221+
}
222+
191223
fn decode_numeric(color: vec4<f32>) -> vec4<f32> {
192224
{{numericBodies.Decode}}
193225
}
@@ -220,16 +252,29 @@ fn decode_paint_color(color: vec4<f32>) -> vec4<f32> {
220252
return color;
221253
}
222254
223-
fn pack_clip_color(color: vec4<f32>) -> vec2<u32> {
224-
return vec2<u32>(pack2x16float(color.rg), pack2x16float(color.ba));
255+
fn pack_clip_color(color: vec4<f32>) -> vec4<u32> {
256+
// The clip stack preserves the backdrop across isolated layer pops, where
257+
// per-draw blend modes composite against it. The CPU renderer composes
258+
// against the exact target value, so the save must be bit-lossless:
259+
// binary16 packing here shifts results near storage rounding boundaries.
260+
return bitcast<vec4<u32>>(color);
225261
}
226262
227-
fn unpack_clip_color(color: vec2<u32>) -> vec4<f32> {
228-
return vec4<f32>(unpack2x16float(color.x), unpack2x16float(color.y));
263+
fn unpack_clip_color(color: vec4<u32>) -> vec4<f32> {
264+
return bitcast<vec4<f32>>(color);
229265
}
230266
""";
231267

232-
return new ShaderTraits(compositeTraits.OutputFormat, targetConversionFunctions, "textureStore(output, vec2<i32>(coords), encode_target(rgba[i]));");
268+
// Every store quantizes in-shader first: hardware store conversions carry slack the
269+
// CPU renderer does not (f32-to-f16 may truncate toward zero, float-to-unorm allows
270+
// up to 0.6 ULP of error), while the spec-defined pack builtins and the explicit
271+
// binary16 helper round exactly like the CPU's pixel packing. Passing an exactly
272+
// representable value makes the hardware conversion lossless, so both backends
273+
// store identical pixels.
274+
return new ShaderTraits(
275+
compositeTraits.OutputFormat,
276+
targetConversionFunctions,
277+
"textureStore(output, vec2<i32>(coords), round_trip_target_format(encode_target(rgba[i])));");
233278
}
234279

235280
/// <summary>

0 commit comments

Comments
 (0)