Compare commits
10
Commits
4212839f0d
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da992c139a | ||
|
|
49130f5ecf | ||
|
|
493a743a16 | ||
|
|
0d7aadb991 | ||
|
|
ae7ca97c41 | ||
|
|
3f5853a23c | ||
|
|
2e116ba47b | ||
|
|
a15e951f84 | ||
|
|
27c5a1a603 | ||
|
|
d78594af7e |
+1
-1
@@ -3,4 +3,4 @@ Cargo.lock
|
|||||||
dist
|
dist
|
||||||
frames*
|
frames*
|
||||||
out.mp4
|
out.mp4
|
||||||
|
__pycache__
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
A deep-zoom fractal explorer (Rust + wgpu + egui + WGSL). It zooms past the
|
A deep-zoom fractal explorer (Rust + wgpu + egui + WGSL). It zooms past the
|
||||||
~10¹³× limit of plain `f64` using **perturbation theory**: one high-precision
|
~10¹³× limit of plain `f64` using **perturbation theory**: one high-precision
|
||||||
reference orbit is computed on the CPU (arbitrary precision via `dashu-float`),
|
reference orbit is computed on the CPU (arbitrary precision via `bignum::Big`:
|
||||||
|
`rug` natively, `malachite-float` on the web),
|
||||||
and every pixel is rendered on the GPU as a cheap `f32` delta from it, with
|
and every pixel is rendered on the GPU as a cheap `f32` delta from it, with
|
||||||
rebasing to avoid glitches. Plain `f32` deltas run out of exponent range
|
rebasing to avoid glitches. Plain `f32` deltas run out of exponent range
|
||||||
once a pixel is ~2^-124 wide (~10³⁴× at 1080p), so from 2^-122 per pixel
|
once a pixel is ~2^-124 wide (~10³⁴× at 1080p), so from 2^-122 per pixel
|
||||||
@@ -22,9 +23,12 @@ storage buffers, which the fragment shader needs for the reference orbit).
|
|||||||
```sh
|
```sh
|
||||||
cargo run --release # native, run (release matters: fractal math is hot)
|
cargo run --release # native, run (release matters: fractal math is hot)
|
||||||
cargo test # reference-orbit math, share-link round-trip, WGSL validation
|
cargo test # reference-orbit math, share-link round-trip, WGSL validation
|
||||||
|
cargo test --features wasm # same, on the web build's malachite big-float backend
|
||||||
cargo test --test shader_valid # just the WGSL parse/validate tests (naga, no GPU needed)
|
cargo test --test shader_valid # just the WGSL parse/validate tests (naga, no GPU needed)
|
||||||
cargo clippy
|
cargo clippy
|
||||||
cargo fmt # rustfmt.toml just pins edition = "2024"
|
cargo fmt # rustfmt.toml just pins edition = "2024"
|
||||||
|
cargo build --release --no-default-features # headless-only binary: no eframe/egui (the default `gui` feature)
|
||||||
|
tools/bench/bench.sh --rev HEAD # perf of uncommitted edits vs HEAD (hyperfine; see tools/bench/README.md)
|
||||||
```
|
```
|
||||||
|
|
||||||
Web build (WebGPU):
|
Web build (WebGPU):
|
||||||
@@ -32,7 +36,7 @@ Web build (WebGPU):
|
|||||||
```sh
|
```sh
|
||||||
rustup target add wasm32-unknown-unknown
|
rustup target add wasm32-unknown-unknown
|
||||||
cargo install wasm-bindgen-cli --version 0.2.128 # must match the wasm-bindgen crate version
|
cargo install wasm-bindgen-cli --version 0.2.128 # must match the wasm-bindgen crate version
|
||||||
./build-web.sh # -> ./dist
|
./build-web.sh # -> ./dist (builds with --features wasm)
|
||||||
python3 -m http.server -d dist 8080
|
python3 -m http.server -d dist 8080
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -40,7 +44,8 @@ Native CLI flags (`src/cli.rs`, applied in `FractalApp::apply_cli`): `--kind`,
|
|||||||
`--power`, `--julia re,im`, `--phoenix-p re,im`, `--lambda-l re,im`,
|
`--power`, `--julia re,im`, `--phoenix-p re,im`, `--lambda-l re,im`,
|
||||||
`--palette`, `--share <fragment>`,
|
`--palette`, `--share <fragment>`,
|
||||||
`--view re,im,half_height[,iterations]`, `--rendering-kind`,
|
`--view re,im,half_height[,iterations]`, `--rendering-kind`,
|
||||||
`--yaw`/`--pitch` (3D camera, degrees), `--de`, `--buddhabrot`,
|
`--yaw`/`--pitch` (3D camera, degrees), `--de`, `--antialias` (2×2),
|
||||||
|
`--buddhabrot`,
|
||||||
`--buddha-palette`. `--headless` (`src/headless.rs`) skips the window
|
`--buddha-palette`. `--headless` (`src/headless.rs`) skips the window
|
||||||
entirely: it builds the same view from the other flags, creates its own
|
entirely: it builds the same view from the other flags, creates its own
|
||||||
offscreen wgpu device, and renders straight to a PNG (`--width`/`--height`,
|
offscreen wgpu device, and renders straight to a PNG (`--width`/`--height`,
|
||||||
@@ -58,13 +63,22 @@ position/zoom/iterations are pulled out of the link), `--to-iterations`,
|
|||||||
and `--to-kind` (per-step formula blend via `KindMorph`, camera untouched).
|
and `--to-kind` (per-step formula blend via `KindMorph`, camera untouched).
|
||||||
Anything without a target stays at its start value; colors stay fixed.
|
Anything without a target stays at its start value; colors stay fixed.
|
||||||
`--export-path` then names an output *directory* of `frame-00001.png`,
|
`--export-path` then names an output *directory* of `frame-00001.png`,
|
||||||
`frame-00002.png`, ... instead of a single file. `headless.rs::AnimTargets`
|
`frame-00002.png`, ... instead of a single file. `--export-path -` writes
|
||||||
|
to stdout instead (refused on a terminal): the PNG for a still, or for an
|
||||||
|
animation raw RGBA8 frames in order (`unpad_rgba`, no PNG encode) for
|
||||||
|
`ffmpeg -f rawvideo -pix_fmt rgba -s WxH -r FPS -i -`. A single writer
|
||||||
|
thread reorders the frames. Its buffer is bounded by the orbit workers not
|
||||||
|
starting a frame more than `window` past the last one written, not by
|
||||||
|
blocking the writer, which could deadlock. `headless.rs::AnimTargets`
|
||||||
collects the targets; the export pipeline is rebuilt only when the
|
collects the targets; the export pipeline is rebuilt only when the
|
||||||
`PipelineKey` changes between frames (kind morph). `view::interpolate_view`
|
`PipelineKey` changes between frames (kind morph). `view::interpolate_view`
|
||||||
does the camera: half-height geometrically (log-linear, since zoom spans many
|
does the camera: half-height geometrically (log-linear, since zoom spans many
|
||||||
decades), center linearly through the complex plane at full `Big` precision;
|
decades), center linearly through the complex plane at full `Big` precision;
|
||||||
constants interpolate linearly. `--linear` swaps the default smoothstep
|
constants interpolate linearly. `--linear` swaps the default smoothstep
|
||||||
easing for constant pacing. Without `--to-iterations` (or a share link's),
|
easing for constant pacing. `--shards N --shard K` (1-based) renders only
|
||||||
|
the K-th of N contiguous parts (`shard_range`), still timed against the whole
|
||||||
|
animation (global `t`, global `frame-NNNNN.png` numbers; stdout streams just
|
||||||
|
that part), so separately rendered clips join seamlessly. Without `--to-iterations` (or a share link's),
|
||||||
iteration count auto-scales with zoom depth per frame (same
|
iteration count auto-scales with zoom depth per frame (same
|
||||||
`auto_iteration_count` the interactive app uses while zooming).
|
`auto_iteration_count` the interactive app uses while zooming).
|
||||||
`--to-yaw`/`--to-pitch` (degrees, from `--yaw`/`--pitch`, yaw unwrapped so
|
`--to-yaw`/`--to-pitch` (degrees, from `--yaw`/`--pitch`, yaw unwrapped so
|
||||||
@@ -94,8 +108,19 @@ runs out), rebase: `e ← y_n − X_0`, restart the reference index at 0. This i
|
|||||||
what makes deep zoom cheap — one expensive high-precision orbit, then every
|
what makes deep zoom cheap — one expensive high-precision orbit, then every
|
||||||
pixel is a handful of `f32` complex multiplies.
|
pixel is a handful of `f32` complex multiplies.
|
||||||
|
|
||||||
- `src/view.rs` — `ViewState`; center is arbitrary-precision `FBig` (`Big`
|
- `src/bignum/` — `Big`, the arbitrary-precision binary float, with one
|
||||||
type alias). The pixel scale (`half_height`) is a `Scale`, an f64
|
backend per library behind the same inherent API + operators: `rug`
|
||||||
|
(GMP/MPFR, default, fastest, can't target wasm32) and `malachite-float`
|
||||||
|
(pure Rust, the `wasm` feature, required for the web build; a
|
||||||
|
`compile_error!` enforces it). Cargo features are additive, so rug is a
|
||||||
|
non-wasm32 target dependency and the backend is picked by
|
||||||
|
`cfg(feature = "wasm")`. Both follow the precision rule: a result has the
|
||||||
|
larger operand precision, rounded to nearest; shifts are exact. Malachite's
|
||||||
|
zero has no precision, so its wrapper stores `prec` alongside. Any new
|
||||||
|
`Big` operation must be added to both backends (`cargo test` and
|
||||||
|
`cargo test --features wasm` run the same tests on each).
|
||||||
|
- `src/view.rs` — `ViewState`; center is arbitrary-precision `Big`
|
||||||
|
(re-exported as `view::Big`). The pixel scale (`half_height`) is a `Scale`, an f64
|
||||||
mantissa with its own i32 exponent, so it goes past f64's ~1e-308. Never
|
mantissa with its own i32 exponent, so it goes past f64's ~1e-308. Never
|
||||||
collapse it (or a center difference) to a plain `f64` on a path used at
|
collapse it (or a center difference) to a plain `f64` on a path used at
|
||||||
depth. Rescale first: `Scale::scaled_f64(k)`, or shift the `Big` by
|
depth. Rescale first: `Scale::scaled_f64(k)`, or shift the `Big` by
|
||||||
@@ -120,10 +145,10 @@ pixel is a handful of `f32` complex multiplies.
|
|||||||
`f32` pairs — that's the reference orbit the GPU perturbs from. At
|
`f32` pairs — that's the reference orbit the GPU perturbs from. At
|
||||||
precision ≤ `F64_MAX_PRECISION` (80 bits, i.e. shallow views) it takes a
|
precision ≤ `F64_MAX_PRECISION` (80 bits, i.e. shallow views) it takes a
|
||||||
plain-`f64` fast path (`compute_reference_f64`), so each kind's formula
|
plain-`f64` fast path (`compute_reference_f64`), so each kind's formula
|
||||||
exists twice in this file (f64 + `FBig`) and both must stay in sync;
|
exists twice in this file (f64 + `Big`) and both must stay in sync;
|
||||||
`f64_fast_path_matches_big` checks they agree. The result is a `RefOrbit`:
|
`f64_fast_path_matches_big` checks they agree. The result is a `RefOrbit`:
|
||||||
`points` plus a parallel `exps`. A point below 2^-100 (only possible on the
|
`points` plus a parallel `exps`. A point below 2^-100 (only possible on the
|
||||||
`FBig` path) is stored as a normalized mantissa with its exponent in `exps`
|
`Big` path) is stored as a normalized mantissa with its exponent in `exps`
|
||||||
(the true value is `points[n]·2^exps[n]`). That happens when the orbit
|
(the true value is `points[n]·2^exps[n]`). That happens when the orbit
|
||||||
passes near 0 at a deep minibrot. `has_scaled()` then forces the deep
|
passes near 0 at a deep minibrot. `has_scaled()` then forces the deep
|
||||||
pipeline, the only one that reads `exps`. Requests are made with 1.5×
|
pipeline, the only one that reads `exps`. Requests are made with 1.5×
|
||||||
@@ -179,7 +204,7 @@ pixel is a handful of `f32` complex multiplies.
|
|||||||
are `advance_delta_kind`/`fprime_kind`; `advance_delta`/`fprime` wrap them
|
are `advance_delta_kind`/`fprime_kind`; `advance_delta`/`fprime` wrap them
|
||||||
to blend two kinds during the kind-switch morph (`u.morph_from`,
|
to blend two kinds during the kind-switch morph (`u.morph_from`,
|
||||||
`u.morph_w`: each step is `(1-w)·f_kind + w·f_from`, mirrored on the CPU by
|
`u.morph_w`: each step is `(1-w)·f_kind + w·f_from`, mirrored on the CPU by
|
||||||
the `morph` argument of `compute_reference`, in both its f64 and `FBig`
|
the `morph` argument of `compute_reference`, in both its f64 and `Big`
|
||||||
paths). The blend only exists in pipelines built with the `MORPH` override
|
paths). The blend only exists in pipelines built with the `MORPH` override
|
||||||
(part of `PipelineKey`, on while `morph_w > 0`); those also skip periodicity
|
(part of `PipelineKey`, on while `morph_w > 0`); those also skip periodicity
|
||||||
detection and the cardioid bypass. App side: `KindMorph` in
|
detection and the cardioid bypass. App side: `KindMorph` in
|
||||||
|
|||||||
+16
-4
@@ -3,19 +3,31 @@ name = "mandelbrot"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["gui"]
|
||||||
|
# Windowed egui app. Without it only `--headless` rendering is built.
|
||||||
|
gui = ["dep:eframe", "dep:egui"]
|
||||||
|
# Pure-Rust big floats for the web build (`rug` needs GMP/MPFR, which can't
|
||||||
|
# target wasm32). Required for wasm32, optional natively (to test that backend).
|
||||||
|
wasm = ["dep:malachite-float", "dep:malachite-base"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bytemuck = { version = "1.25.2", features = ["derive"] }
|
bytemuck = { version = "1.25.2", features = ["derive"] }
|
||||||
dashu-float = "0.6.0"
|
eframe = { version = "0.36.2", default-features = false, features = ["wgpu", "default_fonts", "x11", "wayland", "accesskit"], optional = true }
|
||||||
eframe = { version = "0.36.2", default-features = false, features = ["wgpu", "default_fonts", "x11", "wayland", "accesskit"] }
|
egui = { version = "0.36.2", optional = true }
|
||||||
egui = "0.36.2"
|
ecolor = { version = "0.36.2", features = ["bytemuck"] }
|
||||||
|
wgpu = "30.0.1"
|
||||||
glam = "0.33.8"
|
glam = "0.33.8"
|
||||||
log = "0.4.34"
|
log = "0.4.34"
|
||||||
png = "0.18.1"
|
png = "0.18.1"
|
||||||
|
malachite-float = { version = "0.12", optional = true }
|
||||||
|
malachite-base = { version = "0.12", optional = true }
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
env_logger = "0.11.11"
|
env_logger = "0.11.11"
|
||||||
clap = { version = "4.5.51", features = ["derive"] }
|
clap = { version = "4.5.51", features = ["derive"] }
|
||||||
pollster = "1.0.1"
|
pollster = "1.0.1"
|
||||||
|
rug = { version = "1.30.0", default-features = false, features = ["float", "std"] }
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
futures-channel = { version = "0.3.34", default-features = false, features = ["alloc", "std"] }
|
futures-channel = { version = "0.3.34", default-features = false, features = ["alloc", "std"] }
|
||||||
@@ -32,7 +44,7 @@ opt-level = 3
|
|||||||
# codegen-units = 1
|
# codegen-units = 1
|
||||||
debug = true
|
debug = true
|
||||||
|
|
||||||
# Dev: keep our own crate debuggable, but optimize dependencies (dashu, wgpu,
|
# Dev: keep our own crate debuggable, but optimize dependencies (big floats, wgpu,
|
||||||
# egui) so the explorer is actually interactive during development.
|
# egui) so the explorer is actually interactive during development.
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
opt-level = 1
|
opt-level = 1
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
.PHONY: hevc-nvenc x265 av1
|
||||||
|
|
||||||
|
FFMPEG=ffmpeg
|
||||||
|
IN_FMT=rgba
|
||||||
|
|
||||||
|
RESOLUTION=1920x1080
|
||||||
|
FPS=60
|
||||||
|
OUT=out.mp4
|
||||||
|
|
||||||
|
FFMPEG_CMD=$(FFMPEG) -f rawvideo -pix_fmt $(IN_FMT) -s $(RESOLUTION) -framerate $(FPS) -i - -vf "scale=out_color_matrix=bt709:out_range=tv:flags=accurate_rnd+full_chroma_int+bitexact,format=p010le"
|
||||||
|
|
||||||
|
hevc-nvenc:
|
||||||
|
$(FFMPEG_CMD) \
|
||||||
|
-c:v hevc_nvenc -preset p7 -tune hq -rc vbr -cq 14 -b:v 0 -maxrate 200M -bufsize 400M \
|
||||||
|
-profile:v main10 -pix_fmt p010le \
|
||||||
|
-spatial-aq 1 -aq-strength 6 -temporal-aq 1 \
|
||||||
|
-rc-lookahead 32 -bf 4 -b_ref_mode middle -multipass fullres \
|
||||||
|
-colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv \
|
||||||
|
-tag:v hvc1 -movflags +faststart \
|
||||||
|
$(OUT)
|
||||||
|
|
||||||
|
x265:
|
||||||
|
$(FFMPEG_CMD) \
|
||||||
|
-c:v libx265 -crf 18 -preset medium -pix_fmt yuv420p10le \
|
||||||
|
-x265-params "aq-mode=3:no-sao=1" \
|
||||||
|
-colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv \
|
||||||
|
-tag:v hvc1 -movflags +faststart \
|
||||||
|
$(OUT)
|
||||||
|
|
||||||
|
av1:
|
||||||
|
$(FFMPEG_CMD) \
|
||||||
|
-c:v libsvtav1 -crf 24 -preset 6 -pix_fmt yuv420p10le \
|
||||||
|
-svtav1-params "film-grain=0:enable-overlays=0" \
|
||||||
|
-colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv \
|
||||||
|
$(OUT)
|
||||||
@@ -3,7 +3,8 @@
|
|||||||
A fast, interactive deep-zoom fractal explorer — Mandelbrot and Julia sets —
|
A fast, interactive deep-zoom fractal explorer — Mandelbrot and Julia sets —
|
||||||
built with **Rust + wgpu + egui + WGSL**. It zooms far past the ~10¹³× limit of
|
built with **Rust + wgpu + egui + WGSL**. It zooms far past the ~10¹³× limit of
|
||||||
plain `f64` using **perturbation theory**: one high-precision reference orbit is
|
plain `f64` using **perturbation theory**: one high-precision reference orbit is
|
||||||
computed on the CPU (arbitrary precision via `dashu-float`), and every pixel is
|
computed on the CPU (arbitrary precision via `rug` natively, `malachite-float`
|
||||||
|
on the web), and every pixel is
|
||||||
rendered on the GPU as a cheap `f32` delta from it, with **rebasing** to avoid
|
rendered on the GPU as a cheap `f32` delta from it, with **rebasing** to avoid
|
||||||
glitches. Runs natively (Vulkan/Metal/DX12) and in the browser (WebGPU).
|
glitches. Runs natively (Vulkan/Metal/DX12) and in the browser (WebGPU).
|
||||||
|
|
||||||
@@ -27,6 +28,9 @@ The `f32` GPU tier reaches roughly **10³⁰× magnification** with sharp detail
|
|||||||
cargo run --release
|
cargo run --release
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Native builds use `rug` (GMP/MPFR) for the reference orbit, which needs a C
|
||||||
|
toolchain and `m4` (on Windows, MSYS2).
|
||||||
|
|
||||||
## Build & run — web (WebGPU)
|
## Build & run — web (WebGPU)
|
||||||
|
|
||||||
Requires the `wasm32-unknown-unknown` target and `wasm-bindgen-cli` (matching the
|
Requires the `wasm32-unknown-unknown` target and `wasm-bindgen-cli` (matching the
|
||||||
@@ -37,6 +41,7 @@ rustup target add wasm32-unknown-unknown
|
|||||||
cargo install wasm-bindgen-cli --version 0.2.128 # once
|
cargo install wasm-bindgen-cli --version 0.2.128 # once
|
||||||
|
|
||||||
./build-web.sh # outputs ./dist (index.html, .js, .wasm)
|
./build-web.sh # outputs ./dist (index.html, .js, .wasm)
|
||||||
|
# (builds with --features wasm: pure-Rust big floats)
|
||||||
python3 -m http.server -d dist 8080 # serve over http
|
python3 -m http.server -d dist 8080 # serve over http
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -60,7 +65,8 @@ qualifies. Deploy by serving the `dist/` directory as static files.
|
|||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
- `src/view.rs` — view state. Center is arbitrary precision (`FBig`); the pixel
|
- `src/view.rs` — view state. Center is arbitrary precision (`Big`, from
|
||||||
|
`src/bignum/`); the pixel
|
||||||
scale stays `f64` (even at 10³⁰× it is ~10⁻³³, within `f64` range).
|
scale stays `f64` (even at 10³⁰× it is ~10⁻³³, within `f64` range).
|
||||||
- `src/fractal/reference.rs` — high-precision reference orbit `Z_{n+1}=Z_n²+C`.
|
- `src/fractal/reference.rs` — high-precision reference orbit `Z_{n+1}=Z_n²+C`.
|
||||||
- `src/shaders/mandelbrot.wgsl` — per-pixel perturbation `e_{n+1}=2·Z_n·e_n+e_n²+δc`
|
- `src/shaders/mandelbrot.wgsl` — per-pixel perturbation `e_{n+1}=2·Z_n·e_n+e_n²+δc`
|
||||||
@@ -84,6 +90,7 @@ reference computation to a Web Worker.
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo test
|
cargo test
|
||||||
|
cargo test --features wasm # same suite on the web build's big-float backend
|
||||||
```
|
```
|
||||||
|
|
||||||
Covers the reference orbit (vs. a naive `f64` iteration, Mandelbrot and Julia)
|
Covers the reference orbit (vs. a naive `f64` iteration, Mandelbrot and Julia)
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ export PATH="$HOME/.cargo/bin:$PATH"
|
|||||||
OUT="${1:-dist}"
|
OUT="${1:-dist}"
|
||||||
|
|
||||||
echo "==> cargo build (wasm32, release)"
|
echo "==> cargo build (wasm32, release)"
|
||||||
cargo build --release --target wasm32-unknown-unknown
|
cargo build --release --target wasm32-unknown-unknown --features wasm
|
||||||
|
|
||||||
echo "==> wasm-bindgen -> $OUT"
|
echo "==> wasm-bindgen -> $OUT"
|
||||||
mkdir -p "$OUT"
|
mkdir -p "$OUT"
|
||||||
|
|||||||
+33
-20
@@ -1,9 +1,9 @@
|
|||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
use eframe::CreationContext;
|
use eframe::CreationContext;
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
use eframe::egui_wgpu;
|
use eframe::egui_wgpu;
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
use eframe::egui_wgpu::wgpu;
|
|
||||||
use glam::Vec4;
|
use glam::Vec4;
|
||||||
use glam::Vec4Swizzles;
|
use glam::Vec4Swizzles;
|
||||||
|
|
||||||
@@ -316,6 +316,7 @@ impl ConstOrbit {
|
|||||||
|
|
||||||
/// Checkbox + speed/radius sliders; (re)centers the orbit on `current`
|
/// Checkbox + speed/radius sliders; (re)centers the orbit on `current`
|
||||||
/// when switched on.
|
/// when switched on.
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
fn ui(&mut self, ui: &mut egui::Ui, name: &str, current: (f64, f64)) {
|
fn ui(&mut self, ui: &mut egui::Ui, name: &str, current: (f64, f64)) {
|
||||||
if ui.checkbox(&mut self.on, format!("Morph {name}")).changed() && self.on {
|
if ui.checkbox(&mut self.on, format!("Morph {name}")).changed() && self.on {
|
||||||
self.enable(current);
|
self.enable(current);
|
||||||
@@ -371,6 +372,7 @@ impl AxisOsc {
|
|||||||
self.base + self.amplitude * self.phase.sin()
|
self.base + self.amplitude * self.phase.sin()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
fn ui(&mut self, ui: &mut egui::Ui, name: &str, current: f64) {
|
fn ui(&mut self, ui: &mut egui::Ui, name: &str, current: f64) {
|
||||||
if ui
|
if ui
|
||||||
.checkbox(&mut self.on, format!("Animate {name}"))
|
.checkbox(&mut self.on, format!("Animate {name}"))
|
||||||
@@ -594,6 +596,7 @@ pub struct FractalApp {
|
|||||||
/// PNG export resolution multiplier over the on-screen size.
|
/// PNG export resolution multiplier over the on-screen size.
|
||||||
export_scale: f32,
|
export_scale: f32,
|
||||||
/// Last on-screen fractal size in physical pixels (for export sizing).
|
/// Last on-screen fractal size in physical pixels (for export sizing).
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
last_size_px: egui::Vec2,
|
last_size_px: egui::Vec2,
|
||||||
/// egui time (seconds) of the most recent pan/zoom. While recent (within
|
/// egui time (seconds) of the most recent pan/zoom. While recent (within
|
||||||
/// `INTERACT_SETTLE`) the fractal renders downscaled for smooth interaction.
|
/// `INTERACT_SETTLE`) the fractal renders downscaled for smooth interaction.
|
||||||
@@ -646,6 +649,7 @@ fn parse_bits_for(s: &str, min_bits: usize) -> usize {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FractalApp {
|
impl FractalApp {
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
pub fn new(cc: &CreationContext<'_>) -> Self {
|
pub fn new(cc: &CreationContext<'_>) -> Self {
|
||||||
let render_state = cc
|
let render_state = cc
|
||||||
.wgpu_render_state
|
.wgpu_render_state
|
||||||
@@ -740,6 +744,7 @@ impl FractalApp {
|
|||||||
pending: false,
|
pending: false,
|
||||||
export_scale: 2.0,
|
export_scale: 2.0,
|
||||||
render_scale_3d: 2.0,
|
render_scale_3d: 2.0,
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
last_size_px: egui::vec2(1280.0, 720.0),
|
last_size_px: egui::vec2(1280.0, 720.0),
|
||||||
last_interact_time: -1.0e9,
|
last_interact_time: -1.0e9,
|
||||||
export_requested: false,
|
export_requested: false,
|
||||||
@@ -842,6 +847,9 @@ impl FractalApp {
|
|||||||
if cli.de {
|
if cli.de {
|
||||||
self.de_coloring = true;
|
self.de_coloring = true;
|
||||||
}
|
}
|
||||||
|
if cli.antialias {
|
||||||
|
self.antialias = true;
|
||||||
|
}
|
||||||
if cli.buddhabrot {
|
if cli.buddhabrot {
|
||||||
self.mode = FractalMode::Buddhabrot;
|
self.mode = FractalMode::Buddhabrot;
|
||||||
}
|
}
|
||||||
@@ -1136,12 +1144,8 @@ impl FractalApp {
|
|||||||
fn drift_from(&self, key: &RequestKey) -> f64 {
|
fn drift_from(&self, key: &RequestKey) -> f64 {
|
||||||
let hh = self.view.half_height;
|
let hh = self.view.half_height;
|
||||||
let k = -hh.exponent() as isize;
|
let k = -hh.exponent() as isize;
|
||||||
let dre = ((&self.view.center_re - &key.center_re) << k)
|
let dre = ((&self.view.center_re - &key.center_re) << k).to_f64();
|
||||||
.to_f64()
|
let dim = ((&self.view.center_im - &key.center_im) << k).to_f64();
|
||||||
.value();
|
|
||||||
let dim = ((&self.view.center_im - &key.center_im) << k)
|
|
||||||
.to_f64()
|
|
||||||
.value();
|
|
||||||
(dre * dre + dim * dim).sqrt() / hh.scaled_f64(-hh.exponent())
|
(dre * dre + dim * dim).sqrt() / hh.scaled_f64(-hh.exponent())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1190,12 +1194,8 @@ impl FractalApp {
|
|||||||
/// underflow f64 at deep zooms).
|
/// underflow f64 at deep zooms).
|
||||||
fn dc_offset(&self, scale_exp: i32) -> (f64, f64) {
|
fn dc_offset(&self, scale_exp: i32) -> (f64, f64) {
|
||||||
let k = -scale_exp as isize;
|
let k = -scale_exp as isize;
|
||||||
let dre = ((&self.view.center_re - &self.ref_center_re) << k)
|
let dre = ((&self.view.center_re - &self.ref_center_re) << k).to_f64();
|
||||||
.to_f64()
|
let dim = ((&self.view.center_im - &self.ref_center_im) << k).to_f64();
|
||||||
.value();
|
|
||||||
let dim = ((&self.view.center_im - &self.ref_center_im) << k)
|
|
||||||
.to_f64()
|
|
||||||
.value();
|
|
||||||
(dre, dim)
|
(dre, dim)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1414,6 +1414,7 @@ impl FractalApp {
|
|||||||
/// 3D-mode zoom toward the screen point `off` (points from the widget
|
/// 3D-mode zoom toward the screen point `off` (points from the widget
|
||||||
/// center): unproject it through the camera onto the z = 0 fractal plane,
|
/// center): unproject it through the camera onto the z = 0 fractal plane,
|
||||||
/// then zoom the 2D view about the matching fractal-texture pixel.
|
/// then zoom the 2D view about the matching fractal-texture pixel.
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
fn zoom_3d_at(&mut self, off: egui::Vec2, rect: egui::Rect, height_px: f64, factor: f64) {
|
fn zoom_3d_at(&mut self, off: egui::Vec2, rect: egui::Rect, height_px: f64, factor: f64) {
|
||||||
let ndc = (off / rect.size()) * 2.;
|
let ndc = (off / rect.size()) * 2.;
|
||||||
let camera_ndc_pos = self
|
let camera_ndc_pos = self
|
||||||
@@ -1485,8 +1486,8 @@ impl FractalApp {
|
|||||||
/// Buddhabrot mode doesn't support deep zoom (see `fractal::buddhabrot`).
|
/// Buddhabrot mode doesn't support deep zoom (see `fractal::buddhabrot`).
|
||||||
fn make_buddhabrot_uniforms(&self, aspect: f64) -> BuddhabrotUniforms {
|
fn make_buddhabrot_uniforms(&self, aspect: f64) -> BuddhabrotUniforms {
|
||||||
let center = [
|
let center = [
|
||||||
self.view.center_re.to_f64().value() as f32,
|
self.view.center_re.to_f64() as f32,
|
||||||
self.view.center_im.to_f64().value() as f32,
|
self.view.center_im.to_f64() as f32,
|
||||||
];
|
];
|
||||||
BuddhabrotUniforms {
|
BuddhabrotUniforms {
|
||||||
center,
|
center,
|
||||||
@@ -1515,6 +1516,7 @@ impl FractalApp {
|
|||||||
/// Render the current view to a PNG at `export_scale` × the on-screen size,
|
/// Render the current view to a PNG at `export_scale` × the on-screen size,
|
||||||
/// then save it (native: file in cwd; web: browser download). Runs off the
|
/// then save it (native: file in cwd; web: browser download). Runs off the
|
||||||
/// UI thread so a progress bar can animate; progress lands in `self.export`.
|
/// UI thread so a progress bar can animate; progress lands in `self.export`.
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
fn do_export(&mut self, frame: &mut eframe::Frame) {
|
fn do_export(&mut self, frame: &mut eframe::Frame) {
|
||||||
if self.export.is_some() {
|
if self.export.is_some() {
|
||||||
return; // one export at a time
|
return; // one export at a time
|
||||||
@@ -1641,6 +1643,7 @@ impl FractalApp {
|
|||||||
|
|
||||||
/// Pick up a finished export (setting the status line) and keep repainting
|
/// Pick up a finished export (setting the status line) and keep repainting
|
||||||
/// while one is in flight so its progress bar animates.
|
/// while one is in flight so its progress bar animates.
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
fn poll_export(&mut self, ctx: &egui::Context) {
|
fn poll_export(&mut self, ctx: &egui::Context) {
|
||||||
if let Some(shared) = &self.export {
|
if let Some(shared) = &self.export {
|
||||||
let done = shared.lock().unwrap().result.take();
|
let done = shared.lock().unwrap().result.take();
|
||||||
@@ -1661,6 +1664,7 @@ impl FractalApp {
|
|||||||
/// Floating top-left overlay with the panel toggle and fullscreen toggle.
|
/// Floating top-left overlay with the panel toggle and fullscreen toggle.
|
||||||
/// Always on top of the fractal, so both stay reachable when the controls
|
/// Always on top of the fractal, so both stay reachable when the controls
|
||||||
/// panel is collapsed (the common case on a phone).
|
/// panel is collapsed (the common case on a phone).
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
fn overlay_buttons(&mut self, ui: &mut egui::Ui) {
|
fn overlay_buttons(&mut self, ui: &mut egui::Ui) {
|
||||||
egui::Area::new(egui::Id::new("overlay_buttons"))
|
egui::Area::new(egui::Id::new("overlay_buttons"))
|
||||||
.anchor(egui::Align2::LEFT_TOP, egui::vec2(8.0, 8.0))
|
.anchor(egui::Align2::LEFT_TOP, egui::vec2(8.0, 8.0))
|
||||||
@@ -1719,6 +1723,7 @@ impl FractalApp {
|
|||||||
/// "Fractal Info" window. Kept separate from `overlay_buttons` (top-left)
|
/// "Fractal Info" window. Kept separate from `overlay_buttons` (top-left)
|
||||||
/// so it stays out of the way of the panel toggle / fullscreen controls,
|
/// so it stays out of the way of the panel toggle / fullscreen controls,
|
||||||
/// but is still reachable even when the controls panel is collapsed.
|
/// but is still reachable even when the controls panel is collapsed.
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
fn info_button(&mut self, ui: &mut egui::Ui) {
|
fn info_button(&mut self, ui: &mut egui::Ui) {
|
||||||
egui::Area::new(egui::Id::new("info_button"))
|
egui::Area::new(egui::Id::new("info_button"))
|
||||||
.anchor(egui::Align2::LEFT_BOTTOM, egui::vec2(8.0, -8.0))
|
.anchor(egui::Align2::LEFT_BOTTOM, egui::vec2(8.0, -8.0))
|
||||||
@@ -1740,6 +1745,7 @@ impl FractalApp {
|
|||||||
/// Window with details about what's currently on screen: formula, active
|
/// Window with details about what's currently on screen: formula, active
|
||||||
/// per-kind constants, zoom depth, iteration count. Reads live state, so
|
/// per-kind constants, zoom depth, iteration count. Reads live state, so
|
||||||
/// it stays correct as the user pans/zooms/switches kinds.
|
/// it stays correct as the user pans/zooms/switches kinds.
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
fn info_window(&mut self, ctx: &egui::Context) {
|
fn info_window(&mut self, ctx: &egui::Context) {
|
||||||
let mut open = self.info_open;
|
let mut open = self.info_open;
|
||||||
egui::Window::new("Fractal Info")
|
egui::Window::new("Fractal Info")
|
||||||
@@ -1796,6 +1802,7 @@ impl FractalApp {
|
|||||||
|
|
||||||
/// Help window: what the app does, plus a reference for mouse/touch and
|
/// Help window: what the app does, plus a reference for mouse/touch and
|
||||||
/// keyboard controls.
|
/// keyboard controls.
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
fn help_window(&mut self, ctx: &egui::Context) {
|
fn help_window(&mut self, ctx: &egui::Context) {
|
||||||
let mut open = self.help_open;
|
let mut open = self.help_open;
|
||||||
egui::Window::new("Help")
|
egui::Window::new("Help")
|
||||||
@@ -1886,7 +1893,7 @@ impl FractalApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Push the desired fullscreen state to the platform.
|
/// Push the desired fullscreen state to the platform.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(all(feature = "gui", not(target_arch = "wasm32")))]
|
||||||
fn apply_fullscreen(&mut self, ctx: &egui::Context) {
|
fn apply_fullscreen(&mut self, ctx: &egui::Context) {
|
||||||
ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(self.fullscreen));
|
ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(self.fullscreen));
|
||||||
}
|
}
|
||||||
@@ -1894,7 +1901,7 @@ impl FractalApp {
|
|||||||
/// Push the desired fullscreen state to the browser. `request_fullscreen`
|
/// Push the desired fullscreen state to the browser. `request_fullscreen`
|
||||||
/// must run inside a user gesture; the button click provides the transient
|
/// must run inside a user gesture; the button click provides the transient
|
||||||
/// activation that carries into this frame.
|
/// activation that carries into this frame.
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(all(feature = "gui", target_arch = "wasm32"))]
|
||||||
fn apply_fullscreen(&mut self, _ctx: &egui::Context) {
|
fn apply_fullscreen(&mut self, _ctx: &egui::Context) {
|
||||||
let Some(doc) = web_sys::window().and_then(|w| w.document()) else {
|
let Some(doc) = web_sys::window().and_then(|w| w.document()) else {
|
||||||
return;
|
return;
|
||||||
@@ -1910,14 +1917,14 @@ impl FractalApp {
|
|||||||
|
|
||||||
/// Refresh `self.fullscreen` from the real platform state, so the label is
|
/// Refresh `self.fullscreen` from the real platform state, so the label is
|
||||||
/// correct even when fullscreen is left by Esc/F11 or the browser UI.
|
/// correct even when fullscreen is left by Esc/F11 or the browser UI.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(all(feature = "gui", not(target_arch = "wasm32")))]
|
||||||
fn sync_fullscreen(&mut self, ctx: &egui::Context) {
|
fn sync_fullscreen(&mut self, ctx: &egui::Context) {
|
||||||
if let Some(fs) = ctx.input(|i| i.viewport().fullscreen) {
|
if let Some(fs) = ctx.input(|i| i.viewport().fullscreen) {
|
||||||
self.fullscreen = fs;
|
self.fullscreen = fs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(all(feature = "gui", target_arch = "wasm32"))]
|
||||||
fn sync_fullscreen(&mut self, _ctx: &egui::Context) {
|
fn sync_fullscreen(&mut self, _ctx: &egui::Context) {
|
||||||
if let Some(doc) = web_sys::window().and_then(|w| w.document()) {
|
if let Some(doc) = web_sys::window().and_then(|w| w.document()) {
|
||||||
self.fullscreen = doc.fullscreen_element().is_some();
|
self.fullscreen = doc.fullscreen_element().is_some();
|
||||||
@@ -1929,6 +1936,7 @@ impl FractalApp {
|
|||||||
/// divides once the window closes, so the readout is steady rather than
|
/// divides once the window closes, so the readout is steady rather than
|
||||||
/// jittering every frame. Only advances when egui repaints — i.e. while the
|
/// jittering every frame. Only advances when egui repaints — i.e. while the
|
||||||
/// app is doing work — so an idle app shows its last measured rate.
|
/// app is doing work — so an idle app shows its last measured rate.
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
fn update_fps(&mut self, ui: &egui::Ui) {
|
fn update_fps(&mut self, ui: &egui::Ui) {
|
||||||
let now = ui.input(|i| i.time);
|
let now = ui.input(|i| i.time);
|
||||||
// Reset the window if time went backwards or hasn't started yet.
|
// Reset the window if time went backwards or hasn't started yet.
|
||||||
@@ -1948,6 +1956,7 @@ impl FractalApp {
|
|||||||
/// Advance any enabled animations by the frame's elapsed time, and request a
|
/// Advance any enabled animations by the frame's elapsed time, and request a
|
||||||
/// repaint while active. Animations render at full resolution/AA (they do not
|
/// repaint while active. Animations render at full resolution/AA (they do not
|
||||||
/// trigger the interaction low-res pass).
|
/// trigger the interaction low-res pass).
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
fn tick_animations(&mut self, ui: &egui::Ui) {
|
fn tick_animations(&mut self, ui: &egui::Ui) {
|
||||||
// Julia c only matters in Julia mode; Phoenix p only for the Phoenix kind; Lambda λ only for Lambda kind.
|
// Julia c only matters in Julia mode; Phoenix p only for the Phoenix kind; Lambda λ only for Lambda kind.
|
||||||
let julia_on = self.anim.julia.on && self.mode == FractalMode::Julia;
|
let julia_on = self.anim.julia.on && self.mode == FractalMode::Julia;
|
||||||
@@ -2092,6 +2101,7 @@ impl FractalApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
fn controls_ui(&mut self, ui: &mut egui::Ui) {
|
fn controls_ui(&mut self, ui: &mut egui::Ui) {
|
||||||
ui.heading("Fractal Explorer");
|
ui.heading("Fractal Explorer");
|
||||||
ui.separator();
|
ui.separator();
|
||||||
@@ -2574,6 +2584,7 @@ impl FractalApp {
|
|||||||
|
|
||||||
/// Controls for Buddhabrot mode: nested iteration caps (Nebulabrot R/G/B
|
/// Controls for Buddhabrot mode: nested iteration caps (Nebulabrot R/G/B
|
||||||
/// coloring), exposure, and the progressive-accumulation toggle.
|
/// coloring), exposure, and the progressive-accumulation toggle.
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
fn buddhabrot_ui(&mut self, ui: &mut egui::Ui) {
|
fn buddhabrot_ui(&mut self, ui: &mut egui::Ui) {
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.add(
|
ui.add(
|
||||||
@@ -2614,6 +2625,7 @@ impl FractalApp {
|
|||||||
ui.small("PNG export isn't available in Buddhabrot mode yet.");
|
ui.small("PNG export isn't available in Buddhabrot mode yet.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
fn fractal_ui(&mut self, ui: &mut egui::Ui) {
|
fn fractal_ui(&mut self, ui: &mut egui::Ui) {
|
||||||
let size = ui.available_size();
|
let size = ui.available_size();
|
||||||
let (rect, response) = ui.allocate_exact_size(size, egui::Sense::click_and_drag());
|
let (rect, response) = ui.allocate_exact_size(size, egui::Sense::click_and_drag());
|
||||||
@@ -2939,6 +2951,7 @@ impl FractalApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
impl eframe::App for FractalApp {
|
impl eframe::App for FractalApp {
|
||||||
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
|
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
|
||||||
self.poll_export(ui.ctx());
|
self.poll_export(ui.ctx());
|
||||||
|
|||||||
@@ -0,0 +1,225 @@
|
|||||||
|
//! Arbitrary-precision binary float [`Big`] (one coordinate of a deep-zoom
|
||||||
|
//! center, or of a reference orbit point), over one of two backends:
|
||||||
|
//!
|
||||||
|
//! - native (default): `rug` (GMP/MPFR), the fastest, but C code that can't
|
||||||
|
//! target `wasm32-unknown-unknown`;
|
||||||
|
//! - `wasm` feature: a pure-Rust library, required for the web build.
|
||||||
|
//!
|
||||||
|
//! Both expose the same inherent API plus the operators below, and follow
|
||||||
|
//! the same precision rule: an operation's result has the larger of its
|
||||||
|
//! operands' precisions (in bits), rounded to nearest. Shifts (`<<`/`>>` by
|
||||||
|
//! an `isize`) are exact multiplications by powers of two.
|
||||||
|
|
||||||
|
#[cfg(not(feature = "wasm"))]
|
||||||
|
mod rug_backend;
|
||||||
|
#[cfg(not(feature = "wasm"))]
|
||||||
|
pub use rug_backend::Big;
|
||||||
|
|
||||||
|
#[cfg(feature = "wasm")]
|
||||||
|
mod web_backend;
|
||||||
|
#[cfg(feature = "wasm")]
|
||||||
|
pub use web_backend::Big;
|
||||||
|
// Native `--features wasm` builds (to test the web backend) still link rug.
|
||||||
|
#[cfg(all(feature = "wasm", not(target_arch = "wasm32")))]
|
||||||
|
use rug as _;
|
||||||
|
|
||||||
|
#[cfg(all(target_arch = "wasm32", not(feature = "wasm")))]
|
||||||
|
compile_error!("the web build needs `--features wasm` (rug can't target wasm32)");
|
||||||
|
|
||||||
|
use core::ops::{Add, Mul, Neg, Shl, Shr, Sub};
|
||||||
|
|
||||||
|
/// Implements `$Trait` for every owned/borrowed combination of `Big`
|
||||||
|
/// operands, through the backend's by-reference `$imp`.
|
||||||
|
macro_rules! forward_binop {
|
||||||
|
($Trait:ident, $method:ident, $imp:ident) => {
|
||||||
|
impl $Trait<&Big> for &Big {
|
||||||
|
type Output = Big;
|
||||||
|
fn $method(self, rhs: &Big) -> Big {
|
||||||
|
self.$imp(rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl $Trait<Big> for &Big {
|
||||||
|
type Output = Big;
|
||||||
|
fn $method(self, rhs: Big) -> Big {
|
||||||
|
self.$imp(&rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl $Trait<&Big> for Big {
|
||||||
|
type Output = Big;
|
||||||
|
fn $method(self, rhs: &Big) -> Big {
|
||||||
|
(&self).$imp(rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl $Trait<Big> for Big {
|
||||||
|
type Output = Big;
|
||||||
|
fn $method(self, rhs: Big) -> Big {
|
||||||
|
(&self).$imp(&rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
forward_binop!(Add, add, add_ref);
|
||||||
|
forward_binop!(Sub, sub, sub_ref);
|
||||||
|
forward_binop!(Mul, mul, mul_ref);
|
||||||
|
|
||||||
|
impl Neg for Big {
|
||||||
|
type Output = Big;
|
||||||
|
fn neg(self) -> Big {
|
||||||
|
self.negated()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Neg for &Big {
|
||||||
|
type Output = Big;
|
||||||
|
fn neg(self) -> Big {
|
||||||
|
self.clone().negated()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Shl<isize> for Big {
|
||||||
|
type Output = Big;
|
||||||
|
fn shl(self, k: isize) -> Big {
|
||||||
|
self.mul_pow2(k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Shl<isize> for &Big {
|
||||||
|
type Output = Big;
|
||||||
|
fn shl(self, k: isize) -> Big {
|
||||||
|
self.clone().mul_pow2(k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Shr<isize> for Big {
|
||||||
|
type Output = Big;
|
||||||
|
fn shr(self, k: isize) -> Big {
|
||||||
|
self.mul_pow2(-k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Shr<isize> for &Big {
|
||||||
|
type Output = Big;
|
||||||
|
fn shr(self, k: isize) -> Big {
|
||||||
|
self.clone().mul_pow2(-k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Significant decimal digits of a value, as a backend's
|
||||||
|
/// `to_decimal_parts` returns them: the value is `±0.digits × 10^exp10`,
|
||||||
|
/// `digits` has no trailing zeros, and it is empty for zero.
|
||||||
|
pub struct DecimalParts {
|
||||||
|
pub negative: bool,
|
||||||
|
pub digits: String,
|
||||||
|
pub exp10: isize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DecimalParts {
|
||||||
|
/// Build from a significand digit string (maybe with trailing zeros)
|
||||||
|
/// whose value is `0.digits × 10^exp10`.
|
||||||
|
fn new(negative: bool, digits: &str, exp10: isize) -> Self {
|
||||||
|
let digits = digits.trim_end_matches('0').to_string();
|
||||||
|
Self {
|
||||||
|
negative: negative && !digits.is_empty(),
|
||||||
|
digits,
|
||||||
|
exp10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Big {
|
||||||
|
/// Decimal string with `sig_digits` significant digits, in plain
|
||||||
|
/// positional notation (`-0.000123`, `4500`), trailing zeros trimmed.
|
||||||
|
pub fn to_decimal_string(&self, sig_digits: usize) -> String {
|
||||||
|
let DecimalParts {
|
||||||
|
negative,
|
||||||
|
digits,
|
||||||
|
exp10,
|
||||||
|
} = self.to_decimal_parts(sig_digits.max(1));
|
||||||
|
if digits.is_empty() {
|
||||||
|
return "0".to_string();
|
||||||
|
}
|
||||||
|
let sign = if negative { "-" } else { "" };
|
||||||
|
let len = digits.len() as isize;
|
||||||
|
if exp10 <= 0 {
|
||||||
|
let zeros = "0".repeat((-exp10) as usize);
|
||||||
|
format!("{sign}0.{zeros}{digits}")
|
||||||
|
} else if exp10 >= len {
|
||||||
|
let zeros = "0".repeat((exp10 - len) as usize);
|
||||||
|
format!("{sign}{digits}{zeros}")
|
||||||
|
} else {
|
||||||
|
let (int, frac) = digits.split_at(exp10 as usize);
|
||||||
|
format!("{sign}{int}.{frac}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn big(x: f64) -> Big {
|
||||||
|
Big::from_f64(x, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decimal_string_is_positional() {
|
||||||
|
assert_eq!(
|
||||||
|
big(-0.7515).to_decimal_string(20),
|
||||||
|
"-0.75149999999999994582"
|
||||||
|
);
|
||||||
|
assert_eq!(big(-0.7515).to_decimal_string(5), "-0.7515");
|
||||||
|
assert_eq!(big(0.0).to_decimal_string(5), "0");
|
||||||
|
assert_eq!(big(1.0).to_decimal_string(5), "1");
|
||||||
|
assert_eq!(big(123.456).to_decimal_string(5), "123.46");
|
||||||
|
assert_eq!(big(1e20).to_decimal_string(5), "100000000000000000000");
|
||||||
|
assert_eq!(big(0.1).to_decimal_string(5), "0.1");
|
||||||
|
let tiny = big(1.0) >> 200;
|
||||||
|
let s = tiny.to_decimal_string(5);
|
||||||
|
assert_eq!(s, format!("0.{}6223", "0".repeat(60)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decimal_round_trip() {
|
||||||
|
let s = "-0.743643887037158704752191506114774";
|
||||||
|
let x = Big::from_decimal_str(s, 200).unwrap();
|
||||||
|
assert_eq!(x.to_decimal_string(33), s);
|
||||||
|
assert_eq!(
|
||||||
|
Big::from_decimal_str("1.5e-20", 64).unwrap().to_f64(),
|
||||||
|
1.5e-20
|
||||||
|
);
|
||||||
|
assert!(Big::from_decimal_str("abc", 64).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn precision_is_max_of_operands() {
|
||||||
|
let a = Big::from_f64(1.0, 100);
|
||||||
|
let b = Big::from_f64(3.0, 200);
|
||||||
|
assert_eq!((&a + &b).precision(), 200);
|
||||||
|
assert_eq!((&a * &b).precision(), 200);
|
||||||
|
assert_eq!((&b - &a).precision(), 200);
|
||||||
|
assert_eq!(a.clone().with_precision(300).precision(), 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exact_shifts_and_log2() {
|
||||||
|
let x = Big::from_f64(0.75, 64) >> 5000;
|
||||||
|
assert_eq!(x.log2_floor(), Some(-5001));
|
||||||
|
assert_eq!((x << 5000).to_f64(), 0.75);
|
||||||
|
assert_eq!(Big::zero(64).log2_floor(), None);
|
||||||
|
assert!(Big::from_f64(-1.0, 64).is_negative());
|
||||||
|
assert!(!Big::zero(64).is_negative());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transcendentals() {
|
||||||
|
let x = Big::from_f64(0.5, 128);
|
||||||
|
assert!((x.ln().to_f64() - 0.5f64.ln()).abs() < 1e-15);
|
||||||
|
assert!((x.exp().to_f64() - 0.5f64.exp()).abs() < 1e-15);
|
||||||
|
let (s, c) = x.sin_cos();
|
||||||
|
assert!((s.to_f64() - 0.5f64.sin()).abs() < 1e-15);
|
||||||
|
assert!((c.to_f64() - 0.5f64.cos()).abs() < 1e-15);
|
||||||
|
let y = Big::from_f64(-0.3, 128);
|
||||||
|
assert!((y.atan2(&x).to_f64() - (-0.3f64).atan2(0.5)).abs() < 1e-15);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
//! [`Big`] over `rug::Float` (MPFR), for native builds.
|
||||||
|
|
||||||
|
use core::cmp::Ordering;
|
||||||
|
|
||||||
|
use rug::{Assign, Float};
|
||||||
|
|
||||||
|
use super::DecimalParts;
|
||||||
|
|
||||||
|
/// Arbitrary-precision binary float. See the module docs.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct Big(Float);
|
||||||
|
|
||||||
|
/// MPFR precision for `bits`, clamped to what it accepts.
|
||||||
|
fn prec(bits: usize) -> u32 {
|
||||||
|
(bits as u32).clamp(rug::float::prec_min(), rug::float::prec_max())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Big {
|
||||||
|
/// `x` at `bits` of precision (exact when `bits >= 53`). Non-finite `x`
|
||||||
|
/// reads as 0.
|
||||||
|
pub fn from_f64(x: f64, bits: usize) -> Self {
|
||||||
|
let x = if x.is_finite() { x } else { 0.0 };
|
||||||
|
Self(Float::with_val(prec(bits), x))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn zero(bits: usize) -> Self {
|
||||||
|
Self(Float::new(prec(bits)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a decimal (`-0.75`, `1.5e-20`, any number of digits) at `bits`
|
||||||
|
/// (at least 53) of precision.
|
||||||
|
pub fn from_decimal_str(s: &str, bits: usize) -> Option<Self> {
|
||||||
|
let parsed = Float::parse(s.trim()).ok()?;
|
||||||
|
let x = Float::with_val(prec(bits.max(53)), parsed);
|
||||||
|
x.is_finite().then_some(Self(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn precision(&self) -> usize {
|
||||||
|
self.0.prec() as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change the precision, rounding to nearest if it shrinks.
|
||||||
|
pub fn with_precision(mut self, bits: usize) -> Self {
|
||||||
|
self.0.set_prec(prec(bits));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_f64(&self) -> f64 {
|
||||||
|
self.0.to_f64()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_zero(&self) -> bool {
|
||||||
|
self.0.is_zero()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_negative(&self) -> bool {
|
||||||
|
self.0.cmp0() == Some(Ordering::Less)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn abs(self) -> Self {
|
||||||
|
Self(self.0.abs())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sqr(&self) -> Self {
|
||||||
|
Self(Float::with_val(self.0.prec(), self.0.square_ref()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `floor(log2|x|)`, or `None` for zero. Exact, at any exponent.
|
||||||
|
pub fn log2_floor(&self) -> Option<isize> {
|
||||||
|
// MPFR normalizes the significand to [0.5, 1).
|
||||||
|
self.0.get_exp().map(|e| e as isize - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ln(&self) -> Self {
|
||||||
|
Self(Float::with_val(self.0.prec(), self.0.ln_ref()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exp(&self) -> Self {
|
||||||
|
Self(Float::with_val(self.0.prec(), self.0.exp_ref()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `atan2(self, x)`: the angle of `(x, self)`.
|
||||||
|
pub fn atan2(&self, x: &Big) -> Self {
|
||||||
|
let p = self.0.prec().max(x.0.prec());
|
||||||
|
Self(Float::with_val(p, self.0.atan2_ref(&x.0)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sin_cos(&self) -> (Self, Self) {
|
||||||
|
let p = self.0.prec();
|
||||||
|
let (mut s, mut c) = (Float::new(p), Float::new(p));
|
||||||
|
(&mut s, &mut c).assign(self.0.sin_cos_ref());
|
||||||
|
(Self(s), Self(c))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `sig` significant decimal digits (rounded to nearest).
|
||||||
|
pub fn to_decimal_parts(&self, sig: usize) -> DecimalParts {
|
||||||
|
if self.0.is_zero() {
|
||||||
|
return DecimalParts::new(false, "", 0);
|
||||||
|
}
|
||||||
|
let (negative, digits, exp) = self.0.to_sign_string_exp(10, Some(sig));
|
||||||
|
DecimalParts::new(negative, &digits, exp.unwrap_or(0) as isize)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn add_ref(&self, rhs: &Big) -> Big {
|
||||||
|
let p = self.0.prec().max(rhs.0.prec());
|
||||||
|
Big(Float::with_val(p, &self.0 + &rhs.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn sub_ref(&self, rhs: &Big) -> Big {
|
||||||
|
let p = self.0.prec().max(rhs.0.prec());
|
||||||
|
Big(Float::with_val(p, &self.0 - &rhs.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn mul_ref(&self, rhs: &Big) -> Big {
|
||||||
|
let p = self.0.prec().max(rhs.0.prec());
|
||||||
|
Big(Float::with_val(p, &self.0 * &rhs.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn negated(self) -> Big {
|
||||||
|
Big(-self.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `self · 2^k`, exact. `|k|` stays far below `i32::MAX` (precision and
|
||||||
|
/// zoom depth are capped around 2^20 bits).
|
||||||
|
pub(super) fn mul_pow2(self, k: isize) -> Big {
|
||||||
|
Big(self.0 << k as i32)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
//! [`Big`] over `malachite-float`, pure Rust, for the web build.
|
||||||
|
|
||||||
|
use malachite_base::num::basic::traits::Zero;
|
||||||
|
use malachite_base::num::conversion::string::options::ToSciOptions;
|
||||||
|
use malachite_base::num::conversion::traits::{RoundingFrom, ToSci};
|
||||||
|
use malachite_base::rounding_modes::RoundingMode::Nearest;
|
||||||
|
use malachite_float::Float;
|
||||||
|
|
||||||
|
use super::DecimalParts;
|
||||||
|
|
||||||
|
/// Arbitrary-precision binary float. See the module docs.
|
||||||
|
///
|
||||||
|
/// The precision is stored next to the value: malachite's zero has none,
|
||||||
|
/// but "zero at `p` bits" must still lift a sum with it to `p` bits.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Big {
|
||||||
|
f: Float,
|
||||||
|
prec: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Big {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.f == other.f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prec(bits: usize) -> u64 {
|
||||||
|
(bits as u64).max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Big {
|
||||||
|
fn new(f: Float, prec: u64) -> Self {
|
||||||
|
// Only finite values reach here; anything else (overflow) is a bug.
|
||||||
|
debug_assert!(f.is_finite(), "non-finite Big: {f}");
|
||||||
|
Self { f, prec }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `x` at `bits` of precision (exact when `bits >= 53`). Non-finite `x`
|
||||||
|
/// reads as 0.
|
||||||
|
pub fn from_f64(x: f64, bits: usize) -> Self {
|
||||||
|
let p = prec(bits);
|
||||||
|
if !x.is_finite() {
|
||||||
|
return Self::zero(bits);
|
||||||
|
}
|
||||||
|
Self::new(Float::from_primitive_float_prec(x, p).0, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn zero(bits: usize) -> Self {
|
||||||
|
Self::new(Float::ZERO, prec(bits))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a decimal (`-0.75`, `1.5e-20`, any number of digits) at `bits`
|
||||||
|
/// (at least 53) of precision.
|
||||||
|
pub fn from_decimal_str(s: &str, bits: usize) -> Option<Self> {
|
||||||
|
let p = prec(bits.max(53));
|
||||||
|
let (f, _) = Float::from_sci_string_prec(s.trim(), p)?;
|
||||||
|
f.is_finite().then(|| Self::new(f, p))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn precision(&self) -> usize {
|
||||||
|
self.prec as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change the precision, rounding to nearest if it shrinks.
|
||||||
|
pub fn with_precision(mut self, bits: usize) -> Self {
|
||||||
|
let p = prec(bits);
|
||||||
|
if self.f.get_prec().is_some() {
|
||||||
|
self.f.set_prec(p);
|
||||||
|
}
|
||||||
|
self.prec = p;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_f64(&self) -> f64 {
|
||||||
|
f64::rounding_from(&self.f, Nearest).0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_zero(&self) -> bool {
|
||||||
|
self.f.is_zero()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_negative(&self) -> bool {
|
||||||
|
self.f.is_sign_negative() && !self.f.is_zero()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn abs(self) -> Self {
|
||||||
|
if self.f.is_sign_negative() {
|
||||||
|
self.negated()
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sqr(&self) -> Self {
|
||||||
|
Self::new(self.f.square_prec_ref(self.prec).0, self.prec)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `floor(log2|x|)`, or `None` for zero. Exact, at any exponent.
|
||||||
|
pub fn log2_floor(&self) -> Option<isize> {
|
||||||
|
// The significand is normalized to [0.5, 1).
|
||||||
|
self.f.get_exponent().map(|e| e as isize - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ln(&self) -> Self {
|
||||||
|
Self::new(self.f.ln_prec_ref(self.prec).0, self.prec)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exp(&self) -> Self {
|
||||||
|
Self::new(self.f.exp_prec_ref(self.prec).0, self.prec)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `atan2(self, x)`: the angle of `(x, self)`.
|
||||||
|
pub fn atan2(&self, x: &Big) -> Self {
|
||||||
|
let p = self.prec.max(x.prec);
|
||||||
|
Self::new(self.f.atan2_prec_ref_ref(&x.f, p).0, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sin_cos(&self) -> (Self, Self) {
|
||||||
|
let (s, c, _, _) = self.f.sin_cos_prec_ref(self.prec);
|
||||||
|
(Self::new(s, self.prec), Self::new(c, self.prec))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `sig` significant decimal digits (rounded to nearest).
|
||||||
|
pub fn to_decimal_parts(&self, sig: usize) -> DecimalParts {
|
||||||
|
if self.f.is_zero() {
|
||||||
|
return DecimalParts::new(false, "", 0);
|
||||||
|
}
|
||||||
|
let mut options = ToSciOptions::default();
|
||||||
|
options.set_precision(sig as u64);
|
||||||
|
let s = self.f.to_sci_with_options(options).to_string();
|
||||||
|
// `[-]int[.frac][e±N]`, positional or scientific depending on size.
|
||||||
|
let (negative, s) = match s.strip_prefix('-') {
|
||||||
|
Some(rest) => (true, rest),
|
||||||
|
None => (false, s.as_str()),
|
||||||
|
};
|
||||||
|
let (mantissa, exp) = match s.split_once(['e', 'E']) {
|
||||||
|
Some((m, e)) => (m, e.trim_start_matches('+').parse::<isize>().unwrap_or(0)),
|
||||||
|
None => (s, 0),
|
||||||
|
};
|
||||||
|
let (int, frac) = mantissa.split_once('.').unwrap_or((mantissa, ""));
|
||||||
|
let all = format!("{int}{frac}");
|
||||||
|
let digits = all.trim_start_matches('0');
|
||||||
|
let leading_zeros = (all.len() - digits.len()) as isize;
|
||||||
|
// 0.digits · 10^exp10 = int.frac · 10^exp.
|
||||||
|
let exp10 = int.len() as isize - leading_zeros + exp;
|
||||||
|
DecimalParts::new(negative, digits, exp10)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn add_ref(&self, rhs: &Big) -> Big {
|
||||||
|
let p = self.prec.max(rhs.prec);
|
||||||
|
Big::new(self.f.add_prec_ref_ref(&rhs.f, p).0, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn sub_ref(&self, rhs: &Big) -> Big {
|
||||||
|
let p = self.prec.max(rhs.prec);
|
||||||
|
Big::new(self.f.sub_prec_ref_ref(&rhs.f, p).0, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn mul_ref(&self, rhs: &Big) -> Big {
|
||||||
|
let p = self.prec.max(rhs.prec);
|
||||||
|
Big::new(self.f.mul_prec_ref_ref(&rhs.f, p).0, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn negated(self) -> Big {
|
||||||
|
Big::new(-self.f, self.prec)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `self · 2^k`, exact (malachite's exponent range is ±2^30, far past
|
||||||
|
/// what zoom depth needs).
|
||||||
|
pub(super) fn mul_pow2(self, k: isize) -> Big {
|
||||||
|
Big::new(self.f << k as i64, self.prec)
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
-1
@@ -74,6 +74,10 @@ pub struct Cli {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub de: bool,
|
pub de: bool,
|
||||||
|
|
||||||
|
/// Enable 2×2 antialiasing (supersampling; ~4× slower).
|
||||||
|
#[arg(long)]
|
||||||
|
pub antialias: bool,
|
||||||
|
|
||||||
/// Coloring palette index.
|
/// Coloring palette index.
|
||||||
#[arg(long, value_name = "INDEX")]
|
#[arg(long, value_name = "INDEX")]
|
||||||
pub palette: Option<u32>,
|
pub palette: Option<u32>,
|
||||||
@@ -81,7 +85,9 @@ pub struct Cli {
|
|||||||
/// Output path for --headless (default: fractal-<timestamp>.png). When
|
/// Output path for --headless (default: fractal-<timestamp>.png). When
|
||||||
/// animating (--to-view/--to-share), this is a directory of
|
/// animating (--to-view/--to-share), this is a directory of
|
||||||
/// frame-00001.png, frame-00002.png, ... instead (default:
|
/// frame-00001.png, frame-00002.png, ... instead (default:
|
||||||
/// frames-<timestamp>/).
|
/// frames-<timestamp>/). "-" writes to stdout: the PNG for a single
|
||||||
|
/// image, or raw RGBA8 frames in order for an animation, to pipe into
|
||||||
|
/// `ffmpeg -f rawvideo -pix_fmt rgba -s WxH -r FPS -i - ...`.
|
||||||
#[arg(long, value_name = "PATH")]
|
#[arg(long, value_name = "PATH")]
|
||||||
pub export_path: Option<String>,
|
pub export_path: Option<String>,
|
||||||
|
|
||||||
@@ -165,6 +171,17 @@ pub struct Cli {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub linear: bool,
|
pub linear: bool,
|
||||||
|
|
||||||
|
/// Split the animation into N equal parts (use with --shard) to render
|
||||||
|
/// it across several runs. Each part keeps the whole animation's timing
|
||||||
|
/// and easing, so the clips join seamlessly.
|
||||||
|
#[arg(long, value_name = "N")]
|
||||||
|
pub shards: Option<u32>,
|
||||||
|
|
||||||
|
/// Which part of --shards to render, 1-based. PNG frames keep their
|
||||||
|
/// global numbering, so all shards can share one --export-path directory.
|
||||||
|
#[arg(long, value_name = "K")]
|
||||||
|
pub shard: Option<u32>,
|
||||||
|
|
||||||
/// Run without opening a window: render the current view to a PNG and
|
/// Run without opening a window: render the current view to a PNG and
|
||||||
/// exit. Combine with --kind/--julia/--share/--view etc. to pick what to
|
/// exit. Combine with --kind/--julia/--share/--view etc. to pick what to
|
||||||
/// render, or any --to-* flag (--to-view, --to-julia, --to-kind, ...) to
|
/// render, or any --to-* flag (--to-view, --to-julia, --to-kind, ...) to
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use eframe::egui_wgpu::{self, wgpu};
|
#[cfg(feature = "gui")]
|
||||||
|
use eframe::egui_wgpu;
|
||||||
|
|
||||||
/// Random samples dispatched per accumulating frame. Chosen so a frame stays
|
/// Random samples dispatched per accumulating frame. Chosen so a frame stays
|
||||||
/// interactive on a modest GPU even when most samples run the full `b_cap`
|
/// interactive on a modest GPU even when most samples run the full `b_cap`
|
||||||
@@ -125,7 +126,9 @@ pub struct BuddhabrotRenderer {
|
|||||||
|
|
||||||
impl BuddhabrotRenderer {
|
impl BuddhabrotRenderer {
|
||||||
pub fn new(device: &wgpu::Device, target_format: wgpu::TextureFormat) -> Self {
|
pub fn new(device: &wgpu::Device, target_format: wgpu::TextureFormat) -> Self {
|
||||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
let shader = unsafe {
|
||||||
|
device.create_shader_module_trusted(
|
||||||
|
wgpu::ShaderModuleDescriptor {
|
||||||
label: Some("buddhabrot"),
|
label: Some("buddhabrot"),
|
||||||
source: wgpu::ShaderSource::Wgsl(
|
source: wgpu::ShaderSource::Wgsl(
|
||||||
concat!(
|
concat!(
|
||||||
@@ -134,7 +137,10 @@ impl BuddhabrotRenderer {
|
|||||||
)
|
)
|
||||||
.into(),
|
.into(),
|
||||||
),
|
),
|
||||||
});
|
},
|
||||||
|
wgpu::ShaderRuntimeChecks::unchecked(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
label: Some("buddhabrot uniforms"),
|
label: Some("buddhabrot uniforms"),
|
||||||
@@ -336,6 +342,7 @@ pub struct BuddhabrotCallback {
|
|||||||
pub size_px: [u32; 2],
|
pub size_px: [u32; 2],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
impl egui_wgpu::CallbackTrait for BuddhabrotCallback {
|
impl egui_wgpu::CallbackTrait for BuddhabrotCallback {
|
||||||
fn prepare(
|
fn prepare(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
+1
-1
@@ -16,5 +16,5 @@ pub use renderer::PipelineKey;
|
|||||||
pub use renderer::encode_png_with_progress;
|
pub use renderer::encode_png_with_progress;
|
||||||
pub use renderer::{ExportRender, FractalCallback, FractalRenderer, MAX_REF_POINTS, Uniforms};
|
pub use renderer::{ExportRender, FractalCallback, FractalRenderer, MAX_REF_POINTS, Uniforms};
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub use renderer::{encode_png, export_to_png_blocking, render_readback_blocking};
|
pub use renderer::{encode_png, export_to_png_blocking, render_readback_blocking, unpad_rgba};
|
||||||
pub use share::ShareState;
|
pub use share::ShareState;
|
||||||
|
|||||||
+65
-95
@@ -1,7 +1,7 @@
|
|||||||
//! High-precision reference-orbit computation for perturbation rendering.
|
//! High-precision reference-orbit computation for perturbation rendering.
|
||||||
//!
|
//!
|
||||||
//! We iterate the fractal's formula `Z_{n+1} = f(Z_n, C)` at high precision
|
//! We iterate the fractal's formula `Z_{n+1} = f(Z_n, C)` at high precision
|
||||||
//! (`dashu-float`), storing each `Z_n` as an `f32` pair. Every pixel is then
|
//! ([`Big`]: `rug` natively, pure Rust on the web), storing each `Z_n` as an `f32` pair. Every pixel is then
|
||||||
//! rendered on the GPU as a small `f32` delta from this orbit — that is what
|
//! rendered on the GPU as a small `f32` delta from this orbit — that is what
|
||||||
//! makes deep zoom cheap. See `shaders/mandelbrot.wgsl` for the delta side; the
|
//! makes deep zoom cheap. See `shaders/mandelbrot.wgsl` for the delta side; the
|
||||||
//! delta formula there must match the orbit formula here.
|
//! delta formula there must match the orbit formula here.
|
||||||
@@ -24,7 +24,7 @@ use crate::view::{Big, big_from_f64};
|
|||||||
const REFERENCE_ESCAPE_SQ: f64 = 1.0e10;
|
const REFERENCE_ESCAPE_SQ: f64 = 1.0e10;
|
||||||
|
|
||||||
/// Up to this working precision (bits) the orbit is iterated in plain `f64`
|
/// Up to this working precision (bits) the orbit is iterated in plain `f64`
|
||||||
/// instead of `FBig` — orders of magnitude faster, which matters most on the
|
/// instead of `Big` — orders of magnitude faster, which matters most on the
|
||||||
/// web (where the reference is computed inline on the UI thread).
|
/// web (where the reference is computed inline on the UI thread).
|
||||||
///
|
///
|
||||||
/// `precision_for` asks for `zoom_bits + 48` guard bits, but the GPU only
|
/// `precision_for` asks for `zoom_bits + 48` guard bits, but the GPU only
|
||||||
@@ -96,29 +96,21 @@ impl<'a> IntoIterator for &'a RefOrbit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `floor(log2|x|)`, or `None` for zero. Exact (from the binary
|
|
||||||
/// representation), and works far below f64's range.
|
|
||||||
fn big_log2_floor(x: &Big) -> Option<isize> {
|
|
||||||
let repr = x.repr();
|
|
||||||
let digits = repr.digits();
|
|
||||||
(digits > 0).then(|| repr.exponent() + digits as isize - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Store `(zr, zi)` into `orbit`, as plain f32 unless its magnitude is below
|
/// Store `(zr, zi)` into `orbit`, as plain f32 unless its magnitude is below
|
||||||
/// `2^TINY_LOG2`, in which case both components share an exponent `k` and
|
/// `2^TINY_LOG2`, in which case both components share an exponent `k` and
|
||||||
/// the stored mantissa `Z * 2^-k` has its larger component in `[0.5, 1)`.
|
/// the stored mantissa `Z * 2^-k` has its larger component in `[0.5, 1)`.
|
||||||
fn push_big_point(orbit: &mut RefOrbit, zr: &Big, zi: &Big) {
|
fn push_big_point(orbit: &mut RefOrbit, zr: &Big, zi: &Big) {
|
||||||
let (lr, li) = (big_log2_floor(zr), big_log2_floor(zi));
|
let (lr, li) = (zr.log2_floor(), zi.log2_floor());
|
||||||
let top = lr.max(li);
|
let top = lr.max(li);
|
||||||
match top {
|
match top {
|
||||||
Some(top) if top < TINY_LOG2 as isize => {
|
Some(top) if top < TINY_LOG2 as isize => {
|
||||||
let k = top + 1;
|
let k = top + 1;
|
||||||
let mr = (zr.clone() << -k).to_f64().value() as f32;
|
let mr = (zr.clone() << -k).to_f64() as f32;
|
||||||
let mi = (zi.clone() << -k).to_f64().value() as f32;
|
let mi = (zi.clone() << -k).to_f64() as f32;
|
||||||
orbit.points.push([mr, mi]);
|
orbit.points.push([mr, mi]);
|
||||||
orbit.exps.push(k as i32);
|
orbit.exps.push(k as i32);
|
||||||
}
|
}
|
||||||
_ => orbit.push([zr.to_f64().value() as f32, zi.to_f64().value() as f32]),
|
_ => orbit.push([zr.to_f64() as f32, zi.to_f64() as f32]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,23 +139,17 @@ pub fn compute_reference(
|
|||||||
let morph = morph.filter(|&(_, w)| w != 0.0);
|
let morph = morph.filter(|&(_, w)| w != 0.0);
|
||||||
if precision <= F64_MAX_PRECISION {
|
if precision <= F64_MAX_PRECISION {
|
||||||
let k = StepConstsF64 {
|
let k = StepConstsF64 {
|
||||||
c: (c_re.to_f64().value(), c_im.to_f64().value()),
|
c: (c_re.to_f64(), c_im.to_f64()),
|
||||||
p: phoenix_p,
|
p: phoenix_p,
|
||||||
l: lambda_l,
|
l: lambda_l,
|
||||||
cpow: complex_power,
|
cpow: complex_power,
|
||||||
power,
|
power,
|
||||||
};
|
};
|
||||||
return compute_reference_f64(
|
return compute_reference_f64((z0_re.to_f64(), z0_im.to_f64()), max_iter, kind, &k, morph);
|
||||||
(z0_re.to_f64().value(), z0_im.to_f64().value()),
|
|
||||||
max_iter,
|
|
||||||
kind,
|
|
||||||
&k,
|
|
||||||
morph,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
let k = StepConsts {
|
let k = StepConsts {
|
||||||
cr: c_re.clone().with_precision(precision).value(),
|
cr: c_re.clone().with_precision(precision),
|
||||||
ci: c_im.clone().with_precision(precision).value(),
|
ci: c_im.clone().with_precision(precision),
|
||||||
pr: big_from_f64(phoenix_p.0, precision),
|
pr: big_from_f64(phoenix_p.0, precision),
|
||||||
pi: big_from_f64(phoenix_p.1, precision),
|
pi: big_from_f64(phoenix_p.1, precision),
|
||||||
lr: big_from_f64(lambda_l.0, precision),
|
lr: big_from_f64(lambda_l.0, precision),
|
||||||
@@ -292,7 +278,7 @@ struct StepConsts {
|
|||||||
precision: usize,
|
precision: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [`compute_reference`] at arbitrary precision (`FBig`), for deep views.
|
/// [`compute_reference`] at arbitrary precision ([`Big`]), for deep views.
|
||||||
fn compute_reference_big(
|
fn compute_reference_big(
|
||||||
z0_re: &Big,
|
z0_re: &Big,
|
||||||
z0_im: &Big,
|
z0_im: &Big,
|
||||||
@@ -304,11 +290,11 @@ fn compute_reference_big(
|
|||||||
let precision = k.precision;
|
let precision = k.precision;
|
||||||
let morph = morph.map(|(from, w)| (from, big_from_f64(w, precision)));
|
let morph = morph.map(|(from, w)| (from, big_from_f64(w, precision)));
|
||||||
|
|
||||||
let mut zr = z0_re.clone().with_precision(precision).value();
|
let mut zr = z0_re.clone().with_precision(precision);
|
||||||
let mut zi = z0_im.clone().with_precision(precision).value();
|
let mut zi = z0_im.clone().with_precision(precision);
|
||||||
// Previous iterate, for the Phoenix two-term recurrence (Y_{-1} = 0).
|
// Previous iterate, for the Phoenix two-term recurrence (Y_{-1} = 0).
|
||||||
let mut zr_prev = big_zero(precision);
|
let mut zr_prev = Big::zero(precision);
|
||||||
let mut zi_prev = big_zero(precision);
|
let mut zi_prev = Big::zero(precision);
|
||||||
|
|
||||||
let mut points = RefOrbit::with_capacity(max_iter as usize + 1);
|
let mut points = RefOrbit::with_capacity(max_iter as usize + 1);
|
||||||
|
|
||||||
@@ -332,8 +318,8 @@ fn compute_reference_big(
|
|||||||
// Shift the previous iterate (only the Phoenix arm reads it).
|
// Shift the previous iterate (only the Phoenix arm reads it).
|
||||||
zr_prev = zr;
|
zr_prev = zr;
|
||||||
zi_prev = zi;
|
zi_prev = zi;
|
||||||
zr = new_zr.with_precision(precision).value();
|
zr = new_zr.with_precision(precision);
|
||||||
zi = new_zi.with_precision(precision).value();
|
zi = new_zi.with_precision(precision);
|
||||||
}
|
}
|
||||||
|
|
||||||
points
|
points
|
||||||
@@ -361,7 +347,7 @@ fn step(
|
|||||||
FractalKind::BurningShip => {
|
FractalKind::BurningShip => {
|
||||||
// (|zr| + i|zi|)^2 = (zr^2 - zi^2) + 2|zr zi| i.
|
// (|zr| + i|zi|)^2 = (zr^2 - zi^2) + 2|zr zi| i.
|
||||||
let re = &zr.sqr() - &zi.sqr() + cr;
|
let re = &zr.sqr() - &zi.sqr() + cr;
|
||||||
let im = big_abs((zr * zi) << 1) + ci;
|
let im = ((zr * zi) << 1).abs() + ci;
|
||||||
(re, im)
|
(re, im)
|
||||||
}
|
}
|
||||||
FractalKind::Tricorn => {
|
FractalKind::Tricorn => {
|
||||||
@@ -376,14 +362,14 @@ fn step(
|
|||||||
}
|
}
|
||||||
FractalKind::Celtic => {
|
FractalKind::Celtic => {
|
||||||
// |Re(z^2)| + i·Im(z^2): abs the real output of the square.
|
// |Re(z^2)| + i·Im(z^2): abs the real output of the square.
|
||||||
let re = big_abs(&zr.sqr() - &zi.sqr()) + cr;
|
let re = (&zr.sqr() - &zi.sqr()).abs() + cr;
|
||||||
let im = ((zr * zi) << 1) + ci;
|
let im = ((zr * zi) << 1) + ci;
|
||||||
(re, im)
|
(re, im)
|
||||||
}
|
}
|
||||||
FractalKind::Perpendicular => {
|
FractalKind::Perpendicular => {
|
||||||
// (x^2 - y^2) - 2·x·|y| i: abs the imaginary input.
|
// (x^2 - y^2) - 2·x·|y| i: abs the imaginary input.
|
||||||
let re = &zr.sqr() - &zi.sqr() + cr;
|
let re = &zr.sqr() - &zi.sqr() + cr;
|
||||||
let im = if *zi < Big::ZERO {
|
let im = if zi.is_negative() {
|
||||||
ci + ((zr * zi) << 1)
|
ci + ((zr * zi) << 1)
|
||||||
} else {
|
} else {
|
||||||
ci - ((zr * zi) << 1)
|
ci - ((zr * zi) << 1)
|
||||||
@@ -392,8 +378,8 @@ fn step(
|
|||||||
}
|
}
|
||||||
FractalKind::Buffalo => {
|
FractalKind::Buffalo => {
|
||||||
// |Re(z^2)| - |Im(z^2)| i: abs both outputs.
|
// |Re(z^2)| - |Im(z^2)| i: abs both outputs.
|
||||||
let re = big_abs(&zr.sqr() - &zi.sqr()) + cr;
|
let re = (&zr.sqr() - &zi.sqr()).abs() + cr;
|
||||||
let im = ci - big_abs((zr * zi) << 1);
|
let im = ci - ((zr * zi) << 1).abs();
|
||||||
(re, im)
|
(re, im)
|
||||||
}
|
}
|
||||||
FractalKind::Phoenix => {
|
FractalKind::Phoenix => {
|
||||||
@@ -406,7 +392,7 @@ fn step(
|
|||||||
}
|
}
|
||||||
FractalKind::Lambda => {
|
FractalKind::Lambda => {
|
||||||
// λ·z(1 - z) + c: logistic map plus the usual additive `c`.
|
// λ·z(1 - z) + c: logistic map plus the usual additive `c`.
|
||||||
let re2 = 1 - zr;
|
let re2 = Big::from_f64(1.0, k.precision) - zr;
|
||||||
let im2 = -zi;
|
let im2 = -zi;
|
||||||
let lzr = &k.lr * zr - &k.li * zi;
|
let lzr = &k.lr * zr - &k.li * zi;
|
||||||
let lzi = &k.lr * zi + &k.li * zr;
|
let lzi = &k.lr * zi + &k.li * zr;
|
||||||
@@ -421,36 +407,20 @@ fn step(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn big_zero(precision: usize) -> Big {
|
|
||||||
Big::from(0i32).with_precision(precision).value()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Absolute value of a `Big`. The sign comes from the `Big` itself: through
|
|
||||||
/// f64, anything below ~1e-308 reads as ±0 and would keep its sign.
|
|
||||||
fn big_abs(x: Big) -> Big {
|
|
||||||
if x < Big::ZERO { -x } else { x }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `(zr + i zi)^power` by repeated complex multiply at `precision` bits.
|
/// `(zr + i zi)^power` by repeated complex multiply at `precision` bits.
|
||||||
fn complex_pow(zr: &Big, zi: &Big, power: u32, precision: usize) -> (Big, Big) {
|
fn complex_pow(zr: &Big, zi: &Big, power: u32, precision: usize) -> (Big, Big) {
|
||||||
let mut rr = Big::from(1i32).with_precision(precision).value();
|
let mut rr = Big::from_f64(1.0, precision);
|
||||||
let mut ri = big_zero(precision);
|
let mut ri = Big::zero(precision);
|
||||||
for _ in 0..power {
|
for _ in 0..power {
|
||||||
// (rr + i ri)(zr + i zi) = (rr zr - ri zi) + (rr zi + ri zr) i.
|
// (rr + i ri)(zr + i zi) = (rr zr - ri zi) + (rr zi + ri zr) i.
|
||||||
let nr = (&rr * zr - &ri * zi).with_precision(precision).value();
|
let nr = (&rr * zr - &ri * zi).with_precision(precision);
|
||||||
let ni = (&rr * zi + &ri * zr).with_precision(precision).value();
|
let ni = (&rr * zi + &ri * zr).with_precision(precision);
|
||||||
rr = nr;
|
rr = nr;
|
||||||
ri = ni;
|
ri = ni;
|
||||||
}
|
}
|
||||||
(rr, ri)
|
(rr, ri)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `true` if `x` is exactly zero (special-cases `ln(0)`). Not via f64,
|
|
||||||
/// which flushes values below ~1e-308 to zero.
|
|
||||||
fn is_big_zero(x: &Big) -> bool {
|
|
||||||
x.repr().significand().is_zero()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `(zr + i zi)^(pr + i pi)` for a complex exponent, via the principal branch
|
/// `(zr + i zi)^(pr + i pi)` for a complex exponent, via the principal branch
|
||||||
/// `z^p = exp(p·ln z)` where `ln z = ln|z| + i·arg(z)`. Used by
|
/// `z^p = exp(p·ln z)` where `ln z = ln|z| + i·arg(z)`. Used by
|
||||||
/// `ComplexMultibrot`; must be kept in sync with the shader's `cpow`.
|
/// `ComplexMultibrot`; must be kept in sync with the shader's `cpow`.
|
||||||
@@ -458,14 +428,14 @@ fn is_big_zero(x: &Big) -> bool {
|
|||||||
/// panic; this is the correct limit for the `Re(p) > 0` region the UI
|
/// panic; this is the correct limit for the `Re(p) > 0` region the UI
|
||||||
/// exposes).
|
/// exposes).
|
||||||
fn complex_pow_complex(zr: &Big, zi: &Big, pr: &Big, pi: &Big, precision: usize) -> (Big, Big) {
|
fn complex_pow_complex(zr: &Big, zi: &Big, pr: &Big, pi: &Big, precision: usize) -> (Big, Big) {
|
||||||
if is_big_zero(zr) && is_big_zero(zi) {
|
if zr.is_zero() && zi.is_zero() {
|
||||||
return (big_zero(precision), big_zero(precision));
|
return (Big::zero(precision), Big::zero(precision));
|
||||||
}
|
}
|
||||||
let r2 = &zr.sqr() + &zi.sqr();
|
let r2 = &zr.sqr() + &zi.sqr();
|
||||||
let ln_r = r2.ln() >> 1; // 0.5 * ln(r2) = ln(sqrt(r2)); exact halving.
|
let ln_r = r2.ln() >> 1; // 0.5 * ln(r2) = ln(sqrt(r2)); exact halving.
|
||||||
let theta = zi.atan2(zr);
|
let theta = zi.atan2(zr);
|
||||||
let exp_re = (pr * &ln_r - pi * &theta).with_precision(precision).value();
|
let exp_re = (pr * &ln_r - pi * &theta).with_precision(precision);
|
||||||
let exp_im = (pr * &theta + pi * &ln_r).with_precision(precision).value();
|
let exp_im = (pr * &theta + pi * &ln_r).with_precision(precision);
|
||||||
let mag = exp_re.exp();
|
let mag = exp_re.exp();
|
||||||
let (sin_a, cos_a) = exp_im.sin_cos();
|
let (sin_a, cos_a) = exp_im.sin_cos();
|
||||||
(&mag * &cos_a, &mag * &sin_a)
|
(&mag * &cos_a, &mag * &sin_a)
|
||||||
@@ -486,7 +456,7 @@ pub fn compute_set_reference(
|
|||||||
complex_power: (f64, f64),
|
complex_power: (f64, f64),
|
||||||
morph: Option<(FractalKind, f64)>,
|
morph: Option<(FractalKind, f64)>,
|
||||||
) -> RefOrbit {
|
) -> RefOrbit {
|
||||||
let zero = big_zero(precision);
|
let zero = Big::zero(precision);
|
||||||
compute_reference(
|
compute_reference(
|
||||||
&zero,
|
&zero,
|
||||||
&zero,
|
&zero,
|
||||||
@@ -511,19 +481,19 @@ mod tests {
|
|||||||
/// `to_f64` reads as ±0.
|
/// `to_f64` reads as ±0.
|
||||||
#[test]
|
#[test]
|
||||||
fn sign_and_zero_below_f64_range() {
|
fn sign_and_zero_below_f64_range() {
|
||||||
let tiny = Big::try_from(1.0_f64).unwrap().with_precision(64).value() >> 5000;
|
let tiny = Big::from_f64(1.0, 64) >> 5000;
|
||||||
assert!(!is_big_zero(&tiny));
|
assert!(!tiny.is_zero());
|
||||||
assert!(is_big_zero(&big_zero(64)));
|
assert!(Big::zero(64).is_zero());
|
||||||
assert_eq!(big_abs(-tiny.clone()), tiny);
|
assert_eq!((-tiny.clone()).abs(), tiny);
|
||||||
assert_eq!(big_abs(tiny.clone()), tiny);
|
assert_eq!(tiny.clone().abs(), tiny);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The high-precision reference must agree with a plain f64 iteration for a
|
/// The high-precision reference must agree with a plain f64 iteration for a
|
||||||
/// shallow point (where f64 is accurate).
|
/// shallow point (where f64 is accurate).
|
||||||
#[test]
|
#[test]
|
||||||
fn reference_matches_naive_f64() {
|
fn reference_matches_naive_f64() {
|
||||||
let cr = Big::try_from(-0.75_f64).unwrap();
|
let cr = Big::from_f64(-0.75, 53);
|
||||||
let ci = Big::try_from(0.1_f64).unwrap();
|
let ci = Big::from_f64(0.1, 53);
|
||||||
let points = compute_set_reference(
|
let points = compute_set_reference(
|
||||||
&cr,
|
&cr,
|
||||||
&ci,
|
&ci,
|
||||||
@@ -656,8 +626,8 @@ mod tests {
|
|||||||
/// A point inside the main cardioid never escapes: full-length orbit.
|
/// A point inside the main cardioid never escapes: full-length orbit.
|
||||||
#[test]
|
#[test]
|
||||||
fn interior_orbit_runs_full_length() {
|
fn interior_orbit_runs_full_length() {
|
||||||
let cr = Big::try_from(-0.2_f64).unwrap();
|
let cr = Big::from_f64(-0.2, 53);
|
||||||
let ci = Big::try_from(0.0_f64).unwrap();
|
let ci = Big::from_f64(0.0, 53);
|
||||||
let points = compute_set_reference(
|
let points = compute_set_reference(
|
||||||
&cr,
|
&cr,
|
||||||
&ci,
|
&ci,
|
||||||
@@ -676,8 +646,8 @@ mod tests {
|
|||||||
/// Burning Ship reference matches a naive f64 iteration of the same formula.
|
/// Burning Ship reference matches a naive f64 iteration of the same formula.
|
||||||
#[test]
|
#[test]
|
||||||
fn burning_ship_reference_matches_naive_f64() {
|
fn burning_ship_reference_matches_naive_f64() {
|
||||||
let cr = Big::try_from(-1.75_f64).unwrap();
|
let cr = Big::from_f64(-1.75, 53);
|
||||||
let ci = Big::try_from(-0.03_f64).unwrap();
|
let ci = Big::from_f64(-0.03, 53);
|
||||||
let points = compute_set_reference(
|
let points = compute_set_reference(
|
||||||
&cr,
|
&cr,
|
||||||
&ci,
|
&ci,
|
||||||
@@ -707,8 +677,8 @@ mod tests {
|
|||||||
/// Multibrot (power 3) reference matches a naive f64 cube iteration.
|
/// Multibrot (power 3) reference matches a naive f64 cube iteration.
|
||||||
#[test]
|
#[test]
|
||||||
fn multibrot3_reference_matches_naive_f64() {
|
fn multibrot3_reference_matches_naive_f64() {
|
||||||
let cr = Big::try_from(0.3_f64).unwrap();
|
let cr = Big::from_f64(0.3, 53);
|
||||||
let ci = Big::try_from(0.2_f64).unwrap();
|
let ci = Big::from_f64(0.2, 53);
|
||||||
let points = compute_set_reference(
|
let points = compute_set_reference(
|
||||||
&cr,
|
&cr,
|
||||||
&ci,
|
&ci,
|
||||||
@@ -740,10 +710,10 @@ mod tests {
|
|||||||
/// Julia orbit (fixed c, z0 = center) matches a naive f64 iteration.
|
/// Julia orbit (fixed c, z0 = center) matches a naive f64 iteration.
|
||||||
#[test]
|
#[test]
|
||||||
fn julia_reference_matches_naive_f64() {
|
fn julia_reference_matches_naive_f64() {
|
||||||
let z0_re = Big::try_from(0.15_f64).unwrap();
|
let z0_re = Big::from_f64(0.15, 53);
|
||||||
let z0_im = Big::try_from(-0.1_f64).unwrap();
|
let z0_im = Big::from_f64(-0.1, 53);
|
||||||
let c_re = Big::try_from(-0.8_f64).unwrap();
|
let c_re = Big::from_f64(-0.8, 53);
|
||||||
let c_im = Big::try_from(0.156_f64).unwrap();
|
let c_im = Big::from_f64(0.156, 53);
|
||||||
let points = compute_reference(
|
let points = compute_reference(
|
||||||
&z0_re,
|
&z0_re,
|
||||||
&z0_im,
|
&z0_im,
|
||||||
@@ -778,10 +748,10 @@ mod tests {
|
|||||||
let (lr, li) = (-0.5_f64, 0.2_f64);
|
let (lr, li) = (-0.5_f64, 0.2_f64);
|
||||||
let (cr, ci) = (0.1_f64, -0.3_f64);
|
let (cr, ci) = (0.1_f64, -0.3_f64);
|
||||||
let points = compute_reference(
|
let points = compute_reference(
|
||||||
&Big::try_from(0.2_f64).unwrap(),
|
&Big::from_f64(0.2, 53),
|
||||||
&Big::try_from(0.1_f64).unwrap(),
|
&Big::from_f64(0.1, 53),
|
||||||
&Big::try_from(cr).unwrap(),
|
&Big::from_f64(cr, 53),
|
||||||
&Big::try_from(ci).unwrap(),
|
&Big::from_f64(ci, 53),
|
||||||
60,
|
60,
|
||||||
200,
|
200,
|
||||||
FractalKind::Lambda,
|
FractalKind::Lambda,
|
||||||
@@ -807,8 +777,8 @@ mod tests {
|
|||||||
/// Celtic reference matches a naive f64 iteration: real = |x^2 - y^2| + cr.
|
/// Celtic reference matches a naive f64 iteration: real = |x^2 - y^2| + cr.
|
||||||
#[test]
|
#[test]
|
||||||
fn celtic_reference_matches_naive_f64() {
|
fn celtic_reference_matches_naive_f64() {
|
||||||
let cr = Big::try_from(-0.6_f64).unwrap();
|
let cr = Big::from_f64(-0.6, 53);
|
||||||
let ci = Big::try_from(0.4_f64).unwrap();
|
let ci = Big::from_f64(0.4, 53);
|
||||||
let points = compute_set_reference(
|
let points = compute_set_reference(
|
||||||
&cr,
|
&cr,
|
||||||
&ci,
|
&ci,
|
||||||
@@ -839,8 +809,8 @@ mod tests {
|
|||||||
/// real = x^2 - y^2 + cr, imag = -2·x·|y| + ci.
|
/// real = x^2 - y^2 + cr, imag = -2·x·|y| + ci.
|
||||||
#[test]
|
#[test]
|
||||||
fn perpendicular_reference_matches_naive_f64() {
|
fn perpendicular_reference_matches_naive_f64() {
|
||||||
let cr = Big::try_from(-0.7_f64).unwrap();
|
let cr = Big::from_f64(-0.7, 53);
|
||||||
let ci = Big::try_from(-0.2_f64).unwrap();
|
let ci = Big::from_f64(-0.2, 53);
|
||||||
let points = compute_set_reference(
|
let points = compute_set_reference(
|
||||||
&cr,
|
&cr,
|
||||||
&ci,
|
&ci,
|
||||||
@@ -871,8 +841,8 @@ mod tests {
|
|||||||
/// real = |x^2 - y^2| + cr, imag = -|2·x·y| + ci.
|
/// real = |x^2 - y^2| + cr, imag = -|2·x·y| + ci.
|
||||||
#[test]
|
#[test]
|
||||||
fn buffalo_reference_matches_naive_f64() {
|
fn buffalo_reference_matches_naive_f64() {
|
||||||
let cr = Big::try_from(-1.2_f64).unwrap();
|
let cr = Big::from_f64(-1.2, 53);
|
||||||
let ci = Big::try_from(-0.35_f64).unwrap();
|
let ci = Big::from_f64(-0.35, 53);
|
||||||
let points = compute_set_reference(
|
let points = compute_set_reference(
|
||||||
&cr,
|
&cr,
|
||||||
&ci,
|
&ci,
|
||||||
@@ -903,8 +873,8 @@ mod tests {
|
|||||||
/// `z_{n+1} = z_n^2 + c + p·z_{n-1}` (z_0 = 0, z_{-1} = 0).
|
/// `z_{n+1} = z_n^2 + c + p·z_{n-1}` (z_0 = 0, z_{-1} = 0).
|
||||||
#[test]
|
#[test]
|
||||||
fn phoenix_reference_matches_naive_f64() {
|
fn phoenix_reference_matches_naive_f64() {
|
||||||
let cr = Big::try_from(0.5667_f64).unwrap();
|
let cr = Big::from_f64(0.5667, 53);
|
||||||
let ci = Big::try_from(0.0_f64).unwrap();
|
let ci = Big::from_f64(0.0, 53);
|
||||||
let p = (-0.5_f64, 0.0_f64);
|
let p = (-0.5_f64, 0.0_f64);
|
||||||
let points = compute_set_reference(
|
let points = compute_set_reference(
|
||||||
&cr,
|
&cr,
|
||||||
@@ -942,8 +912,8 @@ mod tests {
|
|||||||
/// iteration of `z^p = exp(p·ln z)`.
|
/// iteration of `z^p = exp(p·ln z)`.
|
||||||
#[test]
|
#[test]
|
||||||
fn complex_multibrot_reference_matches_naive_f64() {
|
fn complex_multibrot_reference_matches_naive_f64() {
|
||||||
let cr = Big::try_from(0.1_f64).unwrap();
|
let cr = Big::from_f64(0.1, 53);
|
||||||
let ci = Big::try_from(-0.2_f64).unwrap();
|
let ci = Big::from_f64(-0.2, 53);
|
||||||
let power = (2.5_f64, 0.3_f64);
|
let power = (2.5_f64, 0.3_f64);
|
||||||
let points = compute_set_reference(
|
let points = compute_set_reference(
|
||||||
&cr,
|
&cr,
|
||||||
@@ -987,8 +957,8 @@ mod tests {
|
|||||||
|
|
||||||
fn set_ref(cr: f64, ci: f64, kind: FractalKind, morph: Option<(FractalKind, f64)>) -> RefOrbit {
|
fn set_ref(cr: f64, ci: f64, kind: FractalKind, morph: Option<(FractalKind, f64)>) -> RefOrbit {
|
||||||
compute_set_reference(
|
compute_set_reference(
|
||||||
&Big::try_from(cr).unwrap(),
|
&Big::from_f64(cr, 53),
|
||||||
&Big::try_from(ci).unwrap(),
|
&Big::from_f64(ci, 53),
|
||||||
60,
|
60,
|
||||||
200,
|
200,
|
||||||
kind,
|
kind,
|
||||||
|
|||||||
+50
-14
@@ -12,7 +12,8 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use eframe::egui_wgpu::{self, wgpu};
|
#[cfg(feature = "gui")]
|
||||||
|
use eframe::egui_wgpu;
|
||||||
use wgpu::util::DeviceExt as _;
|
use wgpu::util::DeviceExt as _;
|
||||||
|
|
||||||
use super::kind::FractalKind;
|
use super::kind::FractalKind;
|
||||||
@@ -196,7 +197,9 @@ struct Lipschitz {
|
|||||||
|
|
||||||
impl Lipschitz {
|
impl Lipschitz {
|
||||||
fn new(device: &wgpu::Device) -> Self {
|
fn new(device: &wgpu::Device) -> Self {
|
||||||
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
let module = unsafe {
|
||||||
|
device.create_shader_module_trusted(
|
||||||
|
wgpu::ShaderModuleDescriptor {
|
||||||
label: Some("lipschitz"),
|
label: Some("lipschitz"),
|
||||||
source: wgpu::ShaderSource::Wgsl(
|
source: wgpu::ShaderSource::Wgsl(
|
||||||
concat!(
|
concat!(
|
||||||
@@ -205,7 +208,10 @@ impl Lipschitz {
|
|||||||
)
|
)
|
||||||
.into(),
|
.into(),
|
||||||
),
|
),
|
||||||
});
|
},
|
||||||
|
wgpu::ShaderRuntimeChecks::unchecked(),
|
||||||
|
)
|
||||||
|
};
|
||||||
let texture_entry = |binding| wgpu::BindGroupLayoutEntry {
|
let texture_entry = |binding| wgpu::BindGroupLayoutEntry {
|
||||||
binding,
|
binding,
|
||||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||||
@@ -616,7 +622,9 @@ impl FractalRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(device: &wgpu::Device, target_format: wgpu::TextureFormat) -> Self {
|
pub fn new(device: &wgpu::Device, target_format: wgpu::TextureFormat) -> Self {
|
||||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
let shader = unsafe {
|
||||||
|
device.create_shader_module_trusted(
|
||||||
|
wgpu::ShaderModuleDescriptor {
|
||||||
label: Some("mandelbrot"),
|
label: Some("mandelbrot"),
|
||||||
source: wgpu::ShaderSource::Wgsl(
|
source: wgpu::ShaderSource::Wgsl(
|
||||||
concat!(
|
concat!(
|
||||||
@@ -626,7 +634,10 @@ impl FractalRenderer {
|
|||||||
)
|
)
|
||||||
.into(),
|
.into(),
|
||||||
),
|
),
|
||||||
});
|
},
|
||||||
|
wgpu::ShaderRuntimeChecks::unchecked(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
label: Some("fractal uniforms"),
|
label: Some("fractal uniforms"),
|
||||||
@@ -747,7 +758,9 @@ impl FractalRenderer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Colourise pass: data texture + colour uniforms → colour texture.
|
// Colourise pass: data texture + colour uniforms → colour texture.
|
||||||
let colorize_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
let colorize_shader = unsafe {
|
||||||
|
device.create_shader_module_trusted(
|
||||||
|
wgpu::ShaderModuleDescriptor {
|
||||||
label: Some("colorize"),
|
label: Some("colorize"),
|
||||||
source: wgpu::ShaderSource::Wgsl(
|
source: wgpu::ShaderSource::Wgsl(
|
||||||
concat!(
|
concat!(
|
||||||
@@ -757,7 +770,10 @@ impl FractalRenderer {
|
|||||||
)
|
)
|
||||||
.into(),
|
.into(),
|
||||||
),
|
),
|
||||||
});
|
},
|
||||||
|
wgpu::ShaderRuntimeChecks::unchecked(),
|
||||||
|
)
|
||||||
|
};
|
||||||
let colorize_bind_group_layout =
|
let colorize_bind_group_layout =
|
||||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
label: Some("colorize bind group layout"),
|
label: Some("colorize bind group layout"),
|
||||||
@@ -828,7 +844,9 @@ impl FractalRenderer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Blit pipeline: samples the cache texture onto egui's surface.
|
// Blit pipeline: samples the cache texture onto egui's surface.
|
||||||
let blit_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
let blit_shader = unsafe {
|
||||||
|
device.create_shader_module_trusted(
|
||||||
|
wgpu::ShaderModuleDescriptor {
|
||||||
label: Some("blit"),
|
label: Some("blit"),
|
||||||
source: wgpu::ShaderSource::Wgsl(
|
source: wgpu::ShaderSource::Wgsl(
|
||||||
concat!(
|
concat!(
|
||||||
@@ -837,7 +855,10 @@ impl FractalRenderer {
|
|||||||
)
|
)
|
||||||
.into(),
|
.into(),
|
||||||
),
|
),
|
||||||
});
|
},
|
||||||
|
wgpu::ShaderRuntimeChecks::unchecked(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
let blit_bind_group_layout =
|
let blit_bind_group_layout =
|
||||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
@@ -1607,17 +1628,15 @@ pub fn render_readback_blocking(
|
|||||||
bytes
|
bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Like [`encode_png_with_progress`], but encodes the whole image at once
|
/// Strip a readback's row padding and convert it to tightly-packed RGBA8
|
||||||
/// (no progress) at the given compression level. Non-streaming, so the fast
|
/// (`width * height * 4` bytes, rows top to bottom).
|
||||||
/// `fdeflate` levels don't pay the streaming-mode size penalty.
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub fn encode_png(
|
pub fn unpad_rgba(
|
||||||
padded: &[u8],
|
padded: &[u8],
|
||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
padded_bpr: u32,
|
padded_bpr: u32,
|
||||||
swap_rb: bool,
|
swap_rb: bool,
|
||||||
compression: png::Compression,
|
|
||||||
) -> Vec<u8> {
|
) -> Vec<u8> {
|
||||||
let row = (width * 4) as usize;
|
let row = (width * 4) as usize;
|
||||||
let mut pixels = Vec::with_capacity(row * height as usize);
|
let mut pixels = Vec::with_capacity(row * height as usize);
|
||||||
@@ -1635,6 +1654,22 @@ pub fn encode_png(
|
|||||||
pixels.extend_from_slice(src);
|
pixels.extend_from_slice(src);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pixels
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Like [`encode_png_with_progress`], but encodes the whole image at once
|
||||||
|
/// (no progress) at the given compression level. Non-streaming, so the fast
|
||||||
|
/// `fdeflate` levels don't pay the streaming-mode size penalty.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub fn encode_png(
|
||||||
|
padded: &[u8],
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
padded_bpr: u32,
|
||||||
|
swap_rb: bool,
|
||||||
|
compression: png::Compression,
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let pixels = unpad_rgba(padded, width, height, padded_bpr, swap_rb);
|
||||||
|
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
{
|
{
|
||||||
@@ -1884,6 +1919,7 @@ pub struct FractalCallback {
|
|||||||
pub size_px: [u32; 2],
|
pub size_px: [u32; 2],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
impl egui_wgpu::CallbackTrait for FractalCallback {
|
impl egui_wgpu::CallbackTrait for FractalCallback {
|
||||||
fn prepare(
|
fn prepare(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
+192
-18
@@ -3,19 +3,20 @@
|
|||||||
// event loop, no worker-thread debounce (nothing to debounce for a one-shot
|
// event loop, no worker-thread debounce (nothing to debounce for a one-shot
|
||||||
// render); it just creates its own wgpu device, computes the reference orbit
|
// render); it just creates its own wgpu device, computes the reference orbit
|
||||||
// once, and renders through the same `ExportRender` path the "Export PNG"
|
// once, and renders through the same `ExportRender` path the "Export PNG"
|
||||||
// button uses.
|
// button uses. `--export-path -` writes to stdout instead: the PNG for a
|
||||||
|
// single image, or a raw RGBA8 video stream for an animation (for piping
|
||||||
|
// into ffmpeg).
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::{BTreeMap, HashMap};
|
||||||
|
use std::io::{IsTerminal, Write};
|
||||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||||
use std::sync::{Mutex, mpsc};
|
use std::sync::{Mutex, mpsc};
|
||||||
|
|
||||||
use eframe::egui_wgpu::wgpu;
|
|
||||||
|
|
||||||
use crate::app::{FractalApp, RefJob, parse_complex_pair, unix_timestamp};
|
use crate::app::{FractalApp, RefJob, parse_complex_pair, unix_timestamp};
|
||||||
use crate::cli::Cli;
|
use crate::cli::Cli;
|
||||||
use crate::fractal::{
|
use crate::fractal::{
|
||||||
ExportRender, FractalKind, FractalRenderer, PipelineKey, ShareState, encode_png,
|
ExportRender, FractalKind, FractalRenderer, PipelineKey, ShareState, encode_png,
|
||||||
export_to_png_blocking, render_readback_blocking,
|
export_to_png_blocking, render_readback_blocking, unpad_rgba,
|
||||||
};
|
};
|
||||||
use crate::view::{
|
use crate::view::{
|
||||||
ViewState, big_from_decimal_str, interpolate_f64, interpolate_view, parse_view_spec,
|
ViewState, big_from_decimal_str, interpolate_f64, interpolate_view, parse_view_spec,
|
||||||
@@ -25,6 +26,21 @@ use crate::view::{
|
|||||||
/// Cap on the output image dimension (px), to stay within GPU texture limits.
|
/// Cap on the output image dimension (px), to stay within GPU texture limits.
|
||||||
const MAX_DIM: u32 = 8192 * 16;
|
const MAX_DIM: u32 = 8192 * 16;
|
||||||
|
|
||||||
|
/// `--export-path` value meaning "write to stdout".
|
||||||
|
const STDOUT_PATH: &str = "-";
|
||||||
|
|
||||||
|
/// Refuse to dump binary image data onto a terminal.
|
||||||
|
fn check_stdout_piped() -> Result<(), String> {
|
||||||
|
if std::io::stdout().is_terminal() {
|
||||||
|
return Err(
|
||||||
|
"--export-path - writes binary data to stdout; pipe it somewhere \
|
||||||
|
(e.g. `| ffmpeg ...`)"
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn run(cli: Cli) -> Result<(), String> {
|
pub fn run(cli: Cli) -> Result<(), String> {
|
||||||
if cli.buddhabrot {
|
if cli.buddhabrot {
|
||||||
return Err("headless mode doesn't support --buddhabrot yet".into());
|
return Err("headless mode doesn't support --buddhabrot yet".into());
|
||||||
@@ -37,6 +53,13 @@ pub fn run(cli: Cli) -> Result<(), String> {
|
|||||||
// consumes `cli` to build the start state.
|
// consumes `cli` to build the start state.
|
||||||
let targets = AnimTargets::from_cli(&cli)?;
|
let targets = AnimTargets::from_cli(&cli)?;
|
||||||
let export_path = cli.export_path.clone();
|
let export_path = cli.export_path.clone();
|
||||||
|
if export_path.as_deref() == Some(STDOUT_PATH) {
|
||||||
|
check_stdout_piped()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if targets.shard.is_some() && !targets.any() {
|
||||||
|
return Err("--shard/--shards only apply to animations (give a --to-* target)".into());
|
||||||
|
}
|
||||||
|
|
||||||
let mut app = FractalApp::default_state();
|
let mut app = FractalApp::default_state();
|
||||||
app.apply_cli(cli);
|
app.apply_cli(cli);
|
||||||
@@ -74,8 +97,16 @@ pub fn run(cli: Cli) -> Result<(), String> {
|
|||||||
});
|
});
|
||||||
eprintln!();
|
eprintln!();
|
||||||
|
|
||||||
|
if export_path == STDOUT_PATH {
|
||||||
|
let mut out = std::io::stdout().lock();
|
||||||
|
out.write_all(&png)
|
||||||
|
.and_then(|()| out.flush())
|
||||||
|
.map_err(|e| format!("writing to stdout failed: {e}"))?;
|
||||||
|
eprintln!("wrote PNG to stdout ({width}×{height})");
|
||||||
|
} else {
|
||||||
std::fs::write(&export_path, &png).map_err(|e| format!("save failed: {e}"))?;
|
std::fs::write(&export_path, &png).map_err(|e| format!("save failed: {e}"))?;
|
||||||
println!("saved {export_path} ({width}×{height})");
|
println!("saved {export_path} ({width}×{height})");
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +130,8 @@ struct AnimTargets {
|
|||||||
fps: f64,
|
fps: f64,
|
||||||
duration: Option<f64>,
|
duration: Option<f64>,
|
||||||
linear: bool,
|
linear: bool,
|
||||||
|
/// `--shard K --shards N`: render only the K-th (1-based) of N parts.
|
||||||
|
shard: Option<(u32, u32)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AnimTargets {
|
impl AnimTargets {
|
||||||
@@ -109,6 +142,16 @@ impl AnimTargets {
|
|||||||
.transpose()
|
.transpose()
|
||||||
};
|
};
|
||||||
let to_cpow = pair("to-complex-power", &cli.to_complex_power)?;
|
let to_cpow = pair("to-complex-power", &cli.to_complex_power)?;
|
||||||
|
let shard = match (cli.shard, cli.shards) {
|
||||||
|
(None, None) | (None, Some(1)) => None,
|
||||||
|
(Some(k), Some(n)) if (1..=n).contains(&k) => Some((k, n)),
|
||||||
|
(Some(k), Some(n)) => {
|
||||||
|
return Err(format!(
|
||||||
|
"--shard {k} is out of range 1..={n} (--shards {n})"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
_ => return Err("--shard and --shards must be given together".into()),
|
||||||
|
};
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
to_view: cli.to_view.clone(),
|
to_view: cli.to_view.clone(),
|
||||||
to_share: cli.to_share.clone(),
|
to_share: cli.to_share.clone(),
|
||||||
@@ -125,6 +168,7 @@ impl AnimTargets {
|
|||||||
fps: cli.fps,
|
fps: cli.fps,
|
||||||
duration: cli.duration,
|
duration: cli.duration,
|
||||||
linear: cli.linear,
|
linear: cli.linear,
|
||||||
|
shard,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +192,9 @@ impl AnimTargets {
|
|||||||
/// state to `targets`, for feeding into ffmpeg: the camera, iteration count,
|
/// state to `targets`, for feeding into ffmpeg: the camera, iteration count,
|
||||||
/// per-kind constants (c, p, λ, complex power) and, through a kind morph,
|
/// per-kind constants (c, p, λ, complex power) and, through a kind morph,
|
||||||
/// the iteration formula, and the 3D camera angles. Everything else (colors, ...) stays fixed at
|
/// the iteration formula, and the 3D camera angles. Everything else (colors, ...) stays fixed at
|
||||||
/// whatever `apply_cli` set up for the start.
|
/// whatever `apply_cli` set up for the start. With `--export-path -`, frames
|
||||||
|
/// are streamed in order to stdout as raw RGBA8 (for ffmpeg's `rawvideo`
|
||||||
|
/// demuxer) instead of being written as PNGs.
|
||||||
fn run_animation(
|
fn run_animation(
|
||||||
mut app: FractalApp,
|
mut app: FractalApp,
|
||||||
targets: AnimTargets,
|
targets: AnimTargets,
|
||||||
@@ -169,6 +215,16 @@ fn run_animation(
|
|||||||
if frames < 2 {
|
if frames < 2 {
|
||||||
return Err("animation needs at least 2 frames".into());
|
return Err("animation needs at least 2 frames".into());
|
||||||
}
|
}
|
||||||
|
// Frames are still timed against the whole animation (`apply_frame` takes
|
||||||
|
// the global index); a shard only picks which of them this run renders.
|
||||||
|
let range = match targets.shard {
|
||||||
|
Some((_, n)) if n > frames => {
|
||||||
|
return Err(format!("--shards {n} is more than the {frames} frames"));
|
||||||
|
}
|
||||||
|
Some((k, n)) => shard_range(frames, k, n),
|
||||||
|
None => 0..frames,
|
||||||
|
};
|
||||||
|
let (first, count) = (range.start, range.len());
|
||||||
|
|
||||||
let from = app.view_state().clone();
|
let from = app.view_state().clone();
|
||||||
let (to, to_iterations_share) =
|
let (to, to_iterations_share) =
|
||||||
@@ -199,8 +255,17 @@ fn run_animation(
|
|||||||
let yaw1 = targets.to_yaw.map_or(yaw0, f32::to_radians);
|
let yaw1 = targets.to_yaw.map_or(yaw0, f32::to_radians);
|
||||||
let pitch1 = targets.to_pitch.map_or(pitch0, f32::to_radians);
|
let pitch1 = targets.to_pitch.map_or(pitch0, f32::to_radians);
|
||||||
|
|
||||||
|
let stream = export_path.as_deref() == Some(STDOUT_PATH);
|
||||||
let out_dir = export_path.unwrap_or_else(|| format!("frames-{}", unix_timestamp()));
|
let out_dir = export_path.unwrap_or_else(|| format!("frames-{}", unix_timestamp()));
|
||||||
std::fs::create_dir_all(&out_dir).map_err(|e| format!("failed to create {out_dir}: {e}"))?;
|
if stream {
|
||||||
|
eprintln!(
|
||||||
|
"streaming raw video to stdout; ffmpeg input: \
|
||||||
|
-f rawvideo -pix_fmt rgba -s {width}x{height} -r {fps} -i -"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
std::fs::create_dir_all(&out_dir)
|
||||||
|
.map_err(|e| format!("failed to create {out_dir}: {e}"))?;
|
||||||
|
}
|
||||||
|
|
||||||
// Everything about frame `i` is a pure function of its `t`, so the app can
|
// Everything about frame `i` is a pure function of its `t`, so the app can
|
||||||
// be put into any frame's state at any time, in any order.
|
// be put into any frame's state at any time, in any order.
|
||||||
@@ -232,7 +297,10 @@ fn run_animation(
|
|||||||
|
|
||||||
// Snapshot every frame's reference-orbit job up front (cheap: just the
|
// Snapshot every frame's reference-orbit job up front (cheap: just the
|
||||||
// parameters), so the orbits themselves can be computed in parallel.
|
// parameters), so the orbits themselves can be computed in parallel.
|
||||||
let jobs: Vec<RefJob> = (0..frames)
|
// `jobs[j]` is global frame `first + j`; the pipeline below works in
|
||||||
|
// local indices `j`, so the stdout writer's ordering is per shard.
|
||||||
|
let jobs: Vec<RefJob> = range
|
||||||
|
.clone()
|
||||||
.map(|i| {
|
.map(|i| {
|
||||||
apply_frame(&mut app, i);
|
apply_frame(&mut app, i);
|
||||||
app.reference_job()
|
app.reference_job()
|
||||||
@@ -249,7 +317,15 @@ fn run_animation(
|
|||||||
// part at deep zoom) → this thread renders each frame on the GPU → `threads`
|
// part at deep zoom) → this thread renders each frame on the GPU → `threads`
|
||||||
// workers PNG-encode and write frames. Frames flow through out of order
|
// workers PNG-encode and write frames. Frames flow through out of order
|
||||||
// (at most ~`threads` apart); each is written under its own index.
|
// (at most ~`threads` apart); each is written under its own index.
|
||||||
|
//
|
||||||
|
// When streaming, the last stage instead unpads frames to raw RGBA and a
|
||||||
|
// single writer thread puts them back in order before writing to stdout.
|
||||||
|
// Its reorder buffer can't apply backpressure (blocking it while waiting
|
||||||
|
// for frame `k` could stall the pipeline before `k` gets through), so the
|
||||||
|
// orbit workers bound it instead: they don't start a frame more than
|
||||||
|
// `window` ahead of the last one written.
|
||||||
let threads = std::thread::available_parallelism().map_or(4, |n| n.get());
|
let threads = std::thread::available_parallelism().map_or(4, |n| n.get());
|
||||||
|
let window = threads * 4;
|
||||||
let next_job = AtomicUsize::new(0);
|
let next_job = AtomicUsize::new(0);
|
||||||
let saved = AtomicUsize::new(0);
|
let saved = AtomicUsize::new(0);
|
||||||
let failed = AtomicBool::new(false);
|
let failed = AtomicBool::new(false);
|
||||||
@@ -259,20 +335,34 @@ fn run_animation(
|
|||||||
error.lock().unwrap().get_or_insert(e);
|
error.lock().unwrap().get_or_insert(e);
|
||||||
};
|
};
|
||||||
|
|
||||||
eprintln!("rendering {frames} frames ({width}×{height}) on {threads} threads…");
|
if let Some((k, n)) = targets.shard {
|
||||||
|
eprintln!(
|
||||||
|
"shard {k}/{n}: frames {}–{} of {frames}",
|
||||||
|
range.start + 1,
|
||||||
|
range.end
|
||||||
|
);
|
||||||
|
}
|
||||||
|
eprintln!("rendering {count} frames ({width}×{height}) on {threads} threads…");
|
||||||
let (png_tx, png_rx) = mpsc::sync_channel::<(usize, Vec<u8>, u32, bool)>(threads * 2);
|
let (png_tx, png_rx) = mpsc::sync_channel::<(usize, Vec<u8>, u32, bool)>(threads * 2);
|
||||||
let png_rx = Mutex::new(png_rx);
|
let png_rx = Mutex::new(png_rx);
|
||||||
|
let (raw_tx, raw_rx) = mpsc::sync_channel::<(usize, Vec<u8>)>(threads * 2);
|
||||||
std::thread::scope(|scope| {
|
std::thread::scope(|scope| {
|
||||||
let (ref_tx, ref_rx) = mpsc::sync_channel::<(usize, crate::fractal::RefOrbit)>(threads * 2);
|
let (ref_tx, ref_rx) = mpsc::sync_channel::<(usize, crate::fractal::RefOrbit)>(threads * 2);
|
||||||
for _ in 0..threads {
|
for _ in 0..threads {
|
||||||
let ref_tx = ref_tx.clone();
|
let ref_tx = ref_tx.clone();
|
||||||
let (jobs, next_job, failed) = (&jobs, &next_job, &failed);
|
let (jobs, next_job, saved, failed) = (&jobs, &next_job, &saved, &failed);
|
||||||
scope.spawn(move || {
|
scope.spawn(move || {
|
||||||
loop {
|
loop {
|
||||||
let i = next_job.fetch_add(1, Ordering::Relaxed);
|
let i = next_job.fetch_add(1, Ordering::Relaxed);
|
||||||
if i >= jobs.len() || failed.load(Ordering::Relaxed) {
|
if i >= jobs.len() || failed.load(Ordering::Relaxed) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
while stream
|
||||||
|
&& i >= saved.load(Ordering::Relaxed) + window
|
||||||
|
&& !failed.load(Ordering::Relaxed)
|
||||||
|
{
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(2));
|
||||||
|
}
|
||||||
if ref_tx.send((i, jobs[i].compute())).is_err() {
|
if ref_tx.send((i, jobs[i].compute())).is_err() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -281,7 +371,34 @@ fn run_animation(
|
|||||||
}
|
}
|
||||||
drop(ref_tx);
|
drop(ref_tx);
|
||||||
|
|
||||||
|
if stream {
|
||||||
|
let (saved, fail) = (&saved, &fail);
|
||||||
|
scope.spawn(move || {
|
||||||
|
let mut out = std::io::stdout().lock();
|
||||||
|
let mut pending = BTreeMap::new();
|
||||||
|
let mut next = 0;
|
||||||
|
for (i, raw) in raw_rx.iter() {
|
||||||
|
pending.insert(i, raw);
|
||||||
|
while let Some(raw) = pending.remove(&next) {
|
||||||
|
if let Err(e) = out.write_all(&raw) {
|
||||||
|
fail(format!("writing to stdout failed: {e}"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next += 1;
|
||||||
|
saved.store(next, Ordering::Relaxed);
|
||||||
|
eprint!("\r[{next:>4}/{count}] streamed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Err(e) = out.flush() {
|
||||||
|
fail(format!("writing to stdout failed: {e}"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
drop(raw_rx);
|
||||||
|
}
|
||||||
|
|
||||||
for _ in 0..threads {
|
for _ in 0..threads {
|
||||||
|
let raw_tx = raw_tx.clone();
|
||||||
let (png_rx, out_dir, saved, failed, fail) =
|
let (png_rx, out_dir, saved, failed, fail) =
|
||||||
(&png_rx, &out_dir, &saved, &failed, &fail);
|
(&png_rx, &out_dir, &saved, &failed, &fail);
|
||||||
scope.spawn(move || {
|
scope.spawn(move || {
|
||||||
@@ -290,18 +407,26 @@ fn run_animation(
|
|||||||
let Ok((i, padded, bpr, swap_rb)) = png_rx.lock().unwrap().recv() else {
|
let Ok((i, padded, bpr, swap_rb)) = png_rx.lock().unwrap().recv() else {
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
|
// After a failure, keep draining (without work) until the
|
||||||
|
// GPU stage hangs up, so it can't block on a full channel.
|
||||||
if failed.load(Ordering::Relaxed) {
|
if failed.load(Ordering::Relaxed) {
|
||||||
break;
|
continue;
|
||||||
|
}
|
||||||
|
if stream {
|
||||||
|
let raw = unpad_rgba(&padded, width, height, bpr, swap_rb);
|
||||||
|
// Only fails once the writer has failed and hung up.
|
||||||
|
let _ = raw_tx.send((i, raw));
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
let png =
|
let png =
|
||||||
encode_png(&padded, width, height, bpr, swap_rb, png::Compression::Fast);
|
encode_png(&padded, width, height, bpr, swap_rb, png::Compression::Fast);
|
||||||
let path = format!("{out_dir}/frame-{:05}.png", i + 1);
|
let path = format!("{out_dir}/frame-{:05}.png", first as usize + i + 1);
|
||||||
if let Err(e) = std::fs::write(&path, &png) {
|
if let Err(e) = std::fs::write(&path, &png) {
|
||||||
fail(format!("save failed: {e}"));
|
fail(format!("save failed: {e}"));
|
||||||
break;
|
continue;
|
||||||
}
|
}
|
||||||
let done = saved.fetch_add(1, Ordering::Relaxed) + 1;
|
let done = saved.fetch_add(1, Ordering::Relaxed) + 1;
|
||||||
eprint!("\r[{done:>4}/{frames}] saved");
|
eprint!("\r[{done:>4}/{count}] saved");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -314,7 +439,7 @@ fn run_animation(
|
|||||||
if failed.load(Ordering::Relaxed) {
|
if failed.load(Ordering::Relaxed) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
apply_frame(&mut app, i as u32);
|
apply_frame(&mut app, first + i as u32);
|
||||||
app.finish_reference(jobs[i].clone(), points);
|
app.finish_reference(jobs[i].clone(), points);
|
||||||
|
|
||||||
let uniforms = app.make_uniforms(aspect, height as f64);
|
let uniforms = app.make_uniforms(aspect, height as f64);
|
||||||
@@ -337,6 +462,7 @@ fn run_animation(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Dropping the channel ends lets the workers drain and exit.
|
// Dropping the channel ends lets the workers drain and exit.
|
||||||
|
drop(raw_tx);
|
||||||
drop(png_tx);
|
drop(png_tx);
|
||||||
drop(ref_rx);
|
drop(ref_rx);
|
||||||
});
|
});
|
||||||
@@ -346,14 +472,27 @@ fn run_animation(
|
|||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
let saved = saved.into_inner();
|
let saved = saved.into_inner();
|
||||||
if saved != frames as usize {
|
if saved != count {
|
||||||
return Err(format!("only {saved} of {frames} frames were rendered"));
|
return Err(format!("only {saved} of {count} frames were rendered"));
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("saved {frames} frames to {out_dir}/ ({width}×{height})");
|
if stream {
|
||||||
|
eprintln!("streamed {count} frames ({width}×{height})");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
println!("saved {count} frames to {out_dir}/ ({width}×{height})");
|
||||||
|
if targets.shard.is_some() {
|
||||||
|
println!(
|
||||||
|
"tip: once every shard is rendered into {out_dir}/, they form the full sequence; \
|
||||||
|
this shard alone: ffmpeg -framerate {fps} -start_number {} -i {out_dir}/frame-%05d.png \
|
||||||
|
-frames:v {count} -c:v libx264 -pix_fmt yuv420p out.mp4",
|
||||||
|
range.start + 1
|
||||||
|
);
|
||||||
|
} else {
|
||||||
println!(
|
println!(
|
||||||
"tip: ffmpeg -framerate {fps} -i {out_dir}/frame-%05d.png -c:v libx264 -pix_fmt yuv420p out.mp4"
|
"tip: ffmpeg -framerate {fps} -i {out_dir}/frame-%05d.png -c:v libx264 -pix_fmt yuv420p out.mp4"
|
||||||
);
|
);
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,6 +526,14 @@ fn parse_animation_target(
|
|||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Global frame indices of shard `shard` (1-based) out of `shards` equal
|
||||||
|
/// parts of a `frames`-frame animation. Consecutive shards tile `0..frames`
|
||||||
|
/// with no gap or overlap.
|
||||||
|
fn shard_range(frames: u32, shard: u32, shards: u32) -> std::ops::Range<u32> {
|
||||||
|
let bound = |k: u32| (k as u64 * frames as u64 / shards as u64) as u32;
|
||||||
|
bound(shard - 1)..bound(shard)
|
||||||
|
}
|
||||||
|
|
||||||
/// Ease-in/ease-out pacing: slow at both ends, fast through the middle.
|
/// Ease-in/ease-out pacing: slow at both ends, fast through the middle.
|
||||||
fn smoothstep(t: f64) -> f64 {
|
fn smoothstep(t: f64) -> f64 {
|
||||||
t * t * (3.0 - 2.0 * t)
|
t * t * (3.0 - 2.0 * t)
|
||||||
@@ -411,3 +558,30 @@ async fn request_device() -> Result<(wgpu::Device, wgpu::Queue), String> {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| format!("failed to create device: {e}"))
|
.map_err(|e| format!("failed to create device: {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::shard_range;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shards_tile_all_frames() {
|
||||||
|
for frames in [2, 3, 10, 97, 1000, u32::MAX] {
|
||||||
|
for shards in [1, 2, 3, 7, 10] {
|
||||||
|
if shards > frames {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut next = 0;
|
||||||
|
for k in 1..=shards {
|
||||||
|
let r = shard_range(frames, k, shards);
|
||||||
|
assert_eq!(
|
||||||
|
r.start, next,
|
||||||
|
"gap/overlap at shard {k}/{shards} of {frames}"
|
||||||
|
);
|
||||||
|
assert!(!r.is_empty(), "empty shard {k}/{shards} of {frames}");
|
||||||
|
next = r.end;
|
||||||
|
}
|
||||||
|
assert_eq!(next, frames);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+4
-1
@@ -1,7 +1,9 @@
|
|||||||
use std::f32::consts::PI;
|
use std::f32::consts::PI;
|
||||||
|
|
||||||
use bytemuck::{Pod, Zeroable};
|
use bytemuck::{Pod, Zeroable};
|
||||||
use egui::{Color32, Ui};
|
use ecolor::Color32;
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
|
use egui::Ui;
|
||||||
|
|
||||||
/// Maximum number of simultaneous lights.
|
/// Maximum number of simultaneous lights.
|
||||||
pub const MAX_LIGHT_COUNT: usize = 16;
|
pub const MAX_LIGHT_COUNT: usize = 16;
|
||||||
@@ -27,6 +29,7 @@ impl Default for Light {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Light {
|
impl Light {
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
pub fn widget(&mut self, ui: &mut Ui) -> bool {
|
pub fn widget(&mut self, ui: &mut Ui) -> bool {
|
||||||
let formater = |v, _| format!("{}°", ((v * 180. / std::f64::consts::PI) as u32));
|
let formater = |v, _| format!("{}°", ((v * 180. / std::f64::consts::PI) as u32));
|
||||||
let parser = |s: &str| {
|
let parser = |s: &str| {
|
||||||
|
|||||||
+22
-1
@@ -1,6 +1,8 @@
|
|||||||
// Without these, rust fails to infer Send/Sync trait impls
|
// Without these, rust fails to infer Send/Sync trait impls
|
||||||
// Probably caused by the new trait solver
|
// Probably caused by the new trait solver
|
||||||
#![recursion_limit = "256"]
|
#![recursion_limit = "256"]
|
||||||
|
// Without the `gui` feature, UI-only state and helpers go unused.
|
||||||
|
#![cfg_attr(not(feature = "gui"), allow(dead_code, unused_imports))]
|
||||||
|
|
||||||
// Fractal Explorer — Rust + wgpu + egui + WGSL deep-zoom Mandelbrot.
|
// Fractal Explorer — Rust + wgpu + egui + WGSL deep-zoom Mandelbrot.
|
||||||
//
|
//
|
||||||
@@ -9,6 +11,7 @@
|
|||||||
// and calls the wasm `main`, which boots eframe onto the page's <canvas>.
|
// and calls the wasm `main`, which boots eframe onto the page's <canvas>.
|
||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
|
mod bignum;
|
||||||
mod camera;
|
mod camera;
|
||||||
mod fractal;
|
mod fractal;
|
||||||
mod lights;
|
mod lights;
|
||||||
@@ -21,8 +24,17 @@ mod headless;
|
|||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
mod worker;
|
mod worker;
|
||||||
|
|
||||||
|
#[cfg(all(target_arch = "wasm32", not(feature = "gui")))]
|
||||||
|
compile_error!("the web build needs the `gui` feature");
|
||||||
|
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
use app::FractalApp;
|
use app::FractalApp;
|
||||||
|
|
||||||
|
#[cfg(all(feature = "gui", not(target_arch = "wasm32")))]
|
||||||
|
type MainResult = eframe::Result;
|
||||||
|
#[cfg(all(not(feature = "gui"), not(target_arch = "wasm32")))]
|
||||||
|
type MainResult = Result<(), String>;
|
||||||
|
|
||||||
/// wgpu configuration for eframe. The fractal fragment shader reads the
|
/// wgpu configuration for eframe. The fractal fragment shader reads the
|
||||||
/// reference orbit from a **storage buffer**, so the device must allow storage
|
/// reference orbit from a **storage buffer**, so the device must allow storage
|
||||||
/// buffers in the fragment stage. eframe's default requests WebGL2-downlevel
|
/// buffers in the fragment stage. eframe's default requests WebGL2-downlevel
|
||||||
@@ -30,6 +42,7 @@ use app::FractalApp;
|
|||||||
/// * request the adapter's real limits (which include storage buffers), and
|
/// * request the adapter's real limits (which include storage buffers), and
|
||||||
/// * force the WebGPU backend on the web (WebGL2 can't do storage buffers at
|
/// * force the WebGPU backend on the web (WebGL2 can't do storage buffers at
|
||||||
/// all) — failing cleanly on browsers without WebGPU, per the design.
|
/// all) — failing cleanly on browsers without WebGPU, per the design.
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
fn wgpu_options() -> eframe::egui_wgpu::WgpuConfiguration {
|
fn wgpu_options() -> eframe::egui_wgpu::WgpuConfiguration {
|
||||||
use eframe::egui_wgpu::{WgpuSetup, wgpu};
|
use eframe::egui_wgpu::{WgpuSetup, wgpu};
|
||||||
|
|
||||||
@@ -51,7 +64,7 @@ fn wgpu_options() -> eframe::egui_wgpu::WgpuConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn main() -> eframe::Result {
|
fn main() -> MainResult {
|
||||||
use clap::Parser as _;
|
use clap::Parser as _;
|
||||||
|
|
||||||
env_logger::builder()
|
env_logger::builder()
|
||||||
@@ -70,6 +83,13 @@ fn main() -> eframe::Result {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "gui"))]
|
||||||
|
{
|
||||||
|
eprintln!("error: built without the \"gui\" feature; only --headless is supported");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
let native_options = eframe::NativeOptions {
|
let native_options = eframe::NativeOptions {
|
||||||
renderer: eframe::Renderer::Wgpu,
|
renderer: eframe::Renderer::Wgpu,
|
||||||
wgpu_options: wgpu_options(),
|
wgpu_options: wgpu_options(),
|
||||||
@@ -80,6 +100,7 @@ fn main() -> eframe::Result {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
eframe::run_native(
|
eframe::run_native(
|
||||||
"Fractal Explorer",
|
"Fractal Explorer",
|
||||||
native_options,
|
native_options,
|
||||||
|
|||||||
+45
-55
@@ -1,6 +1,6 @@
|
|||||||
//! Camera / view state over the complex plane.
|
//! Camera / view state over the complex plane.
|
||||||
//!
|
//!
|
||||||
//! The center is stored in arbitrary precision (`FBig`) — this is what lets us
|
//! The center is stored in arbitrary precision ([`Big`]) — this is what lets us
|
||||||
//! zoom far past f64's ~1e13x limit. The pixel *scale* is a [`Scale`]: an
|
//! zoom far past f64's ~1e13x limit. The pixel *scale* is a [`Scale`]: an
|
||||||
//! f64 mantissa with its own `i32` binary exponent, so it isn't bound by
|
//! f64 mantissa with its own `i32` binary exponent, so it isn't bound by
|
||||||
//! f64's ~1e-308 range either (the floor, `Scale::MIN`, only keeps the GPU's
|
//! f64's ~1e-308 range either (the floor, `Scale::MIN`, only keeps the GPU's
|
||||||
@@ -10,11 +10,7 @@
|
|||||||
|
|
||||||
use core::str::FromStr;
|
use core::str::FromStr;
|
||||||
|
|
||||||
use dashu_float::round::mode::HalfAway;
|
pub use crate::bignum::Big;
|
||||||
use dashu_float::{DBig, FBig};
|
|
||||||
|
|
||||||
/// Arbitrary-precision binary float (base 2, round-half-away). One coordinate.
|
|
||||||
pub type Big = FBig<HalfAway, 2>;
|
|
||||||
|
|
||||||
/// Half-height (complex units) of the default view; also the zoom-1 reference.
|
/// Half-height (complex units) of the default view; also the zoom-1 reference.
|
||||||
pub const DEFAULT_HALF_HEIGHT: f64 = 1.25;
|
pub const DEFAULT_HALF_HEIGHT: f64 = 1.25;
|
||||||
@@ -194,28 +190,37 @@ impl core::fmt::Display for Scale {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
// Out of f64's range: round the exact decimal expansion instead.
|
// Out of f64's range: round the exact decimal expansion instead.
|
||||||
let sig = f.precision().map_or(17, |p| p + 1);
|
let big = self.to_big();
|
||||||
let dec = self
|
let sci = |sig: usize, pad: Option<usize>| {
|
||||||
.to_big()
|
let parts = big.to_decimal_parts(sig);
|
||||||
.to_decimal()
|
let digits = if parts.digits.is_empty() {
|
||||||
.value()
|
"0"
|
||||||
.with_precision(sig)
|
} else {
|
||||||
.value();
|
&parts.digits
|
||||||
let repr = dec.repr();
|
};
|
||||||
let digits = repr.significand().to_string();
|
// value = 0.digits · 10^exp10; move the point after the first digit.
|
||||||
let digits = digits.trim_end_matches('0');
|
let exp10 = parts.exp10 - 1;
|
||||||
let digits = if digits.is_empty() { "0" } else { digits };
|
|
||||||
// value = significand · 10^exponent; move the point after the first digit.
|
|
||||||
let exp10 = repr.exponent() + repr.significand().to_string().len() as isize - 1;
|
|
||||||
let (head, tail) = digits.split_at(1);
|
let (head, tail) = digits.split_at(1);
|
||||||
let tail = match f.precision() {
|
let tail = match pad {
|
||||||
Some(p) => format!("{tail:0<p$}"),
|
Some(p) => format!("{tail:0<p$}"),
|
||||||
None => tail.to_string(),
|
None => tail.to_string(),
|
||||||
};
|
};
|
||||||
if tail.is_empty() {
|
if tail.is_empty() {
|
||||||
write!(f, "{head}e{exp10}")
|
format!("{head}e{exp10}")
|
||||||
} else {
|
} else {
|
||||||
write!(f, "{head}.{tail}e{exp10}")
|
format!("{head}.{tail}e{exp10}")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match f.precision() {
|
||||||
|
Some(p) => f.write_str(&sci(p + 1, Some(p))),
|
||||||
|
None => {
|
||||||
|
// Like `{:e}` on f64: the shortest string that parses back.
|
||||||
|
let s = (1..17)
|
||||||
|
.map(|sig| sci(sig, None))
|
||||||
|
.find(|s| s.parse::<Scale>() == Ok(*self))
|
||||||
|
.unwrap_or_else(|| sci(17, None));
|
||||||
|
f.write_str(&s)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,18 +242,12 @@ impl FromStr for Scale {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
// Too small (or large) for f64: go through an exact decimal.
|
// Too small (or large) for f64: go through an exact decimal.
|
||||||
let dec = DBig::from_str(s).map_err(|_| ())?;
|
let bin = Big::from_decimal_str(s, 64).ok_or(())?;
|
||||||
let bin: Big = dec.with_base_and_precision::<2>(64).value();
|
if bin.is_negative() {
|
||||||
if bin < Big::ZERO {
|
|
||||||
return Err(());
|
return Err(());
|
||||||
}
|
}
|
||||||
let repr = bin.repr();
|
let top = bin.log2_floor().ok_or(())?;
|
||||||
let digits = repr.digits();
|
let m = (bin >> top).to_f64();
|
||||||
if digits == 0 {
|
|
||||||
return Err(());
|
|
||||||
}
|
|
||||||
let top = repr.exponent() + digits as isize - 1;
|
|
||||||
let m = (bin.clone() >> top).to_f64().value();
|
|
||||||
let e = top.clamp(i32::MIN as isize, i32::MAX as isize) as i32;
|
let e = top.clamp(i32::MIN as isize, i32::MAX as isize) as i32;
|
||||||
Ok(Self::from_parts(m, e))
|
Ok(Self::from_parts(m, e))
|
||||||
}
|
}
|
||||||
@@ -312,10 +311,10 @@ impl ViewState {
|
|||||||
pub fn sync_precision(&mut self) {
|
pub fn sync_precision(&mut self) {
|
||||||
let bits = self.precision_bits();
|
let bits = self.precision_bits();
|
||||||
if self.center_re.precision() < bits {
|
if self.center_re.precision() < bits {
|
||||||
self.center_re = self.center_re.clone().with_precision(bits).value();
|
self.center_re = self.center_re.clone().with_precision(bits);
|
||||||
}
|
}
|
||||||
if self.center_im.precision() < bits {
|
if self.center_im.precision() < bits {
|
||||||
self.center_im = self.center_im.clone().with_precision(bits).value();
|
self.center_im = self.center_im.clone().with_precision(bits);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,8 +359,7 @@ impl ViewState {
|
|||||||
/// Parse a decimal string (any number of digits) losslessly into a `Big` with at
|
/// Parse a decimal string (any number of digits) losslessly into a `Big` with at
|
||||||
/// least `bits` of precision. Used for share links and debug view specs.
|
/// least `bits` of precision. Used for share links and debug view specs.
|
||||||
pub fn big_from_decimal_str(s: &str, bits: usize) -> Option<Big> {
|
pub fn big_from_decimal_str(s: &str, bits: usize) -> Option<Big> {
|
||||||
let dec = DBig::from_str(s.trim()).ok()?;
|
Big::from_decimal_str(s, bits)
|
||||||
Some(dec.with_base_and_precision::<2>(bits.max(53)).value())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a "re,im,half_height[,iterations]" spec (re/im decimal, parsed at
|
/// Parse a "re,im,half_height[,iterations]" spec (re/im decimal, parsed at
|
||||||
@@ -440,10 +438,10 @@ pub fn interpolate_view(from: &ViewState, to: &ViewState, t: f64) -> ViewState {
|
|||||||
// Zooming out: g = (1 - q^(t-1)) / (1 - q^-1), every term bounded.
|
// Zooming out: g = (1 - q^(t-1)) / (1 - q^-1), every term bounded.
|
||||||
big_from_f64(((t - 1.0) * d * ln2).exp_m1() / (-d * ln2).exp_m1(), bits)
|
big_from_f64(((t - 1.0) * d * ln2).exp_m1() / (-d * ln2).exp_m1(), bits)
|
||||||
};
|
};
|
||||||
let re0 = from.center_re.clone().with_precision(bits).value();
|
let re0 = from.center_re.clone().with_precision(bits);
|
||||||
let im0 = from.center_im.clone().with_precision(bits).value();
|
let im0 = from.center_im.clone().with_precision(bits);
|
||||||
let re1 = to.center_re.clone().with_precision(bits).value();
|
let re1 = to.center_re.clone().with_precision(bits);
|
||||||
let im1 = to.center_im.clone().with_precision(bits).value();
|
let im1 = to.center_im.clone().with_precision(bits);
|
||||||
let center_re = &re1 + &(&(&re0 - &re1) * &g_big);
|
let center_re = &re1 + &(&(&re0 - &re1) * &g_big);
|
||||||
let center_im = &im1 + &(&(&im0 - &im1) * &g_big);
|
let center_im = &im1 + &(&(&im0 - &im1) * &g_big);
|
||||||
ViewState::with_center(center_re, center_im, half_height)
|
ViewState::with_center(center_re, center_im, half_height)
|
||||||
@@ -456,12 +454,7 @@ pub fn interpolate_f64(from: f64, to: f64, t: f64) -> f64 {
|
|||||||
|
|
||||||
/// Render a `Big` as a decimal string with `sig_digits` significant digits.
|
/// Render a `Big` as a decimal string with `sig_digits` significant digits.
|
||||||
pub fn big_to_decimal_str(x: &Big, sig_digits: usize) -> String {
|
pub fn big_to_decimal_str(x: &Big, sig_digits: usize) -> String {
|
||||||
let dec = x
|
x.to_decimal_string(sig_digits)
|
||||||
.to_decimal()
|
|
||||||
.value()
|
|
||||||
.with_precision(sig_digits.max(1))
|
|
||||||
.value();
|
|
||||||
format!("{dec}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Precision (bits) needed to resolve the center at a given half-height.
|
/// Precision (bits) needed to resolve the center at a given half-height.
|
||||||
@@ -472,12 +465,9 @@ pub fn precision_for(half_height: Scale) -> usize {
|
|||||||
(zoom_bits + GUARD_BITS).clamp(53, MAX_PRECISION_BITS)
|
(zoom_bits + GUARD_BITS).clamp(53, MAX_PRECISION_BITS)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build an `FBig` from an f64 with an explicit precision context.
|
/// Build a `Big` from an f64 with an explicit precision.
|
||||||
pub fn big_from_f64(x: f64, bits: usize) -> Big {
|
pub fn big_from_f64(x: f64, bits: usize) -> Big {
|
||||||
Big::try_from(x)
|
Big::from_f64(x, bits)
|
||||||
.unwrap_or_default()
|
|
||||||
.with_precision(bits)
|
|
||||||
.value()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -489,8 +479,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn re_im_f64(v: &ViewState) -> (f64, f64) {
|
fn re_im_f64(v: &ViewState) -> (f64, f64) {
|
||||||
let re: f64 = v.center_re.to_decimal().value().to_f64().value();
|
let re: f64 = v.center_re.to_f64();
|
||||||
let im: f64 = v.center_im.to_decimal().value().to_f64().value();
|
let im: f64 = v.center_im.to_f64();
|
||||||
(re, im)
|
(re, im)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -612,8 +602,8 @@ mod tests {
|
|||||||
prev = mid.half_height;
|
prev = mid.half_height;
|
||||||
// Offset from the target, in units of the view's half-height.
|
// Offset from the target, in units of the view's half-height.
|
||||||
let k = -mid.half_height.exponent() as isize;
|
let k = -mid.half_height.exponent() as isize;
|
||||||
let dre = ((&mid.center_re - &to.center_re) << k).to_f64().value();
|
let dre = ((&mid.center_re - &to.center_re) << k).to_f64();
|
||||||
let dim = ((&mid.center_im - &to.center_im) << k).to_f64().value();
|
let dim = ((&mid.center_im - &to.center_im) << k).to_f64();
|
||||||
let ratio = (dre * dre + dim * dim).sqrt() / mid.half_height.scaled_f64(k as i32);
|
let ratio = (dre * dre + dim * dim).sqrt() / mid.half_height.scaled_f64(k as i32);
|
||||||
assert!(ratio > 0.01 && ratio < 10.0, "t={t}: ratio {ratio}");
|
assert!(ratio > 0.01 && ratio < 10.0, "t={t}: ratio {ratio}");
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
//! Native background worker for reference-orbit computation.
|
//! Native background worker for reference-orbit computation.
|
||||||
//!
|
//!
|
||||||
//! At deep zoom the high-precision reference can take many milliseconds (tens of
|
//! At deep zoom the high-precision reference can take many milliseconds (tens of
|
||||||
//! thousands of `FBig` iterations), which would stutter the UI if done inline.
|
//! thousands of `Big` iterations), which would stutter the UI if done inline.
|
||||||
//! This runs it on a thread and coalesces bursts of requests (e.g. during a
|
//! This runs it on a thread and coalesces bursts of requests (e.g. during a
|
||||||
//! drag) down to the most recent one. On the web we compute inline instead
|
//! drag) down to the most recent one. On the web we compute inline instead
|
||||||
//! (browsers need a Web Worker for threads); see `app.rs`.
|
//! (browsers need a Web Worker for threads); see `app.rs`.
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
results/
|
||||||
|
.worktrees/
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Benchmarks
|
||||||
|
|
||||||
|
`bench.sh` times `mandelbrot --headless` on a fixed set of views with
|
||||||
|
[hyperfine](https://github.com/sharkdp/hyperfine). That's the whole real
|
||||||
|
pipeline: the reference orbit on the CPU, then the GPU render, readback and PNG
|
||||||
|
encode. It builds the headless-only binary (`--no-default-features`) itself.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
tools/bench/bench.sh --rev HEAD # did my uncommitted edits help? (vs last commit)
|
||||||
|
tools/bench/bench.sh --rev main seahorse-1e300 # one scenario vs another branch
|
||||||
|
tools/bench/bench.sh --quick # all scenarios, 960×540, 3 runs
|
||||||
|
tools/bench/bench.sh # all scenarios, 1920×1080, 5 runs
|
||||||
|
tools/bench/bench.sh --list
|
||||||
|
```
|
||||||
|
|
||||||
|
`--rev REF` checks REF out into `.worktrees/<sha>` and builds it there, with its
|
||||||
|
own target dir, so your `target/` stays warm. Each scenario then runs both
|
||||||
|
binaries back to back in one hyperfine call. The line to read is
|
||||||
|
`working tree ran 1.23 ± 0.04 times faster than main (…)`. A ratio within about
|
||||||
|
2σ of 1.00 is noise.
|
||||||
|
|
||||||
|
Results (hyperfine JSON and markdown per scenario) go to
|
||||||
|
`results/<sha>[-dirty]/`, or `results/<rev>-vs-<sha>/` with `--rev`.
|
||||||
|
Both `results/` and `.worktrees/` are git-ignored. To clean up:
|
||||||
|
`rm -rf tools/bench/.worktrees && git worktree prune`.
|
||||||
|
|
||||||
|
## Scenarios
|
||||||
|
|
||||||
|
| name | mostly measures |
|
||||||
|
|---|---|
|
||||||
|
| `shallow` | GPU, plain f32 path, f64 orbit fast path |
|
||||||
|
| `seahorse-1e30` | GPU f32 perturbation from a `Big` orbit |
|
||||||
|
| `seahorse-1e100` | the `DEEP` shader pipeline |
|
||||||
|
| `elephant-1e100-aa-de` | 2×2 antialiasing + DE shading |
|
||||||
|
| `burning-ship` | a non-holomorphic kind |
|
||||||
|
| `3d` | the raymarched 3D export path |
|
||||||
|
| `anim-zoom` | 24-frame zoom (⅓ size): the parallel orbit, GPU and PNG pipeline |
|
||||||
|
| `seahorse-1e300` *(slow)* | ~270k iterations per pixel on the deep path, ~40 s per run even with `--quick`. It's GPU-bound: the orbit itself is under a second. Only runs when named or with `--all` |
|
||||||
|
|
||||||
|
Views come from `tools/deep-zoom/LOCATIONS.md`. Each wall time includes process
|
||||||
|
start, device creation and shader compilation, so a small change to a GPU-bound
|
||||||
|
scenario shows up diluted.
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
- Keep the machine idle and on AC power. Other GPU apps (browsers, video) add a lot of noise.
|
||||||
|
- Use `--quick` while you iterate and a full run to confirm.
|
||||||
|
- Timings are only comparable on the same machine and GPU.
|
||||||
Executable
+128
@@ -0,0 +1,128 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Benchmark `mandelbrot --headless` on a fixed set of scenarios with hyperfine.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bench.sh [--quick] [--runs N] [--rev REF] [--all] [--list] [scenario...]
|
||||||
|
#
|
||||||
|
# (default) time the working tree; results go to results/<sha>[-dirty]/
|
||||||
|
# --rev REF also build REF (in a git worktree) and time both binaries side
|
||||||
|
# by side; hyperfine prints "X ± σ times faster" per scenario
|
||||||
|
# --quick 960×540 and 3 runs instead of 1920×1080 and 5
|
||||||
|
# --runs N timed runs per command (after 1 warmup run)
|
||||||
|
# --all include the slow scenarios
|
||||||
|
# --list print the scenario names and exit
|
||||||
|
#
|
||||||
|
# With no scenario names, all but the slow ones run. See README.md.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||||
|
root=$(git -C "$here" rev-parse --show-toplevel)
|
||||||
|
|
||||||
|
# Views from tools/deep-zoom/LOCATIONS.md.
|
||||||
|
SEAHORSE_40='-0.77568376800905379745347652613832924487504096622022,0.13646736829469012473375311880735014411233827361594'
|
||||||
|
SEAHORSE_101='-0.77568376800905379746948350393474104572824650461579342765769586746577714101553375163935420167637213972439444483,0.13646736829469012473327440961784876519777211159246218448277041663416833530840062457397927487282583495378053495'
|
||||||
|
SEAHORSE_300='-0.775683768009053797469483503934741045728246504615800394794952001400666017222819589894883159368782255028252296689050972508942991790804637379375064948276511044167168394677127466362472192356953918438738697548775738756167744421444715134058363508346750264411321139485834882343906172015818621107808449401958194265462,0.136467368294690124733274409617848765197772111592467696407800195400835665974815500875448611244178656625706823059759836009902469392551108149827753781145338163746214967880988693412636771323356061132825037923307975359146666033691080243216637006179374407866237575096209504710178751614036006531433101633993532313930'
|
||||||
|
ELEPHANT_100='0.2920738619144539179997175176922057340804018914331633820622644536373359023331882946705674169275530423410817910,0.01577091721139011808246902823033833768260048863232968822435568049949556139801802963232184866659058960379114747'
|
||||||
|
|
||||||
|
# name -> headless args (no size or --export-path; those are added per run).
|
||||||
|
# Order matters for display, hence the separate list. SLOW ones (tens of
|
||||||
|
# seconds per run) only run when named or with --all.
|
||||||
|
SCENARIOS=(shallow seahorse-1e30 seahorse-1e100 elephant-1e100-aa-de burning-ship 3d anim-zoom)
|
||||||
|
SLOW=(seahorse-1e300)
|
||||||
|
declare -A ARGS=(
|
||||||
|
[shallow]="--view=-0.75,0,1.5"
|
||||||
|
[seahorse-1e30]="--view=$SEAHORSE_40,1e-30"
|
||||||
|
[seahorse-1e100]="--view=$SEAHORSE_101,3.4e-101"
|
||||||
|
[seahorse-1e300]="--view=$SEAHORSE_300,5.5e-300"
|
||||||
|
[elephant-1e100-aa-de]="--view=$ELEPHANT_100,1e-75 --antialias --de"
|
||||||
|
[burning-ship]="--kind burning-ship"
|
||||||
|
[3d]="--rendering-kind 3d --de --pitch -40 --view=-0.75,0,1.5"
|
||||||
|
[anim-zoom]="--view=-0.75,0,1.5 --to-view=$SEAHORSE_40,1e-30 --frames 24"
|
||||||
|
)
|
||||||
|
|
||||||
|
quick=0
|
||||||
|
runs=
|
||||||
|
rev=
|
||||||
|
picked=()
|
||||||
|
while (($#)); do
|
||||||
|
case $1 in
|
||||||
|
--quick) quick=1 ;;
|
||||||
|
--runs) runs=$2; shift ;;
|
||||||
|
--rev) rev=$2; shift ;;
|
||||||
|
--all) picked+=("${SCENARIOS[@]}" "${SLOW[@]}") ;;
|
||||||
|
--list) printf '%s\n' "${SCENARIOS[@]}"; printf '%s (slow)\n' "${SLOW[@]}"; exit 0 ;;
|
||||||
|
-h|--help) sed -n '2,16s/^# \{0,1\}//p' "$0"; exit 0 ;;
|
||||||
|
-*) echo "unknown option: $1" >&2; exit 2 ;;
|
||||||
|
*)
|
||||||
|
[[ -v ARGS[$1] ]] || { echo "unknown scenario: $1 (try --list)" >&2; exit 2; }
|
||||||
|
picked+=("$1") ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
((${#picked[@]})) || picked=("${SCENARIOS[@]}")
|
||||||
|
|
||||||
|
if ((quick)); then
|
||||||
|
width=960 height=540 runs=${runs:-3}
|
||||||
|
else
|
||||||
|
width=1920 height=1080 runs=${runs:-5}
|
||||||
|
fi
|
||||||
|
|
||||||
|
command -v hyperfine >/dev/null || { echo "hyperfine not found (https://github.com/sharkdp/hyperfine)" >&2; exit 1; }
|
||||||
|
|
||||||
|
# Headless-only binary: no eframe/egui, faster to build, same render path.
|
||||||
|
build() { cargo build --release --no-default-features --quiet --manifest-path "$1/Cargo.toml"; }
|
||||||
|
|
||||||
|
echo "building working tree…" >&2
|
||||||
|
build "$root"
|
||||||
|
new_bin="$root/target/release/mandelbrot"
|
||||||
|
|
||||||
|
if [[ -n $rev ]]; then
|
||||||
|
rev_sha=$(git -C "$root" rev-parse --short "$rev^{commit}")
|
||||||
|
wt="$here/.worktrees/$rev_sha"
|
||||||
|
[[ -d $wt ]] || git -C "$root" worktree add --detach --quiet "$wt" "$rev_sha"
|
||||||
|
echo "building $rev ($rev_sha)…" >&2
|
||||||
|
# Separate target dir so switching revisions doesn't invalidate target/.
|
||||||
|
CARGO_TARGET_DIR="$here/.worktrees/target" build "$wt"
|
||||||
|
old_bin="$here/.worktrees/target/release/mandelbrot"
|
||||||
|
fi
|
||||||
|
|
||||||
|
tmp=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$tmp"' EXIT
|
||||||
|
|
||||||
|
sha=$(git -C "$root" rev-parse --short HEAD)
|
||||||
|
git -C "$root" diff --quiet HEAD -- . ':!tools/bench' || sha+=-dirty
|
||||||
|
out="$here/results/${rev:+$rev_sha-vs-}$sha"
|
||||||
|
mkdir -p "$out"
|
||||||
|
|
||||||
|
# Full headless command line for scenario $2 with binary $1.
|
||||||
|
command_for() {
|
||||||
|
local bin=$1 name=$2 w=$width h=$height dest=$tmp/$2.png
|
||||||
|
if [[ $name == anim-* ]]; then
|
||||||
|
# Animation: a directory of frames, at a smaller size.
|
||||||
|
w=$((width / 3)) h=$((height / 3)) dest=$tmp/$name
|
||||||
|
fi
|
||||||
|
echo "$bin --headless --width $w --height $h --export-path $dest ${ARGS[$name]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
for name in "${picked[@]}"; do
|
||||||
|
echo >&2
|
||||||
|
echo "=== $name ===" >&2
|
||||||
|
if [[ -n $rev ]]; then
|
||||||
|
cmds=(-n "$rev ($rev_sha)" "$(command_for "$old_bin" "$name")"
|
||||||
|
-n "working tree" "$(command_for "$new_bin" "$name")")
|
||||||
|
else
|
||||||
|
cmds=(-n "$name" "$(command_for "$new_bin" "$name")")
|
||||||
|
fi
|
||||||
|
hyperfine --warmup 1 --runs "$runs" --style basic \
|
||||||
|
--export-json "$out/$name.json" --export-markdown "$out/$name.md" \
|
||||||
|
"${cmds[@]}"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "## Summary (${width}×${height}, $runs runs; results in ${out#"$root"/}/)"
|
||||||
|
for name in "${picked[@]}"; do
|
||||||
|
echo
|
||||||
|
echo "### $name"
|
||||||
|
cat "$out/$name.md"
|
||||||
|
done
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
# Deep-zoom locations
|
||||||
|
|
||||||
|
Beautiful deep Mandelbrot locations, all computed with `find_deep.py` and checked with a
|
||||||
|
`--headless` render. Every one is an exact minibrot nucleus (Newton's method at
|
||||||
|
`2·depth + 60` digits), not a coordinate copied from somewhere, so the digits are right all
|
||||||
|
the way down.
|
||||||
|
|
||||||
|
## How to use them
|
||||||
|
|
||||||
|
Each location has two views on the same centre:
|
||||||
|
|
||||||
|
- **Minibrot:** half-height ≈ 3× the minibrot's size. A tiny copy of the whole set sits in
|
||||||
|
concentric colour bands, and there are more bands the deeper it is.
|
||||||
|
- **Embedded Julia set:** half-height ≈ size^0.75, about ¾ of the way down in log-zoom.
|
||||||
|
This is where each location looks unique. The minibrots all look alike.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run --release -- --view=RE,IM,HALF_HEIGHT # interactive
|
||||||
|
cargo run --release -- --headless --view=RE,IM,HALF_HEIGHT --export-path shot.png
|
||||||
|
# zoom video from the full set down to a minibrot, piped to ffmpeg (see Makefile):
|
||||||
|
cargo run --release -- --headless --view=-0.75,0,1.5 --to-view=RE,IM,HALF_HEIGHT \
|
||||||
|
--fps 60 --duration 60 --export-path - | make hevc-nvenc
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass negative coordinates as `--view=-0.77...` (with the `=`): clap would read `--view -0.77...` as a flag.
|
||||||
|
Iterations scale with depth automatically (about 90k at 10⁻¹⁰⁰ and 270k at 10⁻³⁰⁰), which is
|
||||||
|
enough for these periods.
|
||||||
|
|
||||||
|
## Infinite spirals (Misiurewicz points)
|
||||||
|
|
||||||
|
These are the anchor points. Zoom straight into one and the same spiral repeats forever,
|
||||||
|
at any depth. Every minibrot below sits just beside one of them.
|
||||||
|
|
||||||
|
| Region | Point | Preperiod, period | Looks like |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Seahorse valley | `-0.7756837680090537974694835039347410457282465046158` `0.13646736829469012473327440961784876519777211159247` | 24, 2 | Two-armed spirals of seahorses, repeating at every depth. |
|
||||||
|
| Elephant valley | `0.29207386191445391799971751769220573408040189143315` `0.015770917211390118082469028230338337682600488632339` | 32, 3 | Three-armed spirals of elephant trunks. |
|
||||||
|
| Top antenna | `-0.10110375797210418675706890936004723524958119917843` `0.95629400795235409290719434118824484558046591980423` | 38, 4 | Four-armed branch points on the upper antenna, with thin dendrites instead of spirals. |
|
||||||
|
|
||||||
|
## Seahorse valley
|
||||||
|
|
||||||
|
### Seahorse valley, minibrot at 3.7e-41 (period 1034)
|
||||||
|
|
||||||
|
The embedded Julia set is a pale double spiral: two S-shaped arms of tiny seahorses wind into a bright centre, with a third lace band sweeping around them. The best one for a short video, and it renders in seconds.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# embedded Julia set
|
||||||
|
--view=-0.77568376800905379745347652613832924487504096622022,0.13646736829469012473375311880735014411233827361594,1e-30
|
||||||
|
# minibrot
|
||||||
|
--view=-0.77568376800905379745347652613832924487504096622022,0.13646736829469012473375311880735014411233827361594,1.1e-40
|
||||||
|
```
|
||||||
|
|
||||||
|
### Seahorse valley, minibrot at 1.14e-101 (period 2868)
|
||||||
|
|
||||||
|
At the Julia depth there's a dark blue field with a glowing core and lace threads crossing it.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# embedded Julia set
|
||||||
|
--view=-0.77568376800905379746948350393474104572824650461579342765769586746577714101553375163935420167637213972439444483,0.13646736829469012473327440961784876519777211159246218448277041663416833530840062457397927487282583495378053495,1e-76
|
||||||
|
# minibrot
|
||||||
|
--view=-0.77568376800905379746948350393474104572824650461579342765769586746577714101553375163935420167637213972439444483,0.13646736829469012473327440961784876519777211159246218448277041663416833530840062457397927487282583495378053495,3.4e-101
|
||||||
|
```
|
||||||
|
|
||||||
|
### Seahorse valley, minibrot at 1.37e-200 (period 5895)
|
||||||
|
|
||||||
|
Two filaments cross in an X over a warm brown background, with a small dark spiral where they meet.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# embedded Julia set
|
||||||
|
--view=-0.77568376800905379746948350393474104572824650461580039479495200140066601722281958989488315936878225494657853849784368247991666558116221905228489656259693587754973261632614186930985854124920967267440737850057557,0.13646736829469012473327440961784876519777211159246769640780019540083566597481550087544861124417865661202932094564677695883049019713653069907950852152823160218141353878979871215911794286224832089496659451498185,1e-150
|
||||||
|
# minibrot
|
||||||
|
--view=-0.77568376800905379746948350393474104572824650461580039479495200140066601722281958989488315936878225494657853849784368247991666558116221905228489656259693587754973261632614186930985854124920967267440737850057557,0.13646736829469012473327440961784876519777211159246769640780019540083566597481550087544861124417865661202932094564677695883049019713653069907950852152823160218141353878979871215911794286224832089496659451498185,4.1e-200
|
||||||
|
```
|
||||||
|
|
||||||
|
### Seahorse valley, minibrot at 1.83e-300 (period 8922)
|
||||||
|
|
||||||
|
Faint cream threads, and a spiral wound so tight it reads as a point. Period 8922, so the minibrot needs a long render.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# embedded Julia set
|
||||||
|
--view=-0.775683768009053797469483503934741045728246504615800394794952001400666017222819589894883159368782255028252296689050972508942991790804637379375064948276511044167168394677127466362472192356953918438738697548775738756167744421444715134058363508346750264411321139485834882343906172015818621107808449401958194265462,0.136467368294690124733274409617848765197772111592467696407800195400835665974815500875448611244178656625706823059759836009902469392551108149827753781145338163746214967880988693412636771323356061132825037923307975359146666033691080243216637006179374407866237575096209504710178751614036006531433101633993532313930,1e-225
|
||||||
|
# minibrot
|
||||||
|
--view=-0.775683768009053797469483503934741045728246504615800394794952001400666017222819589894883159368782255028252296689050972508942991790804637379375064948276511044167168394677127466362472192356953918438738697548775738756167744421444715134058363508346750264411321139485834882343906172015818621107808449401958194265462,0.136467368294690124733274409617848765197772111592467696407800195400835665974815500875448611244178656625706823059759836009902469392551108149827753781145338163746214967880988693412636771323356061132825037923307975359146666033691080243216637006179374407866237575096209504710178751614036006531433101633993532313930,5.5e-300
|
||||||
|
```
|
||||||
|
|
||||||
|
## Elephant valley
|
||||||
|
|
||||||
|
### Elephant valley, minibrot at 1.29e-100 (period 782)
|
||||||
|
|
||||||
|
A cream-coloured double S-spiral, like the seahorse one but with fatter, beaded arms.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# embedded Julia set
|
||||||
|
--view=0.2920738619144539179997175176922057340804018914331633820622644536373359023331882946705674169275530423410817910,0.01577091721139011808246902823033833768260048863232968822435568049949556139801802963232184866659058960379114747,1e-75
|
||||||
|
# minibrot
|
||||||
|
--view=0.2920738619144539179997175176922057340804018914331633820622644536373359023331882946705674169275530423410817910,0.01577091721139011808246902823033833768260048863232968822435568049949556139801802963232184866659058960379114747,3.9e-100
|
||||||
|
```
|
||||||
|
|
||||||
|
### Elephant valley, minibrot at 1.43e-199 (period 1585)
|
||||||
|
|
||||||
|
A single huge spiral arm made of blue beads curling across the whole frame.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# embedded Julia set
|
||||||
|
--view=0.2920738619144539179997175176922057340804018914331538397786950546981063169810374784665382516408545756138262706218874924432355556243165250541700374639889797148498919340003273815669351046658761147565052417000965,0.01577091721139011808246902823033833768260048863233880759961760295375606839216565332458201188913912642954367433579737643167232896069795830226759356080847862646789481028238105648099709502522309228500630759855757,1e-149
|
||||||
|
# minibrot
|
||||||
|
--view=0.2920738619144539179997175176922057340804018914331538397786950546981063169810374784665382516408545756138262706218874924432355556243165250541700374639889797148498919340003273815669351046658761147565052417000965,0.01577091721139011808246902823033833768260048863233880759961760295375606839216565332458201188913912642954367433579737643167232896069795830226759356080847862646789481028238105648099709502522309228500630759855757,4.3e-199
|
||||||
|
```
|
||||||
|
|
||||||
|
### Elephant valley, minibrot at 3.82e-300 (period 2395)
|
||||||
|
|
||||||
|
A spiral galaxy: several beaded arms around a pinwheel centre. One of the prettiest frames here.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# embedded Julia set
|
||||||
|
--view=0.292073861914453917999717517692205734080401891433153839778695054698106316981037478466538251640854575589843066619974864800862332115945960921998657430606126553202998390050225413256445451049563878137878839982267258218894836912449161037745627504826537881261626243871678392881757578278459983842286413975991353319739,0.0157709172113901180824690282303383376826004886323388075996176029537560683921656533245820118891391263013273917522825544701901299061542128984468742890308365571942081484001405437827548345359998503228665872823816067983725089244499770271352539008763406229496083105636482420467114597267096082878888045800333807911146,1e-225
|
||||||
|
# minibrot
|
||||||
|
--view=0.292073861914453917999717517692205734080401891433153839778695054698106316981037478466538251640854575589843066619974864800862332115945960921998657430606126553202998390050225413256445451049563878137878839982267258218894836912449161037745627504826537881261626243871678392881757578278459983842286413975991353319739,0.0157709172113901180824690282303383376826004886323388075996176029537560683921656533245820118891391263013273917522825544701901299061542128984468742890308365571942081484001405437827548345359998503228665872823816067983725089244499770271352539008763406229496083105636482420467114597267096082878888045800333807911146,1.1e-299
|
||||||
|
```
|
||||||
|
|
||||||
|
## Top antenna
|
||||||
|
|
||||||
|
### Top antenna, minibrot at 6.11e-101 (period 245)
|
||||||
|
|
||||||
|
A dark branching dendrite like a frost cross, with four-way junctions.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# embedded Julia set
|
||||||
|
--view=-0.10110375797210418675706890936004723524958119917842627217950669362738928440088634862532432662296073501093701563,0.95629400795235409290719434118824484558046591980423293884325556254155313535666457147658005358708674409029391915,1e-75
|
||||||
|
# minibrot
|
||||||
|
--view=-0.10110375797210418675706890936004723524958119917842627217950669362738928440088634862532432662296073501093701563,0.95629400795235409290719434118824484558046591980423293884325556254155313535666457147658005358708674409029391915,1.8e-100
|
||||||
|
```
|
||||||
|
|
||||||
|
### Top antenna, minibrot at 5.28e-200 (period 473)
|
||||||
|
|
||||||
|
A chain of six-armed snowflakes linked by thin filaments.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# embedded Julia set
|
||||||
|
--view=-0.10110375797210418675706890936004723524958119917842839398761868801597328463328677019373309617158827470368429189213012997868613724029746440432852247903869013157556141380437202406412221853553594880182186959159214,0.95629400795235409290719434118824484558046591980423399388630140415938010604675760901574969597486831722998038561988250849596064882005832681021199867736493316924923123916964900533995659228499205141785254396348248,1e-149
|
||||||
|
# minibrot
|
||||||
|
--view=-0.10110375797210418675706890936004723524958119917842839398761868801597328463328677019373309617158827470368429189213012997868613724029746440432852247903869013157556141380437202406412221853553594880182186959159214,0.95629400795235409290719434118824484558046591980423399388630140415938010604675760901574969597486831722998038561988250849596064882005832681021199867736493316924923123916964900533995659228499205141785254396348248,1.6e-199
|
||||||
|
```
|
||||||
|
|
||||||
|
### Top antenna, minibrot at 8.34e-301 (period 705)
|
||||||
|
|
||||||
|
A single twelve-armed star, very symmetric. The antenna minibrots have short periods (245-705), so these are the fastest deep renders.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# embedded Julia set
|
||||||
|
--view=-0.1011037579721041867570689093600472352495811991784283939876186880159732846332867701937330961715882746718394428627914481009391257007785408536971203165750666758077852384490279260184029373211737264310068845597273991782305778295187471341711226626762421910644338164121042775094463557363347706110614218633830535086184,0.9562940079523540929071943411882448455804659198042339938863014041593801060467576090157496959748683171680339080646917869452781885502668216669916185990922330667568398963038228431738928191227884090004548615469303071899797403388188723954061899156655653387543351937028936674153437083087070219108726088551230849901498,1e-225
|
||||||
|
# minibrot
|
||||||
|
--view=-0.1011037579721041867570689093600472352495811991784283939876186880159732846332867701937330961715882746718394428627914481009391257007785408536971203165750666758077852384490279260184029373211737264310068845597273991782305778295187471341711226626762421910644338164121042775094463557363347706110614218633830535086184,0.9562940079523540929071943411882448455804659198042339938863014041593801060467576090157496959748683171680339080646917869452781885502668216669916185990922330667568398963038228431738928191227884090004548615469303071899797403388188723954061899156655653387543351937028936674153437083087070219108726088551230849901498,2.5e-300
|
||||||
|
```
|
||||||
|
|
||||||
|
## Finding more
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./find_deep.py --list # known regions
|
||||||
|
./find_deep.py seahorse 250 # minibrot of size ~1e-500 near the seahorse point
|
||||||
|
./find_deep.py seahorse 100 0.2,0.7 # a different minibrot at the same depth
|
||||||
|
./find_deep.py -0.1011,0.9563,38,4 80 # any Misiurewicz point, given as re,im,k,p
|
||||||
|
```
|
||||||
|
|
||||||
|
A disk of radius 10⁻ᴰ around a Misiurewicz point holds a minibrot of size about 10⁻²ᴰ.
|
||||||
|
Newton sometimes fails to converge (elephant at depth 100 with the default offset did). The
|
||||||
|
script detects this from the size and asks for a different offset. Needs `mpmath`.
|
||||||
Executable
+145
@@ -0,0 +1,145 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Find deep Mandelbrot minibrots near a Misiurewicz point.
|
||||||
|
|
||||||
|
Misiurewicz points (preperiodic c, f^(k+p)(0) = f^k(0)) have self-similar
|
||||||
|
spirals at every depth. A disk of radius r = 10^-D around one contains a
|
||||||
|
minibrot of size ~r^2, found by:
|
||||||
|
|
||||||
|
1. the ball-period method: the first n where the disk's image covers 0
|
||||||
|
gives the period p of a nucleus inside it;
|
||||||
|
2. Newton's method on f^p(0) = 0 at high precision for the nucleus;
|
||||||
|
3. the standard size estimate (Heiland-Allen) for its scale.
|
||||||
|
|
||||||
|
Zooming at the nucleus shows spirals, then an embedded Julia set (around
|
||||||
|
size^0.75), then the minibrot (half-height ~3x size).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
find_deep.py REGION DEPTH [OFFSET_RE,OFFSET_IM]
|
||||||
|
find_deep.py --list
|
||||||
|
|
||||||
|
REGION is a name from REGIONS or "re,im,k,p" for any Misiurewicz point.
|
||||||
|
OFFSET (default 0.6,0.3, in units of r) picks a different minibrot at the
|
||||||
|
same depth. If the printed size is far below 10^-(2*DEPTH), Newton didn't
|
||||||
|
converge: try another offset.
|
||||||
|
|
||||||
|
Needs mpmath (pip install mpmath).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from mpmath import log10, mp, mpc, mpf
|
||||||
|
|
||||||
|
# name: (approximate seed, preperiod k, period p). The exact point is
|
||||||
|
# refined by Newton at the requested precision.
|
||||||
|
REGIONS = {
|
||||||
|
"seahorse": (("-0.77568377", "0.13646737"), 24, 2),
|
||||||
|
"elephant": (("0.2925", "0.0149"), 32, 3),
|
||||||
|
"antenna": (("-0.1011", "0.9563"), 38, 4),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def converged(d):
|
||||||
|
return abs(d) < mpf(10) ** (-mp.dps + 20)
|
||||||
|
|
||||||
|
|
||||||
|
def misiurewicz(c, k, p, steps=200):
|
||||||
|
"""Newton on f^(k+p)(0) - f^k(0) = 0."""
|
||||||
|
for _ in range(steps):
|
||||||
|
z = dz = mpc(0)
|
||||||
|
zk = dzk = None
|
||||||
|
for i in range(1, k + p + 1):
|
||||||
|
dz = 2 * z * dz + 1
|
||||||
|
z = z * z + c
|
||||||
|
if i == k:
|
||||||
|
zk, dzk = z, dz
|
||||||
|
d = (z - zk) / (dz - dzk)
|
||||||
|
c -= d
|
||||||
|
if converged(d):
|
||||||
|
break
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
def period_in_ball(c, r, maxit=1_000_000):
|
||||||
|
"""First n where the disk of radius r around c maps onto a disk containing 0."""
|
||||||
|
z = dz = mpc(0)
|
||||||
|
for n in range(1, maxit):
|
||||||
|
dz = 2 * z * dz + 1
|
||||||
|
z = z * z + c
|
||||||
|
if abs(z) < abs(dz) * r:
|
||||||
|
return n
|
||||||
|
if abs(z) > 4:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def nucleus(c, p, steps=200):
|
||||||
|
"""Newton on f^p(0) = 0."""
|
||||||
|
for _ in range(steps):
|
||||||
|
z = dz = mpc(0)
|
||||||
|
for _ in range(p):
|
||||||
|
dz = 2 * z * dz + 1
|
||||||
|
z = z * z + c
|
||||||
|
d = z / dz
|
||||||
|
c -= d
|
||||||
|
if converged(d):
|
||||||
|
break
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
def size(c, p):
|
||||||
|
"""Approximate size of the minibrot with nucleus c and period p."""
|
||||||
|
z = mpc(0)
|
||||||
|
l = b = mpc(1)
|
||||||
|
for _ in range(1, p):
|
||||||
|
z = z * z + c
|
||||||
|
l = 2 * z * l
|
||||||
|
b = b + 1 / l
|
||||||
|
return abs(1 / (b * l * l))
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
if len(argv) >= 1 and argv[0] == "--list":
|
||||||
|
for name, ((re, im), k, p) in REGIONS.items():
|
||||||
|
print(f"{name:10} ~{re}{'+' if not im.startswith('-') else ''}{im}i M({k},{p})")
|
||||||
|
return 0
|
||||||
|
if len(argv) not in (2, 3):
|
||||||
|
print(__doc__, file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
region, depth = argv[0], int(argv[1])
|
||||||
|
if region in REGIONS:
|
||||||
|
(re, im), k, p = REGIONS[region]
|
||||||
|
else:
|
||||||
|
re, im, k, p = region.split(",")
|
||||||
|
k, p = int(k), int(p)
|
||||||
|
off_re, off_im = argv[2].split(",") if len(argv) == 3 else ("0.6", "0.3")
|
||||||
|
|
||||||
|
mp.dps = 2 * depth + 60
|
||||||
|
m = misiurewicz(mpc(re, im), k, p)
|
||||||
|
r = mpf(10) ** (-depth)
|
||||||
|
c0 = m + r * mpc(off_re, off_im)
|
||||||
|
period = period_in_ball(c0, r)
|
||||||
|
if period is None:
|
||||||
|
print("no period found (the disk escapes)", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
n = nucleus(c0, period)
|
||||||
|
s = size(n, period)
|
||||||
|
if s < mpf(10) ** (-2 * depth - 10):
|
||||||
|
print(f"size {mp.nstr(s, 3)} is implausibly small: Newton didn't converge, "
|
||||||
|
"try another offset", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
digits = int(-log10(s)) + 10
|
||||||
|
log_s = float(log10(s))
|
||||||
|
re_s = mp.nstr(n.real, digits, strip_zeros=False)
|
||||||
|
im_s = mp.nstr(n.imag, digits, strip_zeros=False)
|
||||||
|
print(f"# {region} depth={depth} period={period} size={mp.nstr(s, 3)}")
|
||||||
|
print("# minibrot:")
|
||||||
|
print(f"--view={re_s},{im_s},{mp.nstr(3 * s, 2)}")
|
||||||
|
print(f"# embedded Julia set on the way down:")
|
||||||
|
print(f"--view={re_s},{im_s},1e{round(0.75 * log_s)}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main(sys.argv[1:]))
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
clips/
|
||||||
|
work/
|
||||||
|
results/
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Pass 1: which presets fit the 3-5 fps budget at 4K on this machine.
|
||||||
|
x265-fast | -pix_fmt yuv420p10le -c:v libx265 -profile:v main10 -preset fast -crf 18 -x265-params log-level=error:aq-mode=3:no-sao=1
|
||||||
|
x265-medium | -pix_fmt yuv420p10le -c:v libx265 -profile:v main10 -preset medium -crf 18 -x265-params log-level=error:aq-mode=3:no-sao=1
|
||||||
|
x265-slow | -pix_fmt yuv420p10le -c:v libx265 -profile:v main10 -preset slow -crf 18 -x265-params log-level=error:aq-mode=3:no-sao=1
|
||||||
|
av1-p5 | -pix_fmt yuv420p10le -c:v libsvtav1 -preset 5 -crf 26 -svtav1-params film-grain=0
|
||||||
|
av1-p6 | -pix_fmt yuv420p10le -c:v libsvtav1 -preset 6 -crf 26 -svtav1-params film-grain=0
|
||||||
|
av1-p7 | -pix_fmt yuv420p10le -c:v libsvtav1 -preset 7 -crf 26 -svtav1-params film-grain=0
|
||||||
|
av1-p8 | -pix_fmt yuv420p10le -c:v libsvtav1 -preset 8 -crf 26 -svtav1-params film-grain=0
|
||||||
Executable
+66
@@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Render the lossless 4K60 test clips that sweep.sh encodes.
|
||||||
|
#
|
||||||
|
# Each clip goes through the same RGBA -> bt709 tv-range 10-bit conversion as
|
||||||
|
# the Makefile, then into lossless FFV1. The clips are therefore exactly what the
|
||||||
|
# encoders see, so VMAF against them measures the encoder alone.
|
||||||
|
#
|
||||||
|
# tools/encode/make_clips.sh [--size WxH] [--seconds S] [--force] [clip...]
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
here=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
root=$(cd "$here/../.." && pwd)
|
||||||
|
out="$here/clips"
|
||||||
|
size=3840x2160
|
||||||
|
seconds=4
|
||||||
|
fps=60
|
||||||
|
force=0
|
||||||
|
only=()
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
--size) size=$2; shift 2 ;;
|
||||||
|
--seconds) seconds=$2; shift 2 ;;
|
||||||
|
--force) force=1; shift ;;
|
||||||
|
-h|--help) sed -n '2,9p' "$0"; exit 0 ;;
|
||||||
|
*) only+=("$1"); shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Seahorse valley, minibrot at 3.7e-41 (tools/deep-zoom/LOCATIONS.md).
|
||||||
|
SH_RE=-0.77568376800905379745347652613832924487504096622022
|
||||||
|
SH_IM=0.13646736829469012473375311880735014411233827361594
|
||||||
|
|
||||||
|
# name | mandelbrot flags. Zoom speeds bracket a typical 60 s dive (~0.7 decade/s).
|
||||||
|
clips=(
|
||||||
|
# Worst case: dense filaments, 1.5 decades/s of scaling motion.
|
||||||
|
"filaments|--view=$SH_RE,$SH_IM,1e-8 --to-view=$SH_RE,$SH_IM,3e-15 --linear"
|
||||||
|
# Smooth concentric bands closing in on the minibrot: banding/blocking risk.
|
||||||
|
"bands|--view=$SH_RE,$SH_IM,1e-38 --to-view=$SH_RE,$SH_IM,1.1e-40 --linear"
|
||||||
|
# Embedded Julia set, slow drift: fine static texture, psy/detail retention.
|
||||||
|
"julia|--view=$SH_RE,$SH_IM,2e-30 --to-view=$SH_RE,$SH_IM,1e-30 --linear"
|
||||||
|
)
|
||||||
|
|
||||||
|
[[ -x $root/target/release/mandelbrot ]] || cargo build --release --manifest-path "$root/Cargo.toml"
|
||||||
|
mkdir -p "$out"
|
||||||
|
w=${size%x*}
|
||||||
|
h=${size#*x}
|
||||||
|
|
||||||
|
for entry in "${clips[@]}"; do
|
||||||
|
name=${entry%%|*}
|
||||||
|
flags=${entry#*|}
|
||||||
|
if [[ ${#only[@]} -gt 0 && ! " ${only[*]} " =~ " $name " ]]; then continue; fi
|
||||||
|
dst="$out/$name.mkv"
|
||||||
|
if [[ -f $dst && $force = 0 ]]; then echo "skip $name (exists, --force to redo)"; continue; fi
|
||||||
|
echo "render $name ($size, ${seconds}s @ ${fps}fps)"
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
"$root/target/release/mandelbrot" --headless --antialias $flags \
|
||||||
|
--width "$w" --height "$h" --fps "$fps" --duration "$seconds" --export-path - |
|
||||||
|
ffmpeg -hide_banner -loglevel error -y \
|
||||||
|
-f rawvideo -pix_fmt rgba -s "$size" -framerate "$fps" -i - \
|
||||||
|
-vf "scale=out_color_matrix=bt709:out_range=tv:flags=accurate_rnd+full_chroma_int+bitexact,format=yuv420p10le" \
|
||||||
|
-c:v ffv1 -level 3 -slices 16 -g 1 \
|
||||||
|
-colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv \
|
||||||
|
"$dst.tmp.mkv"
|
||||||
|
mv "$dst.tmp.mkv" "$dst"
|
||||||
|
done
|
||||||
Executable
+87
@@ -0,0 +1,87 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Encode every test clip with every config, then score each encode.
|
||||||
|
#
|
||||||
|
# tools/encode/sweep.sh [--clips a,b] [--keep] [--tag NAME] CONFIG_FILE...
|
||||||
|
#
|
||||||
|
# A config line is `name | ffmpeg output args` (blank lines and # comments are
|
||||||
|
# skipped). The args go between `-i clip.mkv` and the output file, e.g.
|
||||||
|
# x265-crf18 | -c:v libx265 -preset medium -crf 18 -x265-params aq-mode=3
|
||||||
|
# Each row of results/<tag>.tsv has encode fps, bitrate, VMAF 4K (mean, 1st
|
||||||
|
# percentile, min), VMAF-NEG mean, CAMBI banding (mean, max) and PSNR-Y.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
here=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
clipdir="$here/clips"
|
||||||
|
model=/usr/share/model
|
||||||
|
export SVT_LOG=1 # errors only
|
||||||
|
clips=""
|
||||||
|
keep=0
|
||||||
|
tag=$(date +%Y%m%d-%H%M%S)
|
||||||
|
configs=()
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
--clips) clips=$2; shift 2 ;;
|
||||||
|
--keep) keep=1; shift ;;
|
||||||
|
--tag) tag=$2; shift 2 ;;
|
||||||
|
-h|--help) sed -n '2,11p' "$0"; exit 0 ;;
|
||||||
|
*) configs+=("$1"); shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[[ ${#configs[@]} -gt 0 ]] || { echo "usage: $0 [--clips a,b] [--keep] [--tag NAME] CONFIG_FILE..." >&2; exit 1; }
|
||||||
|
|
||||||
|
if [[ -z $clips ]]; then
|
||||||
|
clips=$(cd "$clipdir" && ls ./*.mkv | sed 's|^\./||; s|\.mkv$||' | paste -sd,)
|
||||||
|
fi
|
||||||
|
[[ -n $clips ]] || { echo "no clips in $clipdir: run make_clips.sh first" >&2; exit 1; }
|
||||||
|
|
||||||
|
mkdir -p "$here/results" "$here/work"
|
||||||
|
tsv="$here/results/$tag.tsv"
|
||||||
|
[[ -f $tsv ]] || printf 'clip\tconfig\tfps\tmbps\tvmaf\tvmaf_p1\tvmaf_min\tvmaf_neg\tcambi\tcambi_max\tpsnr_y\n' > "$tsv"
|
||||||
|
|
||||||
|
IFS=, read -ra clip_list <<< "$clips"
|
||||||
|
for cfg in "${configs[@]}"; do
|
||||||
|
while IFS= read -r line || [[ -n $line ]]; do
|
||||||
|
[[ $line =~ ^[[:space:]]*(#|$) ]] && continue
|
||||||
|
name=$(sed 's/[[:space:]]*|.*//' <<< "$line")
|
||||||
|
args=$(sed 's/^[^|]*|[[:space:]]*//' <<< "$line")
|
||||||
|
for clip in "${clip_list[@]}"; do
|
||||||
|
ref="$clipdir/$clip.mkv"
|
||||||
|
enc="$here/work/$clip--$name.mp4"
|
||||||
|
log="$here/work/$clip--$name.json"
|
||||||
|
frames=$(ffprobe -v error -count_packets -select_streams v:0 \
|
||||||
|
-show_entries stream=nb_read_packets -of csv=p=0 "$ref")
|
||||||
|
printf '%-10s %-40s ' "$clip" "$name"
|
||||||
|
|
||||||
|
t0=$(date +%s.%N)
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
ffmpeg -hide_banner -loglevel error -y -i "$ref" $args -an "$enc" < /dev/null
|
||||||
|
t1=$(date +%s.%N)
|
||||||
|
|
||||||
|
# 4K model for both VMAF variants; CAMBI flags banding, which VMAF
|
||||||
|
# barely sees and which is the main risk on smooth fractal gradients.
|
||||||
|
ffmpeg -hide_banner -loglevel error -i "$enc" -i "$ref" -lavfi \
|
||||||
|
"[0:v]setpts=PTS-STARTPTS[d];[1:v]setpts=PTS-STARTPTS[r];[d][r]libvmaf=log_fmt=json:log_path=$log:n_threads=$(nproc):n_subsample=2:model='path=$model/vmaf_4k_v0.6.1.json\\:name=vmaf|path=$model/vmaf_4k_v0.6.1neg.json\\:name=neg':feature='name=cambi|name=psnr'" \
|
||||||
|
-f null - < /dev/null
|
||||||
|
|
||||||
|
row=$(python3 - "$log" "$enc" "$frames" "$t0" "$t1" <<'PY'
|
||||||
|
import json, os, sys
|
||||||
|
log, enc, frames, t0, t1 = sys.argv[1], sys.argv[2], int(sys.argv[3]), float(sys.argv[4]), float(sys.argv[5])
|
||||||
|
fr = json.load(open(log))["frames"]
|
||||||
|
col = lambda k: sorted(f["metrics"][k] for f in fr)
|
||||||
|
v = col("vmaf")
|
||||||
|
p1 = v[max(0, int(0.01 * len(v)) - 1)] if len(v) >= 100 else v[0]
|
||||||
|
mean = lambda xs: sum(xs) / len(xs)
|
||||||
|
cambi = col("cambi")
|
||||||
|
fps = frames / (t1 - t0)
|
||||||
|
mbps = os.path.getsize(enc) * 8 / (frames / 60) / 1e6
|
||||||
|
print(f"{fps:.2f}\t{mbps:.1f}\t{mean(v):.2f}\t{p1:.2f}\t{v[0]:.2f}\t{mean(col('neg')):.2f}\t{mean(cambi):.2f}\t{cambi[-1]:.2f}\t{mean(col('psnr_y')):.2f}")
|
||||||
|
PY
|
||||||
|
)
|
||||||
|
printf '%s\t%s\t%s\n' "$clip" "$name" "$row" >> "$tsv"
|
||||||
|
awk -F'\t' '{printf "%6s fps %7s Mb/s vmaf %s p1 %s min %s neg %s cambi %s/%s psnr %s\n",$1,$2,$3,$4,$5,$6,$7,$8,$9}' <<< "$row"
|
||||||
|
[[ $keep = 1 ]] || rm -f "$enc"
|
||||||
|
done
|
||||||
|
done < "$cfg"
|
||||||
|
done
|
||||||
|
echo "results: $tsv"
|
||||||
Reference in New Issue
Block a user