Compare commits
1
Commits
9555353404
...
BLA
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea2a23999d |
+67
-4
@@ -5,7 +5,7 @@ use eframe::egui_wgpu;
|
|||||||
use eframe::egui_wgpu::wgpu;
|
use eframe::egui_wgpu::wgpu;
|
||||||
|
|
||||||
use crate::fractal::{
|
use crate::fractal::{
|
||||||
ExportRender, FractalCallback, FractalKind, FractalRenderer, MAX_REF_POINTS, ShareState,
|
Bla, ExportRender, FractalCallback, FractalKind, FractalRenderer, MAX_REF_POINTS, ShareState,
|
||||||
Uniforms,
|
Uniforms,
|
||||||
};
|
};
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
@@ -139,6 +139,11 @@ pub struct FractalApp {
|
|||||||
|
|
||||||
/// Reference orbit (`Z_n` as f32 pairs) for the current view.
|
/// Reference orbit (`Z_n` as f32 pairs) for the current view.
|
||||||
reference: Arc<Vec<[f32; 2]>>,
|
reference: Arc<Vec<[f32; 2]>>,
|
||||||
|
/// BLA iteration-skip table for `reference` (empty unless BLA is on and the
|
||||||
|
/// kind supports it). Uploaded to the GPU alongside the orbit.
|
||||||
|
bla: Arc<Vec<Bla>>,
|
||||||
|
/// Enable BLA iteration-skipping (square map only). Big speedup at deep zoom.
|
||||||
|
use_bla: bool,
|
||||||
/// Bumped whenever `reference` is replaced, so the GPU re-uploads it.
|
/// Bumped whenever `reference` is replaced, so the GPU re-uploads it.
|
||||||
generation: u64,
|
generation: u64,
|
||||||
/// Center + zoom the current `reference` was computed at (may differ
|
/// Center + zoom the current `reference` was computed at (may differ
|
||||||
@@ -236,6 +241,8 @@ impl FractalApp {
|
|||||||
antialias: false,
|
antialias: false,
|
||||||
de_coloring: false,
|
de_coloring: false,
|
||||||
reference: Arc::new(Vec::new()),
|
reference: Arc::new(Vec::new()),
|
||||||
|
bla: Arc::new(Vec::new()),
|
||||||
|
use_bla: true,
|
||||||
generation: 0,
|
generation: 0,
|
||||||
ref_center_re,
|
ref_center_re,
|
||||||
ref_center_im,
|
ref_center_im,
|
||||||
@@ -300,6 +307,9 @@ impl FractalApp {
|
|||||||
if let Ok(spec) = std::env::var("MANDEL_VIEW") {
|
if let Ok(spec) = std::env::var("MANDEL_VIEW") {
|
||||||
app.apply_view_spec(&spec);
|
app.apply_view_spec(&spec);
|
||||||
}
|
}
|
||||||
|
if std::env::var("MANDEL_NO_BLA").is_ok() {
|
||||||
|
app.use_bla = false; // for BLA-on vs BLA-off comparison
|
||||||
|
}
|
||||||
if std::env::var("MANDEL_DE").is_ok() {
|
if std::env::var("MANDEL_DE").is_ok() {
|
||||||
app.de_coloring = true;
|
app.de_coloring = true;
|
||||||
}
|
}
|
||||||
@@ -491,14 +501,34 @@ impl FractalApp {
|
|||||||
[dre, dim]
|
[dre, dim]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_reference(&mut self, points: Vec<[f32; 2]>, cre: Big, cim: Big, hh: f64) {
|
fn apply_reference(
|
||||||
|
&mut self,
|
||||||
|
points: Vec<[f32; 2]>,
|
||||||
|
bla: Vec<Bla>,
|
||||||
|
cre: Big,
|
||||||
|
cim: Big,
|
||||||
|
hh: f64,
|
||||||
|
) {
|
||||||
self.reference = Arc::new(points);
|
self.reference = Arc::new(points);
|
||||||
|
self.bla = Arc::new(bla);
|
||||||
self.ref_center_re = cre;
|
self.ref_center_re = cre;
|
||||||
self.ref_center_im = cim;
|
self.ref_center_im = cim;
|
||||||
self.ref_half_height = hh;
|
self.ref_half_height = hh;
|
||||||
self.generation = self.generation.wrapping_add(1);
|
self.generation = self.generation.wrapping_add(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Upper bound on any pixel's `|dc|` for the current view (center→corner
|
||||||
|
/// distance), with margin so a reference reused after slight drift/zoom stays
|
||||||
|
/// valid. Used to size BLA merge radii.
|
||||||
|
fn bla_dc_max(&self, half_height: f64) -> f64 {
|
||||||
|
let aspect = if self.last_size_px.y > 0.0 {
|
||||||
|
(self.last_size_px.x / self.last_size_px.y) as f64
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
half_height * (1.0 + aspect * aspect).sqrt() * 2.0
|
||||||
|
}
|
||||||
|
|
||||||
/// Recompute the reference orbit when needed. Native: dispatch to a worker
|
/// Recompute the reference orbit when needed. Native: dispatch to a worker
|
||||||
/// thread and pick up completed results. Web: compute inline.
|
/// thread and pick up completed results. Web: compute inline.
|
||||||
fn ensure_reference(&mut self) {
|
fn ensure_reference(&mut self) {
|
||||||
@@ -506,6 +536,9 @@ impl FractalApp {
|
|||||||
let key = self.current_key();
|
let key = self.current_key();
|
||||||
let precision = self.view.precision_bits();
|
let precision = self.view.precision_bits();
|
||||||
let max_iter = key.iter.min(MAX_REF_POINTS as u32 - 1);
|
let max_iter = key.iter.min(MAX_REF_POINTS as u32 - 1);
|
||||||
|
// BLA applies to the holomorphic square map (Mandelbrot set + Julia).
|
||||||
|
let want_bla = self.use_bla && key.kind == FractalKind::Mandelbrot;
|
||||||
|
let dc_max = self.bla_dc_max(key.half_height);
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
{
|
{
|
||||||
@@ -519,6 +552,8 @@ impl FractalApp {
|
|||||||
precision,
|
precision,
|
||||||
kind: key.kind,
|
kind: key.kind,
|
||||||
power: key.power,
|
power: key.power,
|
||||||
|
build_bla: want_bla,
|
||||||
|
dc_max,
|
||||||
});
|
});
|
||||||
self.pending = true;
|
self.pending = true;
|
||||||
}
|
}
|
||||||
@@ -547,8 +582,14 @@ impl FractalApp {
|
|||||||
key.power,
|
key.power,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
let bla = if want_bla {
|
||||||
|
crate::fractal::build_bla_table(&points, dc_max)
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
self.apply_reference(
|
self.apply_reference(
|
||||||
points,
|
points,
|
||||||
|
bla,
|
||||||
key.center_re.clone(),
|
key.center_re.clone(),
|
||||||
key.center_im.clone(),
|
key.center_im.clone(),
|
||||||
key.half_height,
|
key.half_height,
|
||||||
@@ -560,7 +601,13 @@ impl FractalApp {
|
|||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
if let Some(res) = self.worker.try_take_latest() {
|
if let Some(res) = self.worker.try_take_latest() {
|
||||||
self.apply_reference(res.points, res.center_re, res.center_im, res.half_height);
|
self.apply_reference(
|
||||||
|
res.points,
|
||||||
|
res.bla,
|
||||||
|
res.center_re,
|
||||||
|
res.center_im,
|
||||||
|
res.half_height,
|
||||||
|
);
|
||||||
self.pending = false;
|
self.pending = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -581,7 +628,11 @@ impl FractalApp {
|
|||||||
power: self.power,
|
power: self.power,
|
||||||
dc_offset: self.dc_offset(),
|
dc_offset: self.dc_offset(),
|
||||||
de_coloring: self.de_coloring as u32,
|
de_coloring: self.de_coloring as u32,
|
||||||
_pad: 0,
|
// Only when a table for the current reference is actually present, so
|
||||||
|
// a stale/empty buffer is never traversed during a kind switch.
|
||||||
|
use_bla: (self.use_bla
|
||||||
|
&& self.kind == FractalKind::Mandelbrot
|
||||||
|
&& !self.bla.is_empty()) as u32,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -618,6 +669,7 @@ impl FractalApp {
|
|||||||
renderer.export_handles()
|
renderer.export_handles()
|
||||||
};
|
};
|
||||||
let reference = Arc::clone(&self.reference);
|
let reference = Arc::clone(&self.reference);
|
||||||
|
let bla = Arc::clone(&self.bla);
|
||||||
|
|
||||||
let shared = Arc::new(Mutex::new(ExportShared {
|
let shared = Arc::new(Mutex::new(ExportShared {
|
||||||
fraction: 0.0,
|
fraction: 0.0,
|
||||||
@@ -645,6 +697,7 @@ impl FractalApp {
|
|||||||
h,
|
h,
|
||||||
uniforms,
|
uniforms,
|
||||||
reference.as_slice(),
|
reference.as_slice(),
|
||||||
|
bla.as_slice(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Render the image tile by tile, waiting for each so progress
|
// Render the image tile by tile, waiting for each so progress
|
||||||
@@ -710,6 +763,7 @@ impl FractalApp {
|
|||||||
h,
|
h,
|
||||||
uniforms,
|
uniforms,
|
||||||
reference.as_slice(),
|
reference.as_slice(),
|
||||||
|
bla.as_slice(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Render tile by tile, awaiting each submission so the browser
|
// Render tile by tile, awaiting each submission so the browser
|
||||||
@@ -871,6 +925,14 @@ impl FractalApp {
|
|||||||
for crisp filaments at deep zoom. Exact for Mandelbrot/Multibrot, \
|
for crisp filaments at deep zoom. Exact for Mandelbrot/Multibrot, \
|
||||||
approximate for Burning Ship/Tricorn.",
|
approximate for Burning Ship/Tricorn.",
|
||||||
);
|
);
|
||||||
|
ui.add_enabled(
|
||||||
|
self.kind == FractalKind::Mandelbrot,
|
||||||
|
egui::Checkbox::new(&mut self.use_bla, "BLA skipping"),
|
||||||
|
)
|
||||||
|
.on_hover_text(
|
||||||
|
"Bivariate Linear Approximation: skip runs of iterations at deep zoom \
|
||||||
|
for a large speedup. Mandelbrot/Julia only.",
|
||||||
|
);
|
||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
// Editable center coordinates. Shown at full precision; parsed
|
// Editable center coordinates. Shown at full precision; parsed
|
||||||
@@ -1104,6 +1166,7 @@ impl FractalApp {
|
|||||||
FractalCallback {
|
FractalCallback {
|
||||||
uniforms,
|
uniforms,
|
||||||
reference: Arc::clone(&self.reference),
|
reference: Arc::clone(&self.reference),
|
||||||
|
bla: Arc::clone(&self.bla),
|
||||||
generation: self.generation,
|
generation: self.generation,
|
||||||
size_px,
|
size_px,
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ pub mod reference;
|
|||||||
pub mod renderer;
|
pub mod renderer;
|
||||||
pub mod share;
|
pub mod share;
|
||||||
|
|
||||||
pub use reference::{FractalKind, compute_reference, compute_set_reference};
|
pub use reference::{Bla, FractalKind, build_bla_table, compute_reference, compute_set_reference};
|
||||||
pub use renderer::{
|
pub use renderer::{
|
||||||
ExportRender, FractalCallback, FractalRenderer, MAX_REF_POINTS, Uniforms,
|
ExportRender, FractalCallback, FractalRenderer, MAX_REF_POINTS, Uniforms,
|
||||||
encode_png_with_progress,
|
encode_png_with_progress,
|
||||||
|
|||||||
@@ -146,6 +146,150 @@ pub fn compute_set_reference(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Bivariate Linear Approximation (BLA)
|
||||||
|
//
|
||||||
|
// Deep in a zoom the per-pixel delta stays far smaller than the reference, so
|
||||||
|
// the nonlinear `e^2` term of the perturbation step is negligible and the step
|
||||||
|
// is effectively linear: `e -> A e + B dc`. BLA precomputes, for runs of
|
||||||
|
// iterations, the composed linear coefficients `(A, B)` plus a validity radius
|
||||||
|
// `r` (the largest `|e|` for which dropping `e^2` stays within tolerance). A
|
||||||
|
// pixel can then skip a whole run in one multiply whenever `|e| < r`.
|
||||||
|
//
|
||||||
|
// Runs are merged pairwise into a binary tree of levels: level `k` holds BLAs of
|
||||||
|
// length `2^k` starting at multiples of `2^k`. The GPU walks levels high→low to
|
||||||
|
// take the longest valid skip at the current index. Both sides recompute the
|
||||||
|
// per-level counts from `ref_len` (count[0] = ref_len-1, count[k] = count[k-1]/2)
|
||||||
|
// so no offset table needs to travel to the GPU — only this flat array does.
|
||||||
|
//
|
||||||
|
// Only the holomorphic square map (Mandelbrot/Julia, `A = 2 Z`, `B = 1`) is
|
||||||
|
// supported; other kinds fall back to per-iteration stepping on the GPU.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// One merged linear step: `e_{n+l} = A e_n + B dc`, valid while `|e_n| < r`.
|
||||||
|
/// Laid out to match the WGSL `Bla` struct (two `vec2<f32>`, then `f32`, `u32`;
|
||||||
|
/// 24-byte std430 stride).
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
|
pub struct Bla {
|
||||||
|
pub a: [f32; 2],
|
||||||
|
pub b: [f32; 2],
|
||||||
|
pub r: f32,
|
||||||
|
pub l: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Relative budget for the dropped nonlinear term, chosen near the f32 orbit
|
||||||
|
/// storage noise floor so BLA adds no visible error over plain perturbation.
|
||||||
|
const BLA_EPS: f64 = 1.0e-6;
|
||||||
|
|
||||||
|
/// f64 working form of a BLA (merges accumulate in f64, stored as f32).
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct BlaF {
|
||||||
|
ar: f64,
|
||||||
|
ai: f64,
|
||||||
|
br: f64,
|
||||||
|
bi: f64,
|
||||||
|
r: f64,
|
||||||
|
l: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BlaF {
|
||||||
|
fn to_bla(self) -> Bla {
|
||||||
|
Bla {
|
||||||
|
a: [self.ar as f32, self.ai as f32],
|
||||||
|
b: [self.br as f32, self.bi as f32],
|
||||||
|
r: self.r as f32,
|
||||||
|
l: self.l,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merge two consecutive BLAs (`x` then `y`) into one covering both runs.
|
||||||
|
fn merge_bla(x: BlaF, y: BlaF, dc_max: f64) -> BlaF {
|
||||||
|
// A = Ay Ax ; B = Ay Bx + By (complex).
|
||||||
|
let ar = y.ar * x.ar - y.ai * x.ai;
|
||||||
|
let ai = y.ar * x.ai + y.ai * x.ar;
|
||||||
|
let br = y.ar * x.br - y.ai * x.bi + y.br;
|
||||||
|
let bi = y.ar * x.bi + y.ai * x.br + y.bi;
|
||||||
|
// Valid if |e| < rx (so x holds) and |Ax e + Bx dc| < ry (so y holds):
|
||||||
|
// |e| < (ry - |Bx| dc_max) / |Ax|.
|
||||||
|
let ax_mag = (x.ar * x.ar + x.ai * x.ai).sqrt();
|
||||||
|
let bx_mag = (x.br * x.br + x.bi * x.bi).sqrt();
|
||||||
|
let r_y = if ax_mag > 0.0 {
|
||||||
|
((y.r - bx_mag * dc_max) / ax_mag).max(0.0)
|
||||||
|
} else {
|
||||||
|
x.r
|
||||||
|
};
|
||||||
|
BlaF {
|
||||||
|
ar,
|
||||||
|
ai,
|
||||||
|
br,
|
||||||
|
bi,
|
||||||
|
r: x.r.min(r_y),
|
||||||
|
l: x.l + y.l,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the BLA table for a square-map reference orbit. `dc_max` is an upper
|
||||||
|
/// bound on any pixel's `|dc|` in the current view (used to size merge radii).
|
||||||
|
/// Returns a flat array with levels concatenated (level 0 first). Empty if the
|
||||||
|
/// orbit is too short to skip.
|
||||||
|
pub fn build_bla_table(points: &[[f32; 2]], dc_max: f64) -> Vec<Bla> {
|
||||||
|
let m = points.len();
|
||||||
|
if m < 2 {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Level 0: one single step from each index i (uses Z_i). A = 2 Z, B = 1.
|
||||||
|
// Dropping e^2 is within tolerance while |e| < BLA_EPS |Z| (since the kept
|
||||||
|
// linear term is |2 Z e|).
|
||||||
|
let mut level0: Vec<BlaF> = Vec::with_capacity(m - 1);
|
||||||
|
for z in &points[..m - 1] {
|
||||||
|
let (zr, zi) = (z[0] as f64, z[1] as f64);
|
||||||
|
let zmag = (zr * zr + zi * zi).sqrt();
|
||||||
|
level0.push(BlaF {
|
||||||
|
ar: 2.0 * zr,
|
||||||
|
ai: 2.0 * zi,
|
||||||
|
br: 1.0,
|
||||||
|
bi: 0.0,
|
||||||
|
r: BLA_EPS * zmag,
|
||||||
|
l: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut levels: Vec<Vec<BlaF>> = vec![level0];
|
||||||
|
while levels.last().unwrap().len() >= 2 {
|
||||||
|
let prev = levels.last().unwrap();
|
||||||
|
let mut next = Vec::with_capacity(prev.len() / 2);
|
||||||
|
let mut i = 0;
|
||||||
|
while i + 1 < prev.len() {
|
||||||
|
next.push(merge_bla(prev[i], prev[i + 1], dc_max));
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
levels.push(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut flat = Vec::with_capacity(levels.iter().map(|l| l.len()).sum());
|
||||||
|
for level in &levels {
|
||||||
|
flat.extend(level.iter().map(|b| b.to_bla()));
|
||||||
|
}
|
||||||
|
flat
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start offset of BLA level `lv` within the flat table, computed from the orbit
|
||||||
|
/// length exactly as the shader does (`count[0] = ref_len-1`, halving each
|
||||||
|
/// level). Kept here so the traversal test mirrors the GPU indexing.
|
||||||
|
#[cfg(test)]
|
||||||
|
fn bla_level_start(ref_len: usize, lv: u32) -> (usize, usize) {
|
||||||
|
let mut start = 0usize;
|
||||||
|
let mut count = ref_len - 1;
|
||||||
|
for _ in 0..lv {
|
||||||
|
start += count;
|
||||||
|
count /= 2;
|
||||||
|
}
|
||||||
|
(start, count)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -256,4 +400,126 @@ mod tests {
|
|||||||
zi = nzi;
|
zi = nzi;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Step-by-step perturbation (drops nothing): `e_{n+1} = 2 Z_n e_n + e_n^2 + dc`,
|
||||||
|
/// using the stored f32 orbit as `Z_n`. Returns `e` after `target_n` steps.
|
||||||
|
fn advance_naive(points: &[[f32; 2]], dc: (f64, f64), target_n: usize) -> (f64, f64) {
|
||||||
|
let (mut er, mut ei) = (0.0f64, 0.0f64);
|
||||||
|
for z in &points[..target_n] {
|
||||||
|
let (zr, zi) = (z[0] as f64, z[1] as f64);
|
||||||
|
let tr = 2.0 * (zr * er - zi * ei);
|
||||||
|
let ti = 2.0 * (zr * ei + zi * er);
|
||||||
|
let sr = er * er - ei * ei;
|
||||||
|
let si = 2.0 * er * ei;
|
||||||
|
er = tr + sr + dc.0;
|
||||||
|
ei = ti + si + dc.1;
|
||||||
|
}
|
||||||
|
(er, ei)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Advance `e` to exactly `target_n` steps using the BLA table — the same
|
||||||
|
/// walk the shader performs (longest valid skip first, else a full step),
|
||||||
|
/// but never skipping past `target_n`. Returns `(e, took_a_multi_step_skip)`.
|
||||||
|
fn advance_bla(
|
||||||
|
points: &[[f32; 2]],
|
||||||
|
table: &[Bla],
|
||||||
|
dc: (f64, f64),
|
||||||
|
target_n: usize,
|
||||||
|
) -> ((f64, f64), bool) {
|
||||||
|
let ref_len = points.len();
|
||||||
|
let (mut er, mut ei) = (0.0f64, 0.0f64);
|
||||||
|
let mut n = 0usize;
|
||||||
|
let mut skipped = false;
|
||||||
|
|
||||||
|
while n < target_n {
|
||||||
|
let emag2 = er * er + ei * ei;
|
||||||
|
let mut applied = false;
|
||||||
|
|
||||||
|
// Highest level worth trying is bounded by how far we may advance.
|
||||||
|
let span = target_n - n;
|
||||||
|
let max_lv = (usize::BITS - 1 - span.leading_zeros()) as i64; // floor(log2(span))
|
||||||
|
let mut lv = max_lv;
|
||||||
|
while lv >= 0 {
|
||||||
|
let lvu = lv as u32;
|
||||||
|
let step = 1usize << lvu;
|
||||||
|
if n % step == 0 && n + step <= target_n {
|
||||||
|
let (start, count) = bla_level_start(ref_len, lvu);
|
||||||
|
let idx = n >> lvu;
|
||||||
|
if idx < count {
|
||||||
|
let b = table[start + idx];
|
||||||
|
let r = b.r as f64;
|
||||||
|
if emag2 < r * r {
|
||||||
|
let (ar, ai) = (b.a[0] as f64, b.a[1] as f64);
|
||||||
|
let (br, bi) = (b.b[0] as f64, b.b[1] as f64);
|
||||||
|
let ner = ar * er - ai * ei + br * dc.0 - bi * dc.1;
|
||||||
|
let nei = ar * ei + ai * er + br * dc.1 + bi * dc.0;
|
||||||
|
er = ner;
|
||||||
|
ei = nei;
|
||||||
|
n += step;
|
||||||
|
skipped |= step > 1;
|
||||||
|
applied = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lv -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !applied {
|
||||||
|
let (zr, zi) = (points[n][0] as f64, points[n][1] as f64);
|
||||||
|
let tr = 2.0 * (zr * er - zi * ei);
|
||||||
|
let ti = 2.0 * (zr * ei + zi * er);
|
||||||
|
let sr = er * er - ei * ei;
|
||||||
|
let si = 2.0 * er * ei;
|
||||||
|
er = tr + sr + dc.0;
|
||||||
|
ei = ti + si + dc.1;
|
||||||
|
n += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
((er, ei), skipped)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The BLA walk must reproduce step-by-step perturbation at deep zoom (where
|
||||||
|
/// the delta is tiny and skips actually fire).
|
||||||
|
#[test]
|
||||||
|
fn bla_matches_step_by_step() {
|
||||||
|
// An interior center (never escapes), so `e` stays bounded and we can
|
||||||
|
// iterate the full orbit; zoomed so |dc| ~ 1e-9 (deep enough for big
|
||||||
|
// skips). Correctness of the walk is independent of which orbit we pick.
|
||||||
|
let cr = Big::try_from(-0.5_f64).unwrap();
|
||||||
|
let ci = Big::try_from(0.0_f64).unwrap();
|
||||||
|
let points = compute_set_reference(&cr, &ci, 800, 160, FractalKind::Mandelbrot, 2);
|
||||||
|
assert!(points.len() > 64, "need a long orbit to exercise BLA levels");
|
||||||
|
|
||||||
|
let half_height = 1.0e-9_f64;
|
||||||
|
let aspect = 1.5_f64;
|
||||||
|
let dc_max = half_height * (1.0 + aspect * aspect).sqrt();
|
||||||
|
let table = build_bla_table(&points, dc_max);
|
||||||
|
assert!(!table.is_empty());
|
||||||
|
|
||||||
|
let target_n = points.len() - 1;
|
||||||
|
// A few pixel offsets across the view (all within dc_max).
|
||||||
|
let offsets = [
|
||||||
|
(0.0, 0.0),
|
||||||
|
(0.6e-9, -0.4e-9),
|
||||||
|
(-0.9e-9, 0.3e-9),
|
||||||
|
(0.2e-9, 0.8e-9),
|
||||||
|
];
|
||||||
|
let mut any_skip = false;
|
||||||
|
for dc in offsets {
|
||||||
|
let naive = advance_naive(&points, dc, target_n);
|
||||||
|
let (bla, skipped) = advance_bla(&points, &table, dc, target_n);
|
||||||
|
any_skip |= skipped;
|
||||||
|
|
||||||
|
let ref_mag = (naive.0 * naive.0 + naive.1 * naive.1).sqrt();
|
||||||
|
let err = ((bla.0 - naive.0).powi(2) + (bla.1 - naive.1).powi(2)).sqrt();
|
||||||
|
// Only the dropped e^2 differs; must stay near the BLA_EPS budget.
|
||||||
|
let tol = 1e-4 * ref_mag + 1e-15;
|
||||||
|
assert!(
|
||||||
|
err <= tol,
|
||||||
|
"dc={dc:?}: BLA {bla:?} vs naive {naive:?} (err {err:e} > tol {tol:e})"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert!(any_skip, "BLA never took a multi-step skip — test is not exercising it");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+120
-54
@@ -13,10 +13,81 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use eframe::egui_wgpu::{self, wgpu};
|
use eframe::egui_wgpu::{self, wgpu};
|
||||||
|
|
||||||
|
use super::reference::Bla;
|
||||||
|
|
||||||
/// Maximum reference-orbit length (points) the storage buffer can hold. Also
|
/// Maximum reference-orbit length (points) the storage buffer can hold. Also
|
||||||
/// bounds the iteration count. 128k points * 8 bytes = 1 MiB.
|
/// bounds the iteration count. 128k points * 8 bytes = 1 MiB.
|
||||||
pub const MAX_REF_POINTS: usize = 1 << 17;
|
pub const MAX_REF_POINTS: usize = 1 << 17;
|
||||||
|
|
||||||
|
/// Maximum BLA-table entries the storage buffer can hold. A full table has
|
||||||
|
/// ~2× the orbit length (all levels summed); ~256k entries * 24 bytes = 6 MiB.
|
||||||
|
pub const MAX_BLA_ENTRIES: usize = 2 * MAX_REF_POINTS;
|
||||||
|
|
||||||
|
/// The fractal pipeline's bind group layout: uniforms (0), reference orbit (1),
|
||||||
|
/// BLA table (2). Shared by the live renderer and [`ExportRender`].
|
||||||
|
fn fractal_bind_group_layout_desc<'a>() -> wgpu::BindGroupLayoutDescriptor<'a> {
|
||||||
|
const STORAGE: wgpu::BindingType = wgpu::BindingType::Buffer {
|
||||||
|
ty: wgpu::BufferBindingType::Storage { read_only: true },
|
||||||
|
has_dynamic_offset: false,
|
||||||
|
min_binding_size: None,
|
||||||
|
};
|
||||||
|
wgpu::BindGroupLayoutDescriptor {
|
||||||
|
label: Some("fractal bind group layout"),
|
||||||
|
entries: &[
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 0,
|
||||||
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||||
|
ty: wgpu::BindingType::Buffer {
|
||||||
|
ty: wgpu::BufferBindingType::Uniform,
|
||||||
|
has_dynamic_offset: false,
|
||||||
|
min_binding_size: None,
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 1,
|
||||||
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||||
|
ty: STORAGE,
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 2,
|
||||||
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||||
|
ty: STORAGE,
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the fractal bind group from its three buffers.
|
||||||
|
fn fractal_bind_group(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
layout: &wgpu::BindGroupLayout,
|
||||||
|
uniform_buffer: &wgpu::Buffer,
|
||||||
|
ref_buffer: &wgpu::Buffer,
|
||||||
|
bla_buffer: &wgpu::Buffer,
|
||||||
|
) -> wgpu::BindGroup {
|
||||||
|
device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
|
label: Some("fractal bind group"),
|
||||||
|
layout,
|
||||||
|
entries: &[
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: uniform_buffer.as_entire_binding(),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 1,
|
||||||
|
resource: ref_buffer.as_entire_binding(),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 2,
|
||||||
|
resource: bla_buffer.as_entire_binding(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// GPU-side view + coloring parameters. Layout must match `Uniforms` in the
|
/// GPU-side view + coloring parameters. Layout must match `Uniforms` in the
|
||||||
/// WGSL shader; total size is a multiple of 16 bytes for uniform-buffer rules.
|
/// WGSL shader; total size is a multiple of 16 bytes for uniform-buffer rules.
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
@@ -45,8 +116,9 @@ pub struct Uniforms {
|
|||||||
pub dc_offset: [f32; 2],
|
pub dc_offset: [f32; 2],
|
||||||
/// 0 = escape-time coloring, 1 = distance-estimation shading.
|
/// 0 = escape-time coloring, 1 = distance-estimation shading.
|
||||||
pub de_coloring: u32,
|
pub de_coloring: u32,
|
||||||
/// Padding to a 16-byte multiple (uniform buffer requirement).
|
/// 0 = per-iteration stepping, 1 = BLA iteration-skipping (square map only).
|
||||||
pub _pad: u32,
|
/// This also fills the struct out to a 16-byte multiple (uniform requirement).
|
||||||
|
pub use_bla: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Offscreen texture the fractal is rendered into, plus the bind group used to
|
/// Offscreen texture the fractal is rendered into, plus the bind group used to
|
||||||
@@ -72,9 +144,10 @@ pub struct FractalRenderer {
|
|||||||
bind_group_layout: wgpu::BindGroupLayout,
|
bind_group_layout: wgpu::BindGroupLayout,
|
||||||
uniform_buffer: wgpu::Buffer,
|
uniform_buffer: wgpu::Buffer,
|
||||||
ref_buffer: wgpu::Buffer,
|
ref_buffer: wgpu::Buffer,
|
||||||
|
bla_buffer: wgpu::Buffer,
|
||||||
bind_group: wgpu::BindGroup,
|
bind_group: wgpu::BindGroup,
|
||||||
target_format: wgpu::TextureFormat,
|
target_format: wgpu::TextureFormat,
|
||||||
/// Generation of the reference orbit currently uploaded to `ref_buffer`.
|
/// Generation of the reference orbit + BLA table currently uploaded.
|
||||||
uploaded_generation: u64,
|
uploaded_generation: u64,
|
||||||
|
|
||||||
/// Blit pipeline + resources that copy the cache texture to egui's surface.
|
/// Blit pipeline + resources that copy the cache texture to egui's surface.
|
||||||
@@ -108,46 +181,23 @@ impl FractalRenderer {
|
|||||||
mapped_at_creation: false,
|
mapped_at_creation: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
let bla_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
label: Some("fractal bind group layout"),
|
label: Some("bla table"),
|
||||||
entries: &[
|
size: (MAX_BLA_ENTRIES * std::mem::size_of::<Bla>()) as u64,
|
||||||
wgpu::BindGroupLayoutEntry {
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||||||
binding: 0,
|
mapped_at_creation: false,
|
||||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
|
||||||
ty: wgpu::BindingType::Buffer {
|
|
||||||
ty: wgpu::BufferBindingType::Uniform,
|
|
||||||
has_dynamic_offset: false,
|
|
||||||
min_binding_size: None,
|
|
||||||
},
|
|
||||||
count: None,
|
|
||||||
},
|
|
||||||
wgpu::BindGroupLayoutEntry {
|
|
||||||
binding: 1,
|
|
||||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
|
||||||
ty: wgpu::BindingType::Buffer {
|
|
||||||
ty: wgpu::BufferBindingType::Storage { read_only: true },
|
|
||||||
has_dynamic_offset: false,
|
|
||||||
min_binding_size: None,
|
|
||||||
},
|
|
||||||
count: None,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
let bind_group_layout =
|
||||||
label: Some("fractal bind group"),
|
device.create_bind_group_layout(&fractal_bind_group_layout_desc());
|
||||||
layout: &bind_group_layout,
|
|
||||||
entries: &[
|
let bind_group = fractal_bind_group(
|
||||||
wgpu::BindGroupEntry {
|
device,
|
||||||
binding: 0,
|
&bind_group_layout,
|
||||||
resource: uniform_buffer.as_entire_binding(),
|
&uniform_buffer,
|
||||||
},
|
&ref_buffer,
|
||||||
wgpu::BindGroupEntry {
|
&bla_buffer,
|
||||||
binding: 1,
|
);
|
||||||
resource: ref_buffer.as_entire_binding(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
label: Some("fractal pipeline layout"),
|
label: Some("fractal pipeline layout"),
|
||||||
@@ -254,6 +304,7 @@ impl FractalRenderer {
|
|||||||
bind_group_layout,
|
bind_group_layout,
|
||||||
uniform_buffer,
|
uniform_buffer,
|
||||||
ref_buffer,
|
ref_buffer,
|
||||||
|
bla_buffer,
|
||||||
bind_group,
|
bind_group,
|
||||||
target_format,
|
target_format,
|
||||||
uploaded_generation: u64::MAX,
|
uploaded_generation: u64::MAX,
|
||||||
@@ -361,6 +412,7 @@ impl ExportRender {
|
|||||||
height: u32,
|
height: u32,
|
||||||
uniforms: Uniforms,
|
uniforms: Uniforms,
|
||||||
reference: &[[f32; 2]],
|
reference: &[[f32; 2]],
|
||||||
|
bla: &[Bla],
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
label: Some("export uniforms"),
|
label: Some("export uniforms"),
|
||||||
@@ -381,20 +433,24 @@ impl ExportRender {
|
|||||||
queue.write_buffer(&ref_buffer, 0, bytemuck::cast_slice(&reference[..count]));
|
queue.write_buffer(&ref_buffer, 0, bytemuck::cast_slice(&reference[..count]));
|
||||||
}
|
}
|
||||||
|
|
||||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
let bla_count = bla.len().min(MAX_BLA_ENTRIES);
|
||||||
label: Some("export bind group"),
|
let bla_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
layout: bind_group_layout,
|
label: Some("export bla table"),
|
||||||
entries: &[
|
size: (bla_count.max(1) * std::mem::size_of::<Bla>()) as u64,
|
||||||
wgpu::BindGroupEntry {
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||||||
binding: 0,
|
mapped_at_creation: false,
|
||||||
resource: uniform_buffer.as_entire_binding(),
|
|
||||||
},
|
|
||||||
wgpu::BindGroupEntry {
|
|
||||||
binding: 1,
|
|
||||||
resource: ref_buffer.as_entire_binding(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
if bla_count > 0 {
|
||||||
|
queue.write_buffer(&bla_buffer, 0, bytemuck::cast_slice(&bla[..bla_count]));
|
||||||
|
}
|
||||||
|
|
||||||
|
let bind_group = fractal_bind_group(
|
||||||
|
device,
|
||||||
|
bind_group_layout,
|
||||||
|
&uniform_buffer,
|
||||||
|
&ref_buffer,
|
||||||
|
&bla_buffer,
|
||||||
|
);
|
||||||
|
|
||||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||||
label: Some("export target"),
|
label: Some("export target"),
|
||||||
@@ -583,6 +639,8 @@ pub fn encode_png_with_progress(
|
|||||||
pub struct FractalCallback {
|
pub struct FractalCallback {
|
||||||
pub uniforms: Uniforms,
|
pub uniforms: Uniforms,
|
||||||
pub reference: Arc<Vec<[f32; 2]>>,
|
pub reference: Arc<Vec<[f32; 2]>>,
|
||||||
|
/// BLA table for the current reference (empty when BLA is off/unsupported).
|
||||||
|
pub bla: Arc<Vec<Bla>>,
|
||||||
pub generation: u64,
|
pub generation: u64,
|
||||||
/// Widget size in physical pixels — the cache texture resolution.
|
/// Widget size in physical pixels — the cache texture resolution.
|
||||||
pub size_px: [u32; 2],
|
pub size_px: [u32; 2],
|
||||||
@@ -612,6 +670,14 @@ impl egui_wgpu::CallbackTrait for FractalCallback {
|
|||||||
0,
|
0,
|
||||||
bytemuck::cast_slice(&self.reference[..count]),
|
bytemuck::cast_slice(&self.reference[..count]),
|
||||||
);
|
);
|
||||||
|
let bla_count = self.bla.len().min(MAX_BLA_ENTRIES);
|
||||||
|
if bla_count > 0 {
|
||||||
|
queue.write_buffer(
|
||||||
|
&renderer.bla_buffer,
|
||||||
|
0,
|
||||||
|
bytemuck::cast_slice(&self.bla[..bla_count]),
|
||||||
|
);
|
||||||
|
}
|
||||||
renderer.uploaded_generation = self.generation;
|
renderer.uploaded_generation = self.generation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,17 @@ struct Uniforms {
|
|||||||
dc_offset: vec2<f32>,
|
dc_offset: vec2<f32>,
|
||||||
// 0 = escape-time coloring, 1 = distance-estimation shading.
|
// 0 = escape-time coloring, 1 = distance-estimation shading.
|
||||||
de_coloring: u32,
|
de_coloring: u32,
|
||||||
|
// 0 = per-iteration stepping, 1 = BLA iteration-skipping (square map only).
|
||||||
|
use_bla: u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
// One merged linear step from the BLA table: e_{n+l} = A e + B dc, valid while
|
||||||
|
// |e| < r. Matches the Rust `Bla` struct (24-byte std430 stride).
|
||||||
|
struct Bla {
|
||||||
|
a: vec2<f32>,
|
||||||
|
b: vec2<f32>,
|
||||||
|
r: f32,
|
||||||
|
l: u32,
|
||||||
};
|
};
|
||||||
|
|
||||||
const KIND_MANDELBROT: u32 = 0u;
|
const KIND_MANDELBROT: u32 = 0u;
|
||||||
@@ -38,6 +49,9 @@ const KIND_MULTIBROT: u32 = 3u;
|
|||||||
|
|
||||||
@group(0) @binding(0) var<uniform> u: Uniforms;
|
@group(0) @binding(0) var<uniform> u: Uniforms;
|
||||||
@group(0) @binding(1) var<storage, read> ref_orbit: array<vec2<f32>>;
|
@group(0) @binding(1) var<storage, read> ref_orbit: array<vec2<f32>>;
|
||||||
|
// BLA table, levels concatenated (level 0 first). Per-level counts are recomputed
|
||||||
|
// from `ref_len` exactly as the CPU builder laid them out.
|
||||||
|
@group(0) @binding(2) var<storage, read> bla_table: array<Bla>;
|
||||||
|
|
||||||
struct VsOut {
|
struct VsOut {
|
||||||
@builtin(position) pos: vec4<f32>,
|
@builtin(position) pos: vec4<f32>,
|
||||||
@@ -169,6 +183,45 @@ fn palette(id: u32, t: f32) -> vec3<f32> {
|
|||||||
return a + b * cos(6.28318530718 * (c * t + d));
|
return a + b * cos(6.28318530718 * (c * t + d));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Result of a BLA lookup at an orbit index.
|
||||||
|
struct Hop {
|
||||||
|
found: bool,
|
||||||
|
a: vec2<f32>,
|
||||||
|
b: vec2<f32>,
|
||||||
|
l: u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Longest valid BLA skip starting at orbit index `n` for a delta of squared
|
||||||
|
// magnitude `emag2`. Walks levels low->high, recomputing each level's flat-array
|
||||||
|
// start and count from `ref_len` (count[0] = ref_len-1, halving each level).
|
||||||
|
// Radii shrink with level and alignment is monotonic, so the valid levels form a
|
||||||
|
// prefix: we keep the last valid one and stop at the first that fails.
|
||||||
|
fn bla_find(n: u32, emag2: f32) -> Hop {
|
||||||
|
var res: Hop;
|
||||||
|
res.found = false;
|
||||||
|
|
||||||
|
var start: u32 = 0u;
|
||||||
|
var count: u32 = u.ref_len - 1u;
|
||||||
|
var lv: u32 = 0u;
|
||||||
|
loop {
|
||||||
|
if (count == 0u) { break; }
|
||||||
|
let step = 1u << lv;
|
||||||
|
if ((n & (step - 1u)) != 0u) { break; } // n not aligned to this level
|
||||||
|
let idx = n >> lv;
|
||||||
|
if (idx >= count) { break; } // run would exceed the orbit
|
||||||
|
let b = bla_table[start + idx];
|
||||||
|
if (emag2 >= b.r * b.r) { break; } // delta too large: not valid
|
||||||
|
res.found = true;
|
||||||
|
res.a = b.a;
|
||||||
|
res.b = b.b;
|
||||||
|
res.l = b.l;
|
||||||
|
start = start + count;
|
||||||
|
count = count / 2u;
|
||||||
|
lv = lv + 1u;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
// Perturbation iterate + color a single sample. `offset` is the per-pixel
|
// Perturbation iterate + color a single sample. `offset` is the per-pixel
|
||||||
// offset in complex units. For Mandelbrot it is the c-plane offset added every
|
// offset in complex units. For Mandelbrot it is the c-plane offset added every
|
||||||
// step (delta starts at 0); for Julia it is the z-plane offset that seeds the
|
// step (delta starts at 0); for Julia it is the z-plane offset that seeds the
|
||||||
@@ -208,16 +261,34 @@ fn shade(offset: vec2<f32>, px: f32) -> vec3<f32> {
|
|||||||
break; // interior
|
break; // interior
|
||||||
}
|
}
|
||||||
|
|
||||||
// Propagate the derivative of the full orbit (unaffected by rebasing,
|
// Advance one step, or skip a whole run via BLA when the delta is small
|
||||||
// which only re-expresses the same value). Only when DE is enabled.
|
// enough (square map only; other kinds keep `use_bla == 0`). The orbit
|
||||||
if (u.de_coloring != 0u) {
|
// derivative for DE follows the same linear map: over a run it advances
|
||||||
dz = cmul(fprime(z), dz) + dz_seed;
|
// by the run's own (A, B) coefficients, matching the per-step recurrence.
|
||||||
|
var did_skip = false;
|
||||||
|
if (u.use_bla != 0u) {
|
||||||
|
let hop = bla_find(m, dot(e, e));
|
||||||
|
if (hop.found) {
|
||||||
|
if (u.de_coloring != 0u) {
|
||||||
|
dz = cmul(hop.a, dz) + cmul(hop.b, dz_seed);
|
||||||
|
}
|
||||||
|
e = cmul(hop.a, e) + cmul(hop.b, step_add);
|
||||||
|
m = m + hop.l;
|
||||||
|
n = n + hop.l;
|
||||||
|
did_skip = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!did_skip) {
|
||||||
|
// Propagate the derivative of the full orbit (unaffected by rebasing,
|
||||||
|
// which only re-expresses the same value). Only when DE is enabled.
|
||||||
|
if (u.de_coloring != 0u) {
|
||||||
|
dz = cmul(fprime(z), dz) + dz_seed;
|
||||||
|
}
|
||||||
|
// Advance the delta by this fractal's formula (+ dc for the set plane).
|
||||||
|
e = advance_delta(xm, e) + step_add;
|
||||||
|
m = m + 1u;
|
||||||
|
n = n + 1u;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Advance the delta by this fractal's formula (+ dc for the set plane).
|
|
||||||
e = advance_delta(xm, e) + step_add;
|
|
||||||
m = m + 1u;
|
|
||||||
n = n + 1u;
|
|
||||||
|
|
||||||
// Keep the reference index valid and the delta small.
|
// Keep the reference index valid and the delta small.
|
||||||
if (m >= u.ref_len) {
|
if (m >= u.ref_len) {
|
||||||
|
|||||||
+12
-1
@@ -9,7 +9,7 @@
|
|||||||
use std::sync::mpsc::{Receiver, Sender, TryRecvError, channel};
|
use std::sync::mpsc::{Receiver, Sender, TryRecvError, channel};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
use crate::fractal::{FractalKind, compute_reference, compute_set_reference};
|
use crate::fractal::{Bla, FractalKind, build_bla_table, compute_reference, compute_set_reference};
|
||||||
use crate::view::{Big, big_from_f64};
|
use crate::view::{Big, big_from_f64};
|
||||||
|
|
||||||
pub struct RefRequest {
|
pub struct RefRequest {
|
||||||
@@ -22,6 +22,10 @@ pub struct RefRequest {
|
|||||||
pub precision: usize,
|
pub precision: usize,
|
||||||
pub kind: FractalKind,
|
pub kind: FractalKind,
|
||||||
pub power: u32,
|
pub power: u32,
|
||||||
|
/// Build the BLA iteration-skip table for this orbit (square map only).
|
||||||
|
pub build_bla: bool,
|
||||||
|
/// Upper bound on any pixel's `|dc|`, sizing the BLA merge radii.
|
||||||
|
pub dc_max: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RefResult {
|
pub struct RefResult {
|
||||||
@@ -29,6 +33,7 @@ pub struct RefResult {
|
|||||||
pub center_im: Big,
|
pub center_im: Big,
|
||||||
pub half_height: f64,
|
pub half_height: f64,
|
||||||
pub points: Vec<[f32; 2]>,
|
pub points: Vec<[f32; 2]>,
|
||||||
|
pub bla: Vec<Bla>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RefWorker {
|
pub struct RefWorker {
|
||||||
@@ -56,12 +61,18 @@ impl RefWorker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let points = compute(&req);
|
let points = compute(&req);
|
||||||
|
let bla = if req.build_bla {
|
||||||
|
build_bla_table(&points, req.dc_max)
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
if res_tx
|
if res_tx
|
||||||
.send(RefResult {
|
.send(RefResult {
|
||||||
center_re: req.center_re,
|
center_re: req.center_re,
|
||||||
center_im: req.center_im,
|
center_im: req.center_im,
|
||||||
half_height: req.half_height,
|
half_height: req.half_height,
|
||||||
points,
|
points,
|
||||||
|
bla,
|
||||||
})
|
})
|
||||||
.is_err()
|
.is_err()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user