feat: Add a CLAUDE.md file
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
# 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
|
||||
|
||||
```sh
|
||||
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):
|
||||
|
||||
```sh
|
||||
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-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/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 here 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), `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`).
|
||||
Reference in New Issue
Block a user