perf: switch from dashu to rug & malachite
This commit is contained in:
@@ -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,6 +23,7 @@ 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"
|
||||||
@@ -33,7 +35,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
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -105,8 +107,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
|
||||||
@@ -131,10 +144,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×
|
||||||
@@ -190,7 +203,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
|
||||||
|
|||||||
+7
-2
@@ -7,10 +7,12 @@ edition = "2024"
|
|||||||
default = ["gui"]
|
default = ["gui"]
|
||||||
# Windowed egui app. Without it only `--headless` rendering is built.
|
# Windowed egui app. Without it only `--headless` rendering is built.
|
||||||
gui = ["dep:eframe", "dep:egui"]
|
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"], optional = true }
|
||||||
egui = { version = "0.36.2", optional = true }
|
egui = { version = "0.36.2", optional = true }
|
||||||
ecolor = { version = "0.36.2", features = ["bytemuck"] }
|
ecolor = { version = "0.36.2", features = ["bytemuck"] }
|
||||||
@@ -18,11 +20,14 @@ 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"] }
|
||||||
@@ -39,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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
+6
-14
@@ -1144,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())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1198,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1494,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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+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,
|
||||||
|
|||||||
@@ -11,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;
|
||||||
|
|||||||
+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`.
|
||||||
|
|||||||
Reference in New Issue
Block a user