Skip to content

Commit b86b71f

Browse files
Merge pull request #412 from SixLabors/js/colrv1-composite-groups
COLRv1 group rendering, zero-callback layered glyph cache, WebGPU blend parity and CPU-exact brush colors
2 parents 1f6ad7e + c3d5012 commit b86b71f

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)