fp64 Burning Ship
The Burning Ship fractal (fold |Re z|, |Im z| before squaring) zoomed onto its needle spike.
// ═══ typeshade example — fp64 Burning Ship ═══//// The Burning Ship (z ← (|Re z| + i·|Im z|)² + c) is the fp64 family's// showcase for `abs` ON THE f64 TYPE: the fold happens inside the// extended-precision iteration, not after a narrowing — df64 abs is exact// (negate both planes), so the fold costs no precision. The camera sits on// the set's spike at c = −1.748 on the real axis (the axis itself never// escapes — same real dynamics as the Mandelbrot needle), where the CPU// check keeps 40–110 distinct escape bands per 48² window from a 1e-5 span// all the way down to 1e-12. The plain-f32 left half collapses flat once the// span drops under one ulp of 1.748 (~1e-7).//// 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, abs, pow, log2, max, mix, step, toF32, toF64, f32T, vec2fT, vec2f64T, If, Loop, Break, Var, Let, u32, uniformStruct,} from '../src/index.js';import { VsOut, vs } from './_fullscreen.js';import type { ShaderExample } from './_shared.js';
// A spike point with escape structure at every depth (CPU-verified); y = 0// keeps the never-escaping real axis mid-frame at every zoom.const CENTER_X = -1.748;const CENTER_Y = 0;const ITER = 128;
const U = uniformStruct( 'Uniforms', { group: 0, binding: 0, as: 'u' }, { center: vec2f64T, // one DF64Vec2 slot — host packs [hi.x, hi.y, lo.x, lo.y] resolution: vec2fT, zoom_exp: f32T, // view span = 10^-zoom_exp complex units fp64: f32T, // toggle: 1 = split-screen f32 | f64 (canonical), 0 = all-f32 },);
const fsShip = fn( 'fs_ship', { vo: VsOut }, (p) => { const span = Let(pow(f32(10.0), U.field.zoom_exp.neg())); const half = Let(p.vo.uv.x.mul(2.0)); const sx = Let(half.sub(p.vo.uv.x.lt(0.5).select(0.0, 1.0))); const dx = Let(sx.sub(0.5).mul(span)); const dy = Let( p.vo.uv.y.sub(0.5).mul(span).mul(U.field.resolution.y.div(U.field.resolution.x).mul(2.0)), );
const it = Var(f32(0)); const m2 = Var(f32(0)); // |z|² of the last z the loop reached: the escape test and the colouring's input If(p.vo.uv.x.lt(0.5).or(U.field.fp64.lt(0.5)), () => { // f32 twin — SAME fold-and-square, center narrowed. const cx = Let(toF32(U.field.center.x).add(dx)); const cy = Let(toF32(U.field.center.y).add(dy)); // The escape loop is written as fp64-julia.ts's (which records why and what it saves): // |z|² is carried in m2 beside z, the squares beside it so none is computed twice a // trip, and the loop leaves at the first escaped z. z₀ = 0, so all three start at 0. const zx = Var(f32(0)); const zy = Var(f32(0)); const x2 = Var(f32(0)); const y2 = Var(f32(0)); Loop( u32(0), (j) => j.lt(u32(ITER)), () => { If(m2.gt(16.0), () => { Break(); }); const nzx = Let(x2.sub(y2).add(cx)); zy.assign(abs(zx.mul(zy)).mul(2.0).add(cy)); zx.assign(nzx); it.assign(it.add(1.0)); x2.assign(zx.mul(zx)); y2.assign(zy.mul(zy)); m2.assign(x2.add(y2)); }, ); }).else(() => { // f64 — |Re|, |Im| fold in extended precision (2|zx·zy| ≡ the |Im| fold // of the squared form: (|zx|+i|zy|)² has Im = 2|zx||zy| = 2|zx·zy|). const cx = Let(U.field.center.x.add(toF64(dx))); const cy = Let(U.field.center.y.add(toF64(dy))); // The escape test reads an f32 |z|² squared from the narrowed words, as in // fp64-julia.ts: 48 bits move |z|² across 16 only from within an f32 rounding of it. const zx = Var(f64(0)); const zy = Var(f64(0)); Loop( u32(0), (j) => j.lt(u32(ITER)), () => { If(m2.gt(16.0), () => { Break(); }); const nzx = Let(zx.mul(zx).sub(zy.mul(zy)).add(cx)); zy.assign(abs(zx.mul(zy)).mul(2.0).add(cy)); zx.assign(nzx); it.assign(it.add(1.0)); const hx = Let(toF32(zx)); const hy = Let(toF32(zy)); m2.assign(hx.mul(hx).add(hy.mul(hy))); }, ); });
// Smooth escape time through an ember palette (dark hull → orange flame → // pale smoke); interior stays black. Same log₂ log₂ smoothing as // fp64-mandelbrot.ts. const sn = Let(it.sub(log2(max(log2(max(m2, 1.0001)), 0.0001))).add(1.0)); const inside = Let(step(f32(ITER).sub(0.5), it)); const s = Let(sn.div(ITER)); const ease = Let(s.mul(s).mul(f32(3).sub(s.mul(2)))); // s²(3−2s) ember ramp const rgb = mix( mix(vec3(0.06, 0.02, 0.05), vec3(0.95, 0.45, 0.08), ease), vec3(1.0, 0.93, 0.75), s.mul(s), ).mul(f32(1).sub(inside)); return vec4(rgb, f32(1)); }, { stage: 'fragment', retAttr: '@location(0)' },);
// `_fp64` guard lands at (group 0, binding 1) automatically.const fp64BurningShipModule = module({ funcs: [vs, fsShip], uses: [U, VsOut],});
export const fp64BurningShip: ShaderExample = { id: 'fp64-burning-ship', title: 'fp64 Burning Ship', blurb: 'The Burning Ship fractal (fold |Re z|, |Im z| before squaring) zoomed onto its needle spike — the fold runs as df64 abs INSIDE the extended-precision iteration, exact by construction. The plain-f32 left half collapses flat past a ~1e-7 span; the emulated-double right half keeps the flame filaments down to the df64 floor. Drag to pan, wheel to zoom, flip the fp64 toggle to collapse the right half in place.', category: 'generic', file: 'fp64-burning-ship.ts', module: fp64BurningShipModule, renderable: true, splitLabels: ['f32', 'f64 (emulated)'], controls: { center: { kind: 'pan2d', value: [CENTER_X, CENTER_Y], zoomExpField: 'zoom_exp', unitsPerWidth: 2, }, resolution: { kind: 'resolution' }, zoom_exp: { kind: 'slider', label: 'Zoom 10^-x', min: 0, max: 16, step: 0.05, value: 4, wheel: true, }, fp64: { kind: 'toggle', label: 'fp64 emulation', value: true }, },};struct Uniforms { @align(16) center: DF64Vec2, resolution: vec2<f32>, zoom_exp: f32, fp64: f32,}
struct VsOut { @builtin(position) pos: vec4<f32>, @location(0) uv: vec2<f32>,}
struct DF64Vec2 { hi: vec2<f32>, lo: 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_ship(vo: VsOut) -> @location(0) vec4<f32> { let _fp64_g = textureLoad(_fp64, vec2<i32>(0, 0), 0).x; let _v0 = pow(10.0, (-u.zoom_exp)); let _v1 = (vo.uv.x * 2.0); let _cse0 = (vo.uv.x < 0.5); let _v2 = (_v1 - select(1.0, 0.0, _cse0)); let _v3 = ((_v2 - 0.5) * _v0); let _v4 = (((vo.uv.y - 0.5) * _v0) * ((u.resolution.y / u.resolution.x) * 2.0)); var _v5: f32 = 0.0; var _v6: f32 = 0.0; let _cse1 = vec2<f32>(u.center.hi.x, u.center.lo.x); let _cse2 = vec2<f32>(u.center.hi.y, u.center.lo.y); if ((_cse0 || (u.fp64 < 0.5))) { let _v7 = (df64_narrow(_cse1) + _v3); let _v8 = (df64_narrow(_cse2) + _v4); var _v9: f32 = 0.0; var _v10: f32 = 0.0; var _v11: f32 = 0.0; var _v12: f32 = 0.0; for (var _v13: u32 = 0u; (_v13 < 128u); _v13 = (_v13 + 1u)) { if ((_v6 > 16.0)) { break; } let _v14 = ((_v11 - _v12) + _v7); _v10 = ((abs((_v9 * _v10)) * 2.0) + _v8); _v9 = _v14; _v5 = (_v5 + 1.0); _v11 = (_v9 * _v9); _v12 = (_v10 * _v10); _v6 = (_v11 + _v12); } } else { let _v15 = df64_add(_cse1, vec2<f32>(_v3, 0.0), _fp64_g); let _v16 = df64_add(_cse2, vec2<f32>(_v4, 0.0), _fp64_g); let _cse3 = vec2<f32>(0.0, 0.0); var _v17: vec2<f32> = _cse3; var _v18: vec2<f32> = _cse3; for (var _v19: u32 = 0u; (_v19 < 128u); _v19 = (_v19 + 1u)) { if ((_v6 > 16.0)) { break; } let _v20 = df64_add(df64_sub(df64_sqr(_v17, _fp64_g), df64_sqr(_v18, _fp64_g), _fp64_g), _v15, _fp64_g); _v18 = df64_add((df64_abs(df64_mul(_v17, _v18, _fp64_g)) * 2.0), _v16, _fp64_g); _v17 = _v20; _v5 = (_v5 + 1.0); let _v21 = df64_narrow(_v17); let _v22 = df64_narrow(_v18); _v6 = ((_v21 * _v21) + (_v22 * _v22)); } } let _v23 = ((_v5 - log2(max(log2(max(_v6, 1.0001)), 0.0001))) + 1.0); let _v24 = step(127.5, _v5); let _v25 = (_v23 * 0.0078125); let _gv0 = (_v25 * _v25); let _v26 = (_gv0 * (3.0 - (_v25 * 2.0))); return vec4<f32>((mix(mix(vec3<f32>(0.06, 0.02, 0.05), vec3<f32>(0.95, 0.45, 0.08), _v26), vec3<f32>(1.0, 0.93, 0.75), _gv0) * (1.0 - _v24)), 1.0);}
fn df64_twoSum(a: f32, b: f32, _fp64_g: f32) -> vec2<f32> { let _v0 = (a + b); let _v1 = (((_v0 * _fp64_g) - a) * _fp64_g); let _v2 = (((a - ((_v0 - _v1) * _fp64_g)) * _fp64_g) + (b - _v1)); return vec2<f32>(_v0, _v2);}
fn df64_quickTwoSum(a: f32, b: f32, _fp64_g: f32) -> vec2<f32> { let _v0 = ((a + b) * _fp64_g); let _v1 = (b - ((_v0 - a) * _fp64_g)); return vec2<f32>(_v0, _v1);}
fn df64_split(a: f32, _fp64_g: f32) -> vec2<f32> { let _v0 = (a * (_fp64_g * 4097.0)); let _v1 = ((_v0 * _fp64_g) - (_v0 - a)); let _v2 = ((a * _fp64_g) - _v1); return vec2<f32>(_v1, _v2);}
fn df64_twoProd(a: f32, b: f32, _fp64_g: f32) -> vec2<f32> { let _v0 = (a * b); let _v1 = df64_split(a, _fp64_g); let _v2 = df64_split(b, _fp64_g); 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_twoSqr(a: f32, _fp64_g: f32) -> vec2<f32> { let _v0 = (a * a); let _v1 = df64_split(a, _fp64_g); let _v2 = (((((_v1.x * _v1.x) - _v0) * _fp64_g) + (((_v1.x * _v1.y) * 2.0) * _fp64_g)) + ((_v1.y * _v1.y) * _fp64_g)); return vec2<f32>(_v0, _v2);}
fn df64_add(a: vec2<f32>, b: vec2<f32>, _fp64_g: f32) -> vec2<f32> { var _v0: vec2<f32> = df64_twoSum(a.x, b.x, _fp64_g); let _v1 = df64_twoSum(a.y, b.y, _fp64_g); _v0.y = (_v0.y + _v1.x); _v0 = df64_quickTwoSum(_v0.x, _v0.y, _fp64_g); _v0.y = (_v0.y + _v1.y); _v0 = df64_quickTwoSum(_v0.x, _v0.y, _fp64_g); return _v0;}
fn df64_sub(a: vec2<f32>, b: vec2<f32>, _fp64_g: f32) -> vec2<f32> { return df64_add(a, (-b), _fp64_g);}
fn df64_mul(a: vec2<f32>, b: vec2<f32>, _fp64_g: f32) -> vec2<f32> { var _v0: vec2<f32> = df64_twoProd(a.x, b.x, _fp64_g); _v0.y = (_v0.y + (a.x * b.y)); _v0 = df64_quickTwoSum(_v0.x, _v0.y, _fp64_g); _v0.y = (_v0.y + (a.y * b.x)); return df64_quickTwoSum(_v0.x, _v0.y, _fp64_g);}
fn df64_sqr(a: vec2<f32>, _fp64_g: f32) -> vec2<f32> { var _v0: vec2<f32> = df64_twoSqr(a.x, _fp64_g); _v0.y = (_v0.y + ((a.x * a.y) * 2.0)); return df64_quickTwoSum(_v0.x, _v0.y, _fp64_g);}
fn df64_abs(a: vec2<f32>) -> vec2<f32> { return select(a, (-a), (a.x < 0.0));}
fn df64_narrow(a: vec2<f32>) -> f32 { return (a.x + a.y);}#version 300 esprecision highp float;precision highp int;
out vec2 uv;
void main() { uint vi = uint(gl_VertexID); float _cse0 = ((float((vi & 1u)) * 4.0) - 1.0); float _cse1 = ((float((vi >> 1u)) * 4.0) - 1.0); gl_Position = vec4(_cse0, _cse1, 0.0, 1.0); uv = vec2(((_cse0 * 0.5) + 0.5), ((_cse1 * 0.5) + 0.5));}#version 300 esprecision highp float;precision highp int;
struct DF64Vec2 { vec2 hi; vec2 lo;};layout(std140) uniform Uniforms { DF64Vec2 center; vec2 resolution; float zoom_exp; float fp64;} u;
uniform highp sampler2D _fp64;vec2 df64_twoSum(float a, float b, float _fp64_g) { float _v0 = (a + b); float _v1 = (((_v0 * _fp64_g) - a) * _fp64_g); float _v2 = (((a - ((_v0 - _v1) * _fp64_g)) * _fp64_g) + (b - _v1)); return vec2(_v0, _v2);}
vec2 df64_quickTwoSum(float a, float b, float _fp64_g) { float _v0 = ((a + b) * _fp64_g); float _v1 = (b - ((_v0 - a) * _fp64_g)); return vec2(_v0, _v1);}
vec2 df64_split(float a, float _fp64_g) { float _v0 = (a * (_fp64_g * 4097.0)); float _v1 = ((_v0 * _fp64_g) - (_v0 - a)); float _v2 = ((a * _fp64_g) - _v1); return vec2(_v1, _v2);}
vec2 df64_twoProd(float a, float b, float _fp64_g) { float _v0 = (a * b); vec2 _v1 = df64_split(a, _fp64_g); vec2 _v2 = df64_split(b, _fp64_g); 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_twoSqr(float a, float _fp64_g) { float _v0 = (a * a); vec2 _v1 = df64_split(a, _fp64_g); float _v2 = (((((_v1.x * _v1.x) - _v0) * _fp64_g) + (((_v1.x * _v1.y) * 2.0) * _fp64_g)) + ((_v1.y * _v1.y) * _fp64_g)); return vec2(_v0, _v2);}
vec2 df64_add(vec2 a, vec2 b, float _fp64_g) { vec2 _v0 = df64_twoSum(a.x, b.x, _fp64_g); vec2 _v1 = df64_twoSum(a.y, b.y, _fp64_g); _v0.y = (_v0.y + _v1.x); _v0 = df64_quickTwoSum(_v0.x, _v0.y, _fp64_g); _v0.y = (_v0.y + _v1.y); _v0 = df64_quickTwoSum(_v0.x, _v0.y, _fp64_g); return _v0;}
vec2 df64_sub(vec2 a, vec2 b, float _fp64_g) { return df64_add(a, (-b), _fp64_g);}
vec2 df64_mul(vec2 a, vec2 b, float _fp64_g) { vec2 _v0 = df64_twoProd(a.x, b.x, _fp64_g); _v0.y = (_v0.y + (a.x * b.y)); _v0 = df64_quickTwoSum(_v0.x, _v0.y, _fp64_g); _v0.y = (_v0.y + (a.y * b.x)); return df64_quickTwoSum(_v0.x, _v0.y, _fp64_g);}
vec2 df64_sqr(vec2 a, float _fp64_g) { vec2 _v0 = df64_twoSqr(a.x, _fp64_g); _v0.y = (_v0.y + ((a.x * a.y) * 2.0)); return df64_quickTwoSum(_v0.x, _v0.y, _fp64_g);}
vec2 df64_abs(vec2 a) { return ((a.x < 0.0) ? (-a) : a);}
float df64_narrow(vec2 a) { return (a.x + a.y);}in vec2 uv;layout(location = 0) out vec4 _ret;
void main() { float _fp64_g = texelFetch(_fp64, ivec2(0, 0), 0).x; float _v0 = pow(10.0, (-u.zoom_exp)); float _v1 = (uv.x * 2.0); bool _cse0 = (uv.x < 0.5); float _v2 = (_v1 - (_cse0 ? 0.0 : 1.0)); float _v3 = ((_v2 - 0.5) * _v0); float _v4 = (((uv.y - 0.5) * _v0) * ((u.resolution.y / u.resolution.x) * 2.0)); float _v5 = 0.0; float _v6 = 0.0; vec2 _cse1 = vec2(u.center.hi.x, u.center.lo.x); vec2 _cse2 = vec2(u.center.hi.y, u.center.lo.y); if ((_cse0 || (u.fp64 < 0.5))) { float _v7 = (df64_narrow(_cse1) + _v3); float _v8 = (df64_narrow(_cse2) + _v4); float _v9 = 0.0; float _v10 = 0.0; float _v11 = 0.0; float _v12 = 0.0; for (uint _v13 = 0u; (_v13 < 128u); _v13 = (_v13 + 1u)) { if ((_v6 > 16.0)) { break; } float _v14 = ((_v11 - _v12) + _v7); _v10 = ((abs((_v9 * _v10)) * 2.0) + _v8); _v9 = _v14; _v5 = (_v5 + 1.0); _v11 = (_v9 * _v9); _v12 = (_v10 * _v10); _v6 = (_v11 + _v12); } } else { vec2 _v15 = df64_add(_cse1, vec2(_v3, 0.0), _fp64_g); vec2 _v16 = df64_add(_cse2, vec2(_v4, 0.0), _fp64_g); vec2 _cse3 = vec2(0.0, 0.0); vec2 _v17 = _cse3; vec2 _v18 = _cse3; for (uint _v19 = 0u; (_v19 < 128u); _v19 = (_v19 + 1u)) { if ((_v6 > 16.0)) { break; } vec2 _v20 = df64_add(df64_sub(df64_sqr(_v17, _fp64_g), df64_sqr(_v18, _fp64_g), _fp64_g), _v15, _fp64_g); _v18 = df64_add((df64_abs(df64_mul(_v17, _v18, _fp64_g)) * 2.0), _v16, _fp64_g); _v17 = _v20; _v5 = (_v5 + 1.0); float _v21 = df64_narrow(_v17); float _v22 = df64_narrow(_v18); _v6 = ((_v21 * _v21) + (_v22 * _v22)); } } float _v23 = ((_v5 - log2(max(log2(max(_v6, 1.0001)), 0.0001))) + 1.0); float _v24 = step(127.5, _v5); float _v25 = (_v23 * 0.0078125); float _gv0 = (_v25 * _v25); float _v26 = (_gv0 * (3.0 - (_v25 * 2.0))); _ret = vec4((mix(mix(vec3(0.06, 0.02, 0.05), vec3(0.95, 0.45, 0.08), _v26), vec3(1.0, 0.93, 0.75), _gv0) * (1.0 - _v24)), 1.0);}This example is steered by a control the page has no value for, so the page shows no picture.
The WGSL and GLSL tabs are the compiler's own output at commit 26de7be8, read from the goldens its emit suite bakes (emit-goldens.test.ts).
This example is written against the fn() builder API, which the editor in the Playground does not take.