fp64 long-uptime clock
The long-uptime animation bug: fract(t) where t is the epoch base plus live seconds. Drag the UPTIME slider — near 10⁴ s both dials sweep, but past ~10⁷·² one f32 ulp grows wider than a second, the +time vanishes into it, and the plain-f32 left dial freezes (it only lurches when the sum crosses an ulp) while the f64 right dial, adding the live time in extended precision, keeps sweeping to 10⁹ s (30+ years). Speed is live; flip the fp64 toggle to freeze the right dial too.
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 long-uptime clock ═══//// The "shader time" bug every long-running app ships eventually: animate with// `fract(t)` when t has grown large and f32 time stops moving — at t ≈ 10⁸// seconds (a bit over 3 years) ulp_f32 = 8 s, so every sub-8-second phase is// GONE. Here the epoch base 10⁸+0.123 s lives as an f64 literal (split// losslessly at build time), the live `time` uniform is added IN extended// precision, and only the fract() phase — a sub-unit value — narrows to f32// to drive the dial. The right clock sweeps smoothly; the plain-f32 left// clock (base narrowed with toF32 before the add) is frozen solid. Flip the// fp64 toggle and the right one freezes too.//// 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, vec2, vec3, vec4, f32, fract, abs, min, mix, length, atan2, smoothstep, exp, step, toF32, toF64, f32T, f64T, Let,} from '../src/index.ts'import { VsOut, vs, fullscreenUniforms, screenCoords } from './_fullscreen.ts'import type { ShaderExample } from './_shared.ts'
const TAU = 6.283185307179586
const U = fullscreenUniforms({ epoch: f64T, // mission-time base seconds (swept 10^4..10^9 via the DISTANCE slider) speed: f32T, // dial revolutions per second fp64: f32T, // toggle: 1 = split-screen f32 | f64 (canonical), 0 = all-f32})
const fsClock = fn( 'fs_clock', { vo: VsOut }, (p) => { // Absolute mission time = epoch + live seconds; the ADD runs in df64 on // the right (exact) and in plain f32 on the left (the +time vanishes // under ulp(10⁸) = 8 s). Only the sub-unit phase ever narrows. const phase64 = Let(toF32(fract(U.field.epoch.add(toF64(U.field.time)).mul(U.field.speed)))) // f32 twin: the epoch narrowed to a plain f32 — once its ulp grows past a // second, `time` is absorbed and the dial only lurches when the sum crosses an ulp. const phase32 = Let(fract(toF32(U.field.epoch).add(U.field.time).mul(U.field.speed)))
const isF32 = Let(p.vo.uv.x.lt(0.5).or(U.field.fp64.lt(0.5))) const phase = Let(isF32.select(phase32, phase64))
// Each half gets its own dial: remap the half's uv to centred isotropic // coords (y ±1 over the height, x ±half-aspect over the half's 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 c = Let( screenCoords(vec2(sx, p.vo.uv.y), vec2(U.field.resolution.x.mul(0.5), U.field.resolution.y)), ) const r = Let(length(c)) // Angle as a 0..1 turn, 12-o'clock = 0, clockwise — matches the phase. const a01 = Let(fract(f32(0.25).sub(atan2(c.y, c.x).div(TAU))))
// Dial face: outer bezel ring, 12 tick marks, sweep hand + decay trail. const px = Let(f32(2).div(U.field.resolution.y)) // isotropic px size const bezel = Let(f32(1).sub(smoothstep(px.mul(1.5), px.mul(3.0), abs(r.sub(0.82)).sub(0.012)))) const tickA = Let( abs(fract(a01.mul(12.0)).sub(0.5)) .neg() .add(0.5), ) // 0 at each tick const tick = Let( f32(1) .sub(smoothstep(f32(0.0), f32(0.035), tickA)) .mul(smoothstep(f32(0.62), f32(0.66), r)) .mul(f32(1).sub(smoothstep(f32(0.78), f32(0.8), r))), ) // Angular distance BEHIND the hand (0 at the hand, growing clockwise-past). const behind = Let(fract(phase.sub(a01).add(1.0))) const hand = Let( f32(1) .sub(smoothstep(f32(0.0), f32(0.006), min(behind, f32(1).sub(behind)))) .mul(step(r, f32(0.6))) .mul(smoothstep(f32(0.05), f32(0.1), r)), ) const trail = Let( exp(behind.mul(-5.0)) .mul(0.35) .mul(step(r, f32(0.58))), ) const hub = Let(f32(1).sub(smoothstep(px.mul(2.0), px.mul(5.0), r)))
const face = Let(mix(vec3(0.03, 0.045, 0.08), vec3(0.05, 0.075, 0.12), r)) const rgb = face .add(vec3(0.85, 0.9, 1.0).mul(bezel.mul(0.35))) .add(vec3(0.8, 0.85, 0.95).mul(tick.mul(0.5))) .add(vec3(1.0, 0.72, 0.2).mul(hand)) .add(vec3(1.0, 0.6, 0.15).mul(trail)) .add(vec3(1.0, 0.85, 0.5).mul(hub)) return vec4(rgb, f32(1)) }, { stage: 'fragment', retAttr: '@location(0)' },)
// `_fp64` guard lands at (group 0, binding 1) automatically.const fp64ClockModule = module({ funcs: [vs, fsClock], uses: [U, VsOut],})
export const fp64Clock: ShaderExample = { id: 'fp64-clock', title: 'fp64 long-uptime clock', blurb: 'The long-uptime animation bug: fract(t) where t is the epoch base plus live seconds. Drag the UPTIME slider — near 10⁴ s both dials sweep, but past ~10⁷·² one f32 ulp grows wider than a second, the +time vanishes into it, and the plain-f32 left dial freezes (it only lurches when the sum crosses an ulp) while the f64 right dial, adding the live time in extended precision, keeps sweeping to 10⁹ s (30+ years). Speed is live; flip the fp64 toggle to freeze the right dial too.', category: 'generic', file: 'fp64-clock.ts', module: fp64ClockModule, renderable: true, splitLabels: ['f32', 'f64 (emulated)'], controls: { time: { kind: 'time' }, // Sweep the mission-time epoch through the f32 threshold (seconds → the dial // freezes past ~10^7·² where one ulp exceeds a second). epoch: { kind: 'logmag1d', magField: 'mag', base: 1, offset: 0.123 }, mag: { kind: 'slider', label: 'Uptime 10^x seconds', min: 4, max: 9, step: 0.02, value: 6, wheel: true, }, resolution: { kind: 'resolution' }, speed: { kind: 'slider', label: 'Rev / s', min: 0.05, max: 1, step: 0.05, value: 0.25 }, fp64: { kind: 'toggle', label: 'fp64 emulation', value: true }, },}struct Uniforms { time: f32, resolution: vec2<f32>, epoch: vec2<f32>, speed: 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_clock(vo: VsOut) -> @location(0) vec4<f32> { let _cse0 = (vo.uv.x < 0.5); let _cse1 = vec2<f32>((U.resolution.x * 0.5), U.resolution.y); let _v0 = df64_narrow(df64_fract(df64_mul(df64_add(U.epoch, vec2<f32>(U.time, 0.0)), vec2<f32>(U.speed, 0.0)))); let _v1 = fract(((df64_narrow(U.epoch) + U.time) * U.speed)); let _v2 = (_cse0 || (U.fp64 < 0.5)); let _v3 = select(_v0, _v1, _v2); let _v4 = (vo.uv.x * 2.0); let _v5 = (_v4 - select(1.0, 0.0, _cse0)); let _lc0 = vec2<f32>(_v5, vo.uv.y); let _v6 = vec2<f32>((((_lc0.x * 2.0) - 1.0) * (_cse1.x / _cse1.y)), ((_lc0.y * 2.0) - 1.0)); let _v7 = length(_v6); let _v8 = fract((0.25 - (atan2(_v6.y, _v6.x) / 6.283185307179586))); let _v9 = (2.0 / U.resolution.y); let _v10 = (1.0 - smoothstep((_v9 * 1.5), (_v9 * 3.0), (abs((_v7 - 0.82)) - 0.012))); let _v11 = ((-abs((fract((_v8 * 12.0)) - 0.5))) + 0.5); let _v12 = (((1.0 - smoothstep(0.0, 0.035, _v11)) * smoothstep(0.62, 0.66, _v7)) * (1.0 - smoothstep(0.78, 0.8, _v7))); let _v13 = fract(((_v3 - _v8) + 1.0)); let _v14 = (((1.0 - smoothstep(0.0, 0.006, min(_v13, (1.0 - _v13)))) * step(_v7, 0.6)) * smoothstep(0.05, 0.1, _v7)); let _v15 = ((exp((_v13 * -5.0)) * 0.35) * step(_v7, 0.58)); let _v16 = (1.0 - smoothstep((_v9 * 2.0), (_v9 * 5.0), _v7)); let _v17 = mix(vec3<f32>(0.03, 0.045, 0.08), vec3<f32>(0.05, 0.075, 0.12), _v7); return vec4<f32>((((((_v17 + (vec3<f32>(0.85, 0.9, 1.0) * (_v10 * 0.35))) + (vec3<f32>(0.8, 0.85, 0.95) * (_v12 * 0.5))) + (vec3<f32>(1.0, 0.72, 0.2) * _v14)) + (vec3<f32>(1.0, 0.6, 0.15) * _v15)) + (vec3<f32>(1.0, 0.85, 0.5) * _v16)), 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_floor(a: vec2<f32>) -> vec2<f32> { let _v0 = floor(a.x); return select(vec2<f32>(_v0, 0.0), df64_quickTwoSum(_v0, floor(a.y)), (_v0 == a.x));}
fn df64_fract(a: vec2<f32>) -> vec2<f32> { return df64_sub(a, df64_floor(a));}
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 { float time; vec2 resolution; vec2 epoch; float speed; 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);vec2 df64_floor(vec2 a);vec2 df64_fract(vec2 a);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);}
vec2 df64_floor(vec2 a) { float _v0 = floor(a.x); return ((_v0 == a.x) ? df64_quickTwoSum(_v0, floor(a.y)) : vec2(_v0, 0.0));}
vec2 df64_fract(vec2 a) { return df64_sub(a, df64_floor(a));}
float df64_narrow(vec2 a) { return (a.x + a.y);}in vec2 uv;layout(location = 0) out vec4 _ret;
vec4 fs_clock_impl(VsOut vo) { bool _cse0 = (vo.uv.x < 0.5); vec2 _cse1 = vec2((U.resolution.x * 0.5), U.resolution.y); float _v0 = df64_narrow(df64_fract(df64_mul(df64_add(U.epoch, vec2(U.time, 0.0)), vec2(U.speed, 0.0)))); float _v1 = fract(((df64_narrow(U.epoch) + U.time) * U.speed)); bool _v2 = (_cse0 || (U.fp64 < 0.5)); float _v3 = (_v2 ? _v1 : _v0); float _v4 = (vo.uv.x * 2.0); float _v5 = (_v4 - (_cse0 ? 0.0 : 1.0)); vec2 _lc0 = vec2(_v5, vo.uv.y); vec2 _v6 = vec2((((_lc0.x * 2.0) - 1.0) * (_cse1.x / _cse1.y)), ((_lc0.y * 2.0) - 1.0)); float _v7 = length(_v6); float _v8 = fract((0.25 - (atan(_v6.y, _v6.x) / 6.283185307179586))); float _v9 = (2.0 / U.resolution.y); float _v10 = (1.0 - smoothstep((_v9 * 1.5), (_v9 * 3.0), (abs((_v7 - 0.82)) - 0.012))); float _v11 = ((-abs((fract((_v8 * 12.0)) - 0.5))) + 0.5); float _v12 = (((1.0 - smoothstep(0.0, 0.035, _v11)) * smoothstep(0.62, 0.66, _v7)) * (1.0 - smoothstep(0.78, 0.8, _v7))); float _v13 = fract(((_v3 - _v8) + 1.0)); float _v14 = (((1.0 - smoothstep(0.0, 0.006, min(_v13, (1.0 - _v13)))) * step(_v7, 0.6)) * smoothstep(0.05, 0.1, _v7)); float _v15 = ((exp((_v13 * -5.0)) * 0.35) * step(_v7, 0.58)); float _v16 = (1.0 - smoothstep((_v9 * 2.0), (_v9 * 5.0), _v7)); vec3 _v17 = mix(vec3(0.03, 0.045, 0.08), vec3(0.05, 0.075, 0.12), _v7); return vec4((((((_v17 + (vec3(0.85, 0.9, 1.0) * (_v10 * 0.35))) + (vec3(0.8, 0.85, 0.95) * (_v12 * 0.5))) + (vec3(1.0, 0.72, 0.2) * _v14)) + (vec3(1.0, 0.6, 0.15) * _v15)) + (vec3(1.0, 0.85, 0.5) * _v16)), 1.0);}
void main() { VsOut vo; vo.pos = gl_FragCoord; vo.uv = uv; _ret = fs_clock_impl(vo);}{ "bindGroups": [ { "group": 0, "entries": [ { "group": 0, "binding": 0, "name": "U", "space": "uniform", "resourceKind": "uniform-buffer", "structName": "Uniforms" } ] } ], "uniforms": [ { "name": "Uniforms", "size": 32, "align": 16, "fields": [ { "name": "time", "type": "f32", "offset": 0, "align": 4, "size": 4 }, { "name": "resolution", "type": "vec2<f32>", "offset": 8, "align": 8, "size": 8 }, { "name": "epoch", "type": "f64", "offset": 16, "align": 8, "size": 8 }, { "name": "speed", "type": "f32", "offset": 24, "align": 4, "size": 4 }, { "name": "fp64", "type": "f32", "offset": 28, "align": 4, "size": 4 } ] } ], "storage": [], "entries": [ { "name": "vs", "stage": "vertex", "inputs": [ "u32" ], "output": "struct:VsOut" }, { "name": "fs_clock", "stage": "fragment", "inputs": [ "struct:VsOut" ], "output": "vec4<f32>" } ], "overrides": []}
Run it locally:
npx tsx examples/print.ts fp64-clock
— prints the WGSL, GLSL, reflection. Source:
GitHub.
New to the IR? See Concepts; the full authoring + emit surface is in the API reference.