demo · v130
subpixel text shader
The canonical use case for dual-source blending: LCD subpixel-AA text rendering. The fragment shader emits one colour for the glyph and a second per-channel coverage mask. The blend equation uses both. Without dual-source blending you can pick monochrome AA (which looks soft on LCDs) or pre-multiplied colour-but-no-coverage; with it, you get the true Quartz / GDI / FreeType subpixel rendering pipeline.
requires Chrome 130+ and a GPU adapter with "dual-source-blending"
The probe below requests
requiredFeatures: ["dual-source-blending"]. On adapters that don't support it (some integrated GPUs), the after canvas shows monochrome AA so you can compare against the legacy path.
single-source blend (monochrome AA)
One coverage value per pixel — same for R, G, B. Soft and grey on the edges.
dual-source blend (subpixel AA)
Three independent coverage values, one per LCD subpixel. Crisper edges; tiny red/blue fringes resolved by the display.
the WGSL
// Dual-source fragment shader — Chrome 130 + dual-source-blending feature.
struct FragOut {
@location(0) @blend_src(0) color : vec4<f32>, // glyph colour
@location(0) @blend_src(1) coverage : vec4<f32>, // per-channel coverage
};
@fragment fn fs(@location(0) uv : vec2<f32>) -> FragOut {
let r = sampleCoverage(uv + vec2(-1.0/3.0, 0.0) / texSize);
let g = sampleCoverage(uv);
let b = sampleCoverage(uv + vec2( 1.0/3.0, 0.0) / texSize);
var o : FragOut;
o.color = vec4(textColor.rgb, 1.0);
o.coverage = vec4(r, g, b, max(r, max(g, b)));
return o;
}
// Pipeline state must specify both blend factors per channel.
const pipeline = device.createRenderPipeline({
fragment: {
module, entryPoint: "fs",
targets: [{
format,
blend: {
color: { srcFactor: "src1", dstFactor: "one-minus-src1", operation: "add" },
alpha: { srcFactor: "src1-alpha", dstFactor: "one-minus-src1-alpha", operation: "add" },
},
}],
},
});