7.5 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
What this is
A deep-zoom fractal explorer (Rust + wgpu + egui + WGSL). It zooms past the
~10¹³× limit of plain f64 using perturbation theory: one high-precision
reference orbit is computed on the CPU (arbitrary precision via dashu-float),
and every pixel is rendered on the GPU as a cheap f32 delta from it, with
rebasing to avoid glitches. The f32 GPU tier reaches roughly 10³⁰×. Runs
natively (Vulkan/Metal/DX12) and in the browser (WebGPU only — WebGL2 can't do
storage buffers, which the fragment shader needs for the reference orbit).
Commands
cargo run --release # native, run (release matters: fractal math is hot)
cargo test # reference-orbit math, share-link round-trip, WGSL validation
cargo test --test shader_valid # just the WGSL parse/validate tests (naga, no GPU needed)
cargo clippy
cargo fmt # rustfmt.toml just pins edition = "2024"
Web build (WebGPU):
rustup target add wasm32-unknown-unknown
cargo install wasm-bindgen-cli --version 0.2.128 # must match the wasm-bindgen crate version
./build-web.sh # -> ./dist
python3 -m http.server -d dist 8080
Native debug env vars (see src/app.rs, near the top of FractalApp::new):
MANDEL_KIND, MANDEL_POWER, MANDEL_JULIA="re,im", MANDEL_SHARE="<fragment>",
MANDEL_VIEW="re,im,half_height[,iterations]", MANDEL_DE=1,
MANDEL_BUDDHABROT=1, MANDEL_EXPORT=1 (+ MANDEL_EXPORT_PATH=out.png).
There's no GPU in most sandboxes: cargo check/cargo test --test shader_valid
are the fast, headless way to validate a change. cargo test also runs but
doesn't touch the GPU — the reference-orbit tests are pure CPU math (see
below), and shader_valid parses/validates WGSL with naga statically instead
of creating a pipeline.
Architecture
The perturbation pipeline (the core mechanism, spans several files)
For a pixel at parameter c = C_ref + dc, its orbit is written as
y_n = X_n + e_n, where X_n is the (shared, high-precision) reference orbit
and e_n is a small f32 delta. Whenever |y_n| < |e_n| (or the reference
runs out), rebase: e ← y_n − X_0, restart the reference index at 0. This is
what makes deep zoom cheap — one expensive high-precision orbit, then every
pixel is a handful of f32 complex multiplies.
src/view.rs—ViewState; center is arbitrary-precisionFBig(Bigtype alias), pixel scale staysf64(still in-range at 10³⁰×). Precision (bits) scales with zoom depth (precision_for).src/fractal/reference.rs—FractalKindenum (Mandelbrot, Burning Ship, Tricorn, Multibrot, Celtic, Perpendicular, Buffalo, Phoenix, Lambda) andcompute_reference/compute_set_reference: iterate the chosen formula at high precision on the CPU, emittingZ_nasf32pairs — that's the reference orbit the GPU perturbs from.src/shaders/mandelbrot.wgsl— the perturbation fragment shader.advance_delta(z, e)is the per-kind delta step (z= reference point,e= current delta); the caller addsstep_add(=dc) afterward — this relies oncbeing additive in every current kind's formula (a kind where it isn't, e.g. a rational map withcin a denominator, would need its own step function that consumesdcinternally instead, plus extra per-step reference data since the orbit point alone wouldn't be enough to recover an exact delta).fprime(z)is the derivative used for distance-estimation (DE) shading; exact for holomorphic kinds, an approximation (~2Z) for the abs-based ones. AKIND_*constant here must match the matchingFractalKindvariant's discriminant exactly.src/fractal/renderer.rs—FractalRenderer(wgpu pipelines, uniform + storage buffers, bind groups),Uniforms(repr(C) layout that must match the WGSLUniformsstruct field-for-field, including padding), andFractalCallback(theegui_wgpu::CallbackTraitimpl:prepare()uploads changed buffers and decides whether to re-run the iterate pass, the cheap colourise pass, or just blit the cached texture). AlsoExportRender, a self-contained tiled renderer used for PNG export off the UI thread.src/worker.rs— native background thread for reference-orbit computation (coalesces bursts of requests so a fast drag doesn't compute every intermediate view). The wasm32 build computes inline instead (see the#[cfg(target_arch = "wasm32")]branch inapp.rs::ensure_reference) — any signature change tocompute_reference/compute_set_referenceorRefRequest/RefResultmust be applied to both call sites.src/app.rs—FractalApp(the egui app + all UI). Key methods:should_request/ensure_reference(decide when the reference is stale and dispatch/collect it),make_uniforms(assemble the per-frameUniforms),tick_animations(drives the "morph c/p/λ" and auto-zoom animations),default_view_for(per-kind starting view).KINDS,JULIA_PRESETS, andSET_PRESETSare sized as[T; FractalKind::<last variant> as usize + 1]— adding a newFractalKindmeans bumping all three (and adding an empty&[]slot to the two preset arrays if the kind has none).src/fractal/share.rs—ShareState: encodes the full view (mode, kind, full-precision decimal center, zoom, iterations, per-kind constants, coloring) as a#-fragment URL for bookmarking/sharing deep-zoom locations.
Adding a new FractalKind
Touches, in order: reference.rs (enum variant + CPU iteration formula, and a
test comparing against a naive f64 iteration), mandelbrot.wgsl (matching
KIND_* const + advance_delta/fprime arms), buddhabrot.wgsl (matching
arm in advance(), if the kind makes sense as a Buddhabrot), renderer.rs
Uniforms (only if the kind needs a new per-kind constant, e.g. Phoenix's
phoenix_p), share.rs (encode/decode string tag), app.rs (KINDS label,
JULIA_PRESETS/SET_PRESETS slot, default_view_for entry, and optionally a
UI control for its constant + an animation toggle, following the
Phoenix/Lambda pattern). If c doesn't enter the formula additively (e.g. a
rational map with c in a denominator), the advance_delta/step_add split
doesn't work — that needs its own step function plus extra per-step reference
data uploaded in a second GPU buffer alongside the orbit.
Buddhabrot is a separate pipeline
src/fractal/buddhabrot.rs + src/shaders/buddhabrot.wgsl implement the
Monte-Carlo orbit-density histogram. It does not use the perturbation/
reference-orbit machinery: a Buddhabrot sample's orbit scatters across the
whole image rather than staying in one pixel, so it's plain f32 iteration
from the live view (no deep zoom) via a compute pass that accumulates into a
histogram buffer, tone-mapped by a fragment pass every frame. Its own
KIND_* iteration formulas in advance() must be kept in sync with
reference.rs by hand (there's no shared code path).
Two-pass render + caching (renderer.rs)
The interactive path splits iteration (expensive, perturbation) from
colourising (cheap, palette remap) into separate offscreen textures, so
palette/color-scale/offset tweaks skip re-iteration entirely (geom_differs
vs color_differs in renderer.rs decide which pass reruns). While the user
is actively panning/zooming, the app renders downscaled with AA off
(INTERACT_DOWNSCALE) and snaps back to full resolution once input settles
(INTERACT_SETTLE).