Files
mandelbrot/CLAUDE.md
T

8.7 KiB
Raw Blame History

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 CLI flags (src/cli.rs, applied in FractalApp::apply_cli): --kind, --power, --julia re,im, --share <fragment>, --view re,im,half_height[,iterations], --de, --buddhabrot, --buddha-palette, --export (+ --export-path out.png). --headless (src/headless.rs) skips the window entirely: it builds the same view from the other flags, creates its own offscreen wgpu device, and renders straight to a PNG (--width/--height, default 1920×1080) — implies --export's save behavior without needing a GPU-backed window/event loop. Not yet supported with --buddhabrot. Run mandelbrot --help for the full list.

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-precision FBig (Big type alias), pixel scale stays f64 (still in-range at 10³⁰×). Precision (bits) scales with zoom depth (precision_for).
  • src/fractal/reference.rs — FractalKind enum (Mandelbrot, Burning Ship, Tricorn, Multibrot, Celtic, Perpendicular, Buffalo, Phoenix, Lambda) and compute_reference/compute_set_reference: iterate the chosen formula at high precision on the CPU, emitting Z_n as f32 pairs — that's the reference orbit the GPU perturbs from.
  • src/shaders/*.wgsl — none of these are standalone WGSL modules; WGSL has no #include, so each is compiled by concatenating plain-text fragments with concat!/include_str! at the create_shader_module call site (see renderer.rs, buddhabrot.rs, and tests/shader_valid.rs, which must concatenate the same pieces to validate what actually gets built). common.wgsl (fullscreen-triangle vertex helper, cmul/cpow, KIND_* constants) is prepended to every shader. iterate_uniforms.wgsl (the perturbation-pipeline Uniforms struct + palette()) is additionally prepended to mandelbrot.wgsl and colorize.wgsl, which share that layout. Because there's no namespacing, a definition must live in exactly one file among those concatenated together for a given shader — don't redefine a common.wgsl/iterate_uniforms.wgsl symbol locally.
  • 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 adds step_add (= dc) afterward — this relies on c being additive in every current kind's formula (a kind where it isn't, e.g. a rational map with c in a denominator, would need its own step function that consumes dc internally 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. A KIND_* constant (from common.wgsl) must match the matching FractalKind variant's discriminant exactly.
  • src/fractal/renderer.rs — FractalRenderer (wgpu pipelines, uniform + storage buffers, bind groups), Uniforms (repr(C) layout that must match the WGSL Uniforms struct field-for-field, including padding), and FractalCallback (the egui_wgpu::CallbackTrait impl: prepare() uploads changed buffers and decides whether to re-run the iterate pass, the cheap colourise pass, or just blit the cached texture). Also ExportRender, 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 in app.rs::ensure_reference) — any signature change to compute_reference/compute_set_reference or RefRequest/RefResult must 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-frame Uniforms), tick_animations (drives the "morph c/p/λ" and auto-zoom animations), default_view_for (per-kind starting view). KINDS, JULIA_PRESETS, and SET_PRESETS are sized as [T; FractalKind::<last variant> as usize + 1] — adding a new FractalKind means 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), common.wgsl (matching KIND_* const), mandelbrot.wgsl (matching 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).