fp64 catastrophic cancellation
The numerics-textbook plot on a GPU: (x−1)⁷ evaluated in EXPANDED form near x = 1 — eight ~1-sized terms must cancel to nine digits. f32 (left) returns pure noise thousands of times the plot range; emulated f64 (right) hugs the true curve. The thin red reference is the FACTORED form, which even f32 nails — restructure the math when you can, reach for fp64 when you can’t. Narrow the width slider and watch df64 hit its own wall two decades later.
One source, compiled every way
The DSL source below, compiled to WebGPU (WGSL), WebGL2 (GLSL ES 3.00 — two stages), and a reflection JSON the host binds from. Nothing but the DSL tab is hand-written.
// ═══ @xgis/shader-dsl example — fp64 catastrophic cancellation ═══//// The numerics-textbook figure, live on the GPU: (x−1)⁷ EVALUATED IN EXPANDED// FORM (x⁷ − 7x⁶ + 21x⁵ − 35x⁴ + 35x³ − 21x² + 7x − 1) near x = 1. The terms// are all ~1 while the true value is ~w⁷ ≈ 10⁻⁹ — eight ~1-sized numbers must// cancel to nine digits, which f32 (7 digits) cannot do AT ALL: its result is// ±5×10⁻⁶ NOISE, thousands of times the whole plot range (CPU-verified:// 7300× signal). The df64 side (~14 digits) hugs the true curve. The thin// reference line is the FACTORED form ((x−1)⁷ — subtract first, no// cancellation), which even f32 evaluates cleanly: the fix is always to// restructure the math, and fp64 is for when you can't.//// Narrow the half-width slider and even df64 starts to fray (w⁷ sinks toward// its ~14-digit floor) — the same budget wall, two decades further out.//// The `_fp64` guard uniform is auto-injected by the lowering; the render// harnesses bind it to 1.0f by probing the program for the Fp64Guard block.
import { fn, module, vec3, vec4, f32, f64, pow, fract, abs, min, mix, step, smoothstep, toF32, toF64, f32T, vec2fT, Let, uniformStruct,} from '../src/index.ts'import { VsOut, vs } from './_fullscreen.ts'import type { ShaderExample } from './_shared.ts'
const U = uniformStruct( 'Uniforms', { group: 0, binding: 0, as: 'u' }, { resolution: vec2fT, half_width: f32T, // plot spans x ∈ [1−w, 1+w] fp64: f32T, // toggle: 1 = split-screen f32 | f64 (canonical), 0 = all-f32 },)
const fsCancel = fn( 'fs_cancel', { vo: VsOut }, (p) => { const w = Let(U.field.half_width) const halfUv = Let(p.vo.uv.x.mul(2.0)) const sx = Let(halfUv.sub(p.vo.uv.x.lt(0.5).select(0.0, 1.0))) const u = Let(sx.sub(0.5).mul(w.mul(2.0))) // x − 1 ∈ [−w, w]
const isF32 = Let(p.vo.uv.x.lt(0.5).or(U.field.fp64.lt(0.5))) // f64 — the expanded polynomial, every term in extended precision; only // the ~w⁷-sized RESULT narrows (small values narrow harmlessly: f32 // precision is relative). const xd = Let(f64(1).add(toF64(u))) const x2 = Let(xd.mul(xd)) const x3 = Let(x2.mul(xd)) const x4 = Let(x2.mul(x2)) const x5 = Let(x4.mul(xd)) const x6 = Let(x3.mul(x3)) const x7 = Let(x6.mul(xd)) const p64 = Let( toF32( x7 .sub(x6.mul(7.0)) .add(x5.mul(21.0)) .sub(x4.mul(35.0)) .add(x3.mul(35.0)) .sub(x2.mul(21.0)) .add(xd.mul(7.0)) .sub(1.0), ), ) // f32 twin — SAME expanded polynomial: eight ~1-sized terms, seven digits. const xf = Let(f32(1).add(u)) const f2 = Let(xf.mul(xf)) const f3 = Let(f2.mul(xf)) const f4 = Let(f2.mul(f2)) const f5 = Let(f4.mul(xf)) const f6 = Let(f3.mul(f3)) const f7 = Let(f6.mul(xf)) const p32 = Let( f7 .sub(f6.mul(7.0)) .add(f5.mul(21.0)) .sub(f4.mul(35.0)) .add(f3.mul(35.0)) .sub(f2.mul(21.0)) .add(xf.mul(7.0)) .sub(1.0), ) const pv = Let(isF32.select(p32, p64))
// Plot in units of the true amplitude w⁷ (the picture is w-invariant). const yscale = Let(pow(w, f32(7)).mul(1.3)) const v = Let(pv.div(yscale)) // computed curve, ±0.77 at the edges const u2 = Let(u.mul(u)) const truth = Let(u2.mul(u2).mul(u2).mul(u).div(yscale)) // factored (x−1)⁷ — no cancellation const py = Let(p.vo.uv.y.sub(0.5).mul(2.0)) const px = Let(f32(2).div(U.field.resolution.y)) // plot-units per pixel
// Graph-paper styling: parchment with a pale grid (10 columns per half, // 0.2-plot-unit rows), ink axes. const gxf = Let(fract(sx.mul(10.0))) const gyf = Let(fract(py.add(1.0).mul(5.0))) const dgx = Let(min(gxf, f32(1).sub(gxf))) // distance to column line, in cells const dgy = Let(min(gyf, f32(1).sub(gyf))) const aaCx = Let(f32(30).div(U.field.resolution.x)) // ~1.5 px in cell units const aaCy = Let(f32(15).div(U.field.resolution.y)) const grid = Let( f32(1) .sub(smoothstep(f32(0), aaCx, dgx)) .add(f32(1).sub(smoothstep(f32(0), aaCy, dgy))), ) const paper = vec3(0.96, 0.94, 0.88) const rgb0 = Let(mix(paper, vec3(0.72, 0.78, 0.86), min(grid, f32(1)).mul(0.45)))
// Fill below the computed curve — its BOUNDARY is the visible verdict: // a smooth odd curve on the f64 side, a full-height noise barcode on f32. const fill = Let(step(py, v)) const rgb1 = Let(mix(rgb0, vec3(0.62, 0.74, 0.9), fill.mul(0.5))) // Ink the computed curve where it is on-screen and locally flat enough. const ink = Let(f32(1).sub(smoothstep(px.mul(1.2), px.mul(3.0), abs(v.sub(py))))) const rgb2 = Let(mix(rgb1, vec3(0.13, 0.16, 0.3), ink.mul(0.85))) // The factored-form reference in warm red — clean on BOTH halves. const ref = Let(f32(1).sub(smoothstep(px.mul(0.8), px.mul(2.2), abs(truth.sub(py))))) const rgb3 = Let(mix(rgb2, vec3(0.8, 0.25, 0.2), ref.mul(0.65))) // Axes: y = 0 and x = 1 (the cancellation point). const axis = Let( min( smoothstep(f32(0), px.mul(1.5), abs(py)), smoothstep(f32(0), px.mul(1.5), abs(sx.sub(0.5)).mul(2.0)), ), ) const rgb = Let(mix(vec3(0.35, 0.33, 0.3), rgb3, axis)) return vec4(rgb, f32(1)) }, { stage: 'fragment', retAttr: '@location(0)' },)
// `_fp64` guard lands at (group 0, binding 1) automatically.const fp64CancellationModule = module({ funcs: [vs, fsCancel], uses: [U, VsOut],})
export const fp64Cancellation: ShaderExample = { id: 'fp64-cancellation', title: 'fp64 catastrophic cancellation', blurb: 'The numerics-textbook plot on a GPU: (x−1)⁷ evaluated in EXPANDED form near x = 1 — eight ~1-sized terms must cancel to nine digits. f32 (left) returns pure noise thousands of times the plot range; emulated f64 (right) hugs the true curve. The thin red reference is the FACTORED form, which even f32 nails — restructure the math when you can, reach for fp64 when you can’t. Narrow the width slider and watch df64 hit its own wall two decades later.', category: 'generic', file: 'fp64-cancellation.ts', module: fp64CancellationModule, renderable: true, splitLabels: ['f32 expanded', 'f64 expanded'], controls: { resolution: { kind: 'resolution' }, half_width: { kind: 'slider', label: 'Half-width w', min: 0.012, max: 0.12, step: 0.002, value: 0.05, wheel: true, }, fp64: { kind: 'toggle', label: 'fp64 emulation', value: true }, },}struct Uniforms { resolution: vec2<f32>, half_width: f32, fp64: f32,}
struct VsOut { @builtin(position) pos: vec4<f32>, @location(0) uv: vec2<f32>,}
@group(0) @binding(0) var<uniform> u: Uniforms;@group(0) @binding(1) var _fp64: texture_2d<f32>;
@vertexfn vs(@builtin(vertex_index) vi: u32) -> VsOut { let _cse0 = ((f32((vi & 1u)) * 4.0) - 1.0); let _cse1 = ((f32((vi >> 1u)) * 4.0) - 1.0); return VsOut(vec4<f32>(_cse0, _cse1, 0.0, 1.0), vec2<f32>(((_cse0 * 0.5) + 0.5), ((_cse1 * 0.5) + 0.5)));}
@fragmentfn fs_cancel(vo: VsOut) -> @location(0) vec4<f32> { let _cse0 = (vo.uv.x < 0.5); let _cse1 = vec2<f32>(1.0, 0.0); let _cse2 = vec2<f32>(0.0, 0.0); let _cse3 = vec2<f32>(7.0, 0.0); let _cse4 = vec2<f32>(21.0, 0.0); let _cse5 = vec2<f32>(35.0, 0.0); let _v0 = u.half_width; let _v1 = (vo.uv.x * 2.0); let _v2 = (_v1 - select(1.0, 0.0, _cse0)); let _v3 = ((_v2 - 0.5) * (_v0 * 2.0)); let _v4 = (_cse0 || (u.fp64 < 0.5)); let _v5 = df64_add(_cse1, vec2<f32>(_v3, 0.0)); let _v6 = df64_mul(_v5, _v5); let _v7 = df64_mul(_v6, _v5); let _v8 = df64_mul(_v6, _v6); let _v9 = df64_mul(_v8, _v5); let _v10 = df64_mul(_v7, _v7); let _v11 = df64_mul(_v10, _v5); let _v12 = df64_narrow(df64_sub(df64_add(df64_sub(df64_add(df64_sub(df64_add(df64_sub(df64_add(_v11, _cse2), df64_mul(_v10, _cse3)), df64_mul(_v9, _cse4)), df64_mul(_v8, _cse5)), df64_mul(_v7, _cse5)), df64_mul(_v6, _cse4)), df64_mul(_v5, _cse3)), df64_add(_cse1, _cse2))); let _v13 = (1.0 + _v3); let _v14 = (_v13 * _v13); let _v15 = (_v14 * _v13); let _v16 = (_v14 * _v14); let _v17 = (_v16 * _v13); let _v18 = (_v15 * _v15); let _v19 = (_v18 * _v13); let _v20 = (((((((_v19 - (_v18 * 7.0)) + (_v17 * 21.0)) - (_v16 * 35.0)) + (_v15 * 35.0)) - (_v14 * 21.0)) + (_v13 * 7.0)) - 1.0); let _v21 = select(_v12, _v20, _v4); let _v22 = (pow(_v0, 7.0) * 1.3); let _v23 = (_v21 / _v22); let _v24 = (_v3 * _v3); let _v25 = ((((_v24 * _v24) * _v24) * _v3) / _v22); let _v26 = ((vo.uv.y - 0.5) * 2.0); let _v27 = (2.0 / u.resolution.y); let _v28 = fract((_v2 * 10.0)); let _v29 = fract(((_v26 + 1.0) * 5.0)); let _v30 = min(_v28, (1.0 - _v28)); let _v31 = min(_v29, (1.0 - _v29)); let _v32 = (30.0 / u.resolution.x); let _v33 = (15.0 / u.resolution.y); let _v34 = ((1.0 - smoothstep(0.0, _v32, _v30)) + (1.0 - smoothstep(0.0, _v33, _v31))); let _v35 = mix(vec3<f32>(0.96, 0.94, 0.88), vec3<f32>(0.72, 0.78, 0.86), (min(_v34, 1.0) * 0.45)); let _v36 = step(_v26, _v23); let _v37 = mix(_v35, vec3<f32>(0.62, 0.74, 0.9), (_v36 * 0.5)); let _v38 = (1.0 - smoothstep((_v27 * 1.2), (_v27 * 3.0), abs((_v23 - _v26)))); let _v39 = mix(_v37, vec3<f32>(0.13, 0.16, 0.3), (_v38 * 0.85)); let _v40 = (1.0 - smoothstep((_v27 * 0.8), (_v27 * 2.2), abs((_v25 - _v26)))); let _v41 = mix(_v39, vec3<f32>(0.8, 0.25, 0.2), (_v40 * 0.65)); let _lc0 = (_v27 * 1.5); let _v42 = min(smoothstep(0.0, _lc0, abs(_v26)), smoothstep(0.0, _lc0, (abs((_v2 - 0.5)) * 2.0))); let _v43 = mix(vec3<f32>(0.35, 0.33, 0.3), _v41, _v42); return vec4<f32>(_v43, 1.0);}
fn df64_twoSum(a: f32, b: f32) -> vec2<f32> { let _cse0 = textureLoad(_fp64, vec2<i32>(0, 0), 0).x; let _v0 = (a + b); let _v1 = (((_v0 * _cse0) - a) * _cse0); let _v2 = (((((a - ((_v0 - _v1) * _cse0)) * _cse0) * _cse0) * _cse0) + (b - _v1)); return vec2<f32>(_v0, _v2);}
fn df64_quickTwoSum(a: f32, b: f32) -> vec2<f32> { let _cse0 = textureLoad(_fp64, vec2<i32>(0, 0), 0).x; let _v0 = ((a + b) * _cse0); let _v1 = (b - ((_v0 - a) * _cse0)); return vec2<f32>(_v0, _v1);}
fn df64_split(a: f32) -> vec2<f32> { let _cse0 = textureLoad(_fp64, vec2<i32>(0, 0), 0).x; let _v0 = (a * (_cse0 * 4097.0)); let _v1 = ((_v0 * _cse0) - (_v0 - a)); let _v2 = ((a * _cse0) - _v1); return vec2<f32>(_v1, _v2);}
fn df64_twoProd(a: f32, b: f32) -> vec2<f32> { let _v0 = (a * b); let _v1 = df64_split(a); let _v2 = df64_split(b); let _v3 = (((((_v1.x * _v2.x) - _v0) + (_v1.x * _v2.y)) + (_v1.y * _v2.x)) + (_v1.y * _v2.y)); return vec2<f32>(_v0, _v3);}
fn df64_add(a: vec2<f32>, b: vec2<f32>) -> vec2<f32> { var _v0: vec2<f32> = df64_twoSum(a.x, b.x); let _v1 = df64_twoSum(a.y, b.y); _v0.y = (_v0.y + _v1.x); _v0 = df64_quickTwoSum(_v0.x, _v0.y); _v0.y = (_v0.y + _v1.y); _v0 = df64_quickTwoSum(_v0.x, _v0.y); return _v0;}
fn df64_sub(a: vec2<f32>, b: vec2<f32>) -> vec2<f32> { return df64_add(a, (-b));}
fn df64_mul(a: vec2<f32>, b: vec2<f32>) -> vec2<f32> { var _v0: vec2<f32> = df64_twoProd(a.x, b.x); _v0.y = (_v0.y + (a.x * b.y)); _v0 = df64_quickTwoSum(_v0.x, _v0.y); _v0.y = (_v0.y + (a.y * b.x)); return df64_quickTwoSum(_v0.x, _v0.y);}
fn df64_narrow(a: vec2<f32>) -> f32 { return (a.x + a.y);}#version 300 esprecision highp float;precision highp int;
struct VsOut { vec4 pos; vec2 uv;};out vec2 uv;
VsOut vs_impl(uint vi) { float _cse0 = ((float((vi & 1u)) * 4.0) - 1.0); float _cse1 = ((float((vi >> 1u)) * 4.0) - 1.0); return VsOut(vec4(_cse0, _cse1, 0.0, 1.0), vec2(((_cse0 * 0.5) + 0.5), ((_cse1 * 0.5) + 0.5)));}
void main() { VsOut _out = vs_impl(uint(gl_VertexID)); gl_Position = _out.pos; uv = _out.uv;}#version 300 esprecision highp float;precision highp int;
struct VsOut { vec4 pos; vec2 uv;};layout(std140) uniform Uniforms { vec2 resolution; float half_width; float fp64;} u;
uniform sampler2D _fp64;vec2 df64_twoSum(float a, float b);vec2 df64_quickTwoSum(float a, float b);vec2 df64_split(float a);vec2 df64_twoProd(float a, float b);vec2 df64_add(vec2 a, vec2 b);vec2 df64_sub(vec2 a, vec2 b);vec2 df64_mul(vec2 a, vec2 b);float df64_narrow(vec2 a);vec2 df64_twoSum(float a, float b) { float _cse0 = texelFetch(_fp64, ivec2(0, 0), 0).x; float _v0 = (a + b); float _v1 = (((_v0 * _cse0) - a) * _cse0); float _v2 = (((((a - ((_v0 - _v1) * _cse0)) * _cse0) * _cse0) * _cse0) + (b - _v1)); return vec2(_v0, _v2);}
vec2 df64_quickTwoSum(float a, float b) { float _cse0 = texelFetch(_fp64, ivec2(0, 0), 0).x; float _v0 = ((a + b) * _cse0); float _v1 = (b - ((_v0 - a) * _cse0)); return vec2(_v0, _v1);}
vec2 df64_split(float a) { float _cse0 = texelFetch(_fp64, ivec2(0, 0), 0).x; float _v0 = (a * (_cse0 * 4097.0)); float _v1 = ((_v0 * _cse0) - (_v0 - a)); float _v2 = ((a * _cse0) - _v1); return vec2(_v1, _v2);}
vec2 df64_twoProd(float a, float b) { float _v0 = (a * b); vec2 _v1 = df64_split(a); vec2 _v2 = df64_split(b); float _v3 = (((((_v1.x * _v2.x) - _v0) + (_v1.x * _v2.y)) + (_v1.y * _v2.x)) + (_v1.y * _v2.y)); return vec2(_v0, _v3);}
vec2 df64_add(vec2 a, vec2 b) { vec2 _v0 = df64_twoSum(a.x, b.x); vec2 _v1 = df64_twoSum(a.y, b.y); _v0.y = (_v0.y + _v1.x); _v0 = df64_quickTwoSum(_v0.x, _v0.y); _v0.y = (_v0.y + _v1.y); _v0 = df64_quickTwoSum(_v0.x, _v0.y); return _v0;}
vec2 df64_sub(vec2 a, vec2 b) { return df64_add(a, (-b));}
vec2 df64_mul(vec2 a, vec2 b) { vec2 _v0 = df64_twoProd(a.x, b.x); _v0.y = (_v0.y + (a.x * b.y)); _v0 = df64_quickTwoSum(_v0.x, _v0.y); _v0.y = (_v0.y + (a.y * b.x)); return df64_quickTwoSum(_v0.x, _v0.y);}
float df64_narrow(vec2 a) { return (a.x + a.y);}in vec2 uv;layout(location = 0) out vec4 _ret;
vec4 fs_cancel_impl(VsOut vo) { bool _cse0 = (vo.uv.x < 0.5); vec2 _cse1 = vec2(1.0, 0.0); vec2 _cse2 = vec2(0.0, 0.0); vec2 _cse3 = vec2(7.0, 0.0); vec2 _cse4 = vec2(21.0, 0.0); vec2 _cse5 = vec2(35.0, 0.0); float _v0 = u.half_width; float _v1 = (vo.uv.x * 2.0); float _v2 = (_v1 - (_cse0 ? 0.0 : 1.0)); float _v3 = ((_v2 - 0.5) * (_v0 * 2.0)); bool _v4 = (_cse0 || (u.fp64 < 0.5)); vec2 _v5 = df64_add(_cse1, vec2(_v3, 0.0)); vec2 _v6 = df64_mul(_v5, _v5); vec2 _v7 = df64_mul(_v6, _v5); vec2 _v8 = df64_mul(_v6, _v6); vec2 _v9 = df64_mul(_v8, _v5); vec2 _v10 = df64_mul(_v7, _v7); vec2 _v11 = df64_mul(_v10, _v5); float _v12 = df64_narrow(df64_sub(df64_add(df64_sub(df64_add(df64_sub(df64_add(df64_sub(df64_add(_v11, _cse2), df64_mul(_v10, _cse3)), df64_mul(_v9, _cse4)), df64_mul(_v8, _cse5)), df64_mul(_v7, _cse5)), df64_mul(_v6, _cse4)), df64_mul(_v5, _cse3)), df64_add(_cse1, _cse2))); float _v13 = (1.0 + _v3); float _v14 = (_v13 * _v13); float _v15 = (_v14 * _v13); float _v16 = (_v14 * _v14); float _v17 = (_v16 * _v13); float _v18 = (_v15 * _v15); float _v19 = (_v18 * _v13); float _v20 = (((((((_v19 - (_v18 * 7.0)) + (_v17 * 21.0)) - (_v16 * 35.0)) + (_v15 * 35.0)) - (_v14 * 21.0)) + (_v13 * 7.0)) - 1.0); float _v21 = (_v4 ? _v20 : _v12); float _v22 = (pow(_v0, 7.0) * 1.3); float _v23 = (_v21 / _v22); float _v24 = (_v3 * _v3); float _v25 = ((((_v24 * _v24) * _v24) * _v3) / _v22); float _v26 = ((vo.uv.y - 0.5) * 2.0); float _v27 = (2.0 / u.resolution.y); float _v28 = fract((_v2 * 10.0)); float _v29 = fract(((_v26 + 1.0) * 5.0)); float _v30 = min(_v28, (1.0 - _v28)); float _v31 = min(_v29, (1.0 - _v29)); float _v32 = (30.0 / u.resolution.x); float _v33 = (15.0 / u.resolution.y); float _v34 = ((1.0 - smoothstep(0.0, _v32, _v30)) + (1.0 - smoothstep(0.0, _v33, _v31))); vec3 _v35 = mix(vec3(0.96, 0.94, 0.88), vec3(0.72, 0.78, 0.86), (min(_v34, 1.0) * 0.45)); float _v36 = step(_v26, _v23); vec3 _v37 = mix(_v35, vec3(0.62, 0.74, 0.9), (_v36 * 0.5)); float _v38 = (1.0 - smoothstep((_v27 * 1.2), (_v27 * 3.0), abs((_v23 - _v26)))); vec3 _v39 = mix(_v37, vec3(0.13, 0.16, 0.3), (_v38 * 0.85)); float _v40 = (1.0 - smoothstep((_v27 * 0.8), (_v27 * 2.2), abs((_v25 - _v26)))); vec3 _v41 = mix(_v39, vec3(0.8, 0.25, 0.2), (_v40 * 0.65)); float _lc0 = (_v27 * 1.5); float _v42 = min(smoothstep(0.0, _lc0, abs(_v26)), smoothstep(0.0, _lc0, (abs((_v2 - 0.5)) * 2.0))); vec3 _v43 = mix(vec3(0.35, 0.33, 0.3), _v41, _v42); return vec4(_v43, 1.0);}
void main() { VsOut vo; vo.pos = gl_FragCoord; vo.uv = uv; _ret = fs_cancel_impl(vo);}{ "bindGroups": [ { "group": 0, "entries": [ { "group": 0, "binding": 0, "name": "u", "space": "uniform", "resourceKind": "uniform-buffer", "structName": "Uniforms" } ] } ], "uniforms": [ { "name": "Uniforms", "size": 16, "align": 16, "fields": [ { "name": "resolution", "type": "vec2<f32>", "offset": 0, "align": 8, "size": 8 }, { "name": "half_width", "type": "f32", "offset": 8, "align": 4, "size": 4 }, { "name": "fp64", "type": "f32", "offset": 12, "align": 4, "size": 4 } ] } ], "storage": [], "entries": [ { "name": "vs", "stage": "vertex", "inputs": [ "u32" ], "output": "struct:VsOut" }, { "name": "fs_cancel", "stage": "fragment", "inputs": [ "struct:VsOut" ], "output": "vec4<f32>" } ], "overrides": []}
Run it locally:
npx tsx examples/print.ts fp64-cancellation
— prints the WGSL, GLSL, reflection. Source:
GitHub.
New to the IR? See Concepts; the full authoring + emit surface is in the API reference.