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" },
      },
    }],
  },
});

see also