How a frame gets fast
Count the work in one naive frame: two million pixels, each iterating up to its budget — near the boundary, thousands of steps; at depth, up to the ~200,000-iteration budget. Multiply it out and a single screenful is billions of multiply-adds. Everything in this chapter is one of two moves: do the work in the right place, or prove you don't have to do it at all. The second kind is where the real wins live.
Two backends, one image
Shallow views go to the GPU: a single shader pass, one thread per pixel, the whole frame in one dispatch — this is what makes casual panning and the flow animation feel instant. WGSL has no 64-bit float, so the shader represents important coordinates as double-single pairs of 32-bit values, recovering about 14 significant decimal digits.
That arithmetic depends on strict rounding. Some drivers collapse the pair toward ordinary 32-bit precision under fast-math, so Mandala runs a tiny one-pixel precision probe on each GPU adapter. A healthy result keeps the GPU active through most of the tier; a degraded result moves the handoff shallower, before blockiness becomes visible. A small hysteresis gap means hovering around the threshold cannot make the renderer flap between backends.
Deep views (and browsers without a healthy WebGPU device) go to the CPU backend: the iteration kernel compiled from Rust to WebAssembly, running across a pool of parallel workers, which is where true and the extended-precision arithmetic of the deep-zoom chapter live. The explorer switches automatically with depth and cross-fades the handoff; both backends colour through the same contract, so the switch is invisible — the HUD is where you see which one is on duty.
From screen pixel to complex number
Before either backend can iterate, it must decide what point each sample represents. A view carries its centre and two complex step vectors and — one screen pixel right and one screen pixel down. Sample maps to
Without rotation those vectors are the familiar horizontal and vertical pixel scales. Rotating the view rotates the basis instead of the finished bitmap, so the fractal is sampled correctly at the new angle by the GPU, , double-double and perturbation engines alike. The deep reference orbit still depends only on ; rotation changes the pixel deltas, not the one expensive orbit at the centre.
Tiles, order, and throwing work away
The CPU backend cuts the frame into fixed -pixel tiles and deals them to a worker pool sized from the available CPU cores (leaving one for the interface). Each worker owns a WASM instance; completed RGBA buffers transfer back without copying and are painted immediately. Three details do the heavy lifting:
- Centre-out order. Tiles are sorted by distance from the view centre, so the region you're looking at resolves first and the corners fill last.
- Cancellation. Every tile carries its frame's id. Zoom again before the frame finishes and the stale tiles are simply dropped on arrival — fast navigation never queues up behind old work.
- Progressive preview. During a pan the previous frame slides with the pointer; during a zoom it becomes a coarse pixelated preview. Pending cells pulse subtly and snap to crisp detail as real tiles land, so even an expensive deep frame is never a blank screen.

One reference, resident where the work happens
A perturbation frame first computes its arbitrary-precision reference orbit in a dedicated worker, with working digits chosen from the current pixel scale plus guard digits. That prevents a deep reference from blocking the interface.
The resulting orbit can be several megabytes, so copying it with every tile would erase the benefit of tiling. Instead it crosses into each render worker only on that worker's first tile for the frame. The worker stores the orbit by id inside its WASM instance and builds the BLA table once; every later tile refers to the resident data by id. The cost is therefore once per worker per frame, never once per tile or pixel.
The best optimisation: not iterating
An escaping pixel stops the moment it escapes — cheap. The expensive pixels are the interior ones, which run their entire budget just to conclude "bounded". Two lines of mathematics eliminate most of them before a single iteration runs.
The Mandelbrot chapter derived where orbits settle: the main cardioid and the period-2 disk. Those derivations invert into membership tests. With and :
A pixel passing either test is provably in the set — painted black immediately, full iteration budget skipped. A handful of multiplications replaces what are, by construction, the most expensive pixels on screen. (The tests are statements about the critical orbit of the quadratic map, so they apply exactly when rendering the Mandelbrot parameter plane — Julia views and other families bypass them.)
The same idea extends dynamically. Many bounded orbits are drawn into an attracting cycle, so an orbit that revisits a value it has seen before can be treated as interior — no need to run out the budget. This is a heuristic, not a theorem: not every bounded orbit is periodic. Some stay bounded yet never repeat — dense or chaotic orbits, and the irrational rotations that occur on parts of the boundary — so cycle detection is an optimisation that catches the common attracting-cycle case, never a proof of membership. Detecting revisits cheaply is a classic problem (Brent's cycle-detection algorithm: compare against a checkpoint that re-arms at power-of-two intervals). Mandala applies it where it pays most: the deep-zoom reference orbit, with one refinement — a cycle is only trusted if it is attracting (multiplier ), so a repelling revisit near a Misiurewicz point can never truncate an orbit that merely came close to repeating.
Patience scales with depth
How many iterations are enough? Near the boundary, escape times grow without bound, and the deeper you zoom the closer to the boundary every pixel sits. Stop too early and undecided pixels get painted black — deep filaments silently fatten into false interior. So the iteration budget is not a constant: Mandala scales it automatically, growing roughly with the square of the zoom depth. The budget is why deep frames cost more even per pixel — and the interior tests above are why that cost stays payable.
Skipping iterations in bulk
At depth, one more trick changes the arithmetic entirely. The deep-zoom chapter shows how every pixel becomes a tiny delta iterated against a shared reference orbit. While is small, its recurrence is effectively linear — and a run of linear steps composes into a single precomputed step. That is bivariate linear approximation (BLA): a table built once per reference orbit that lets a pixel leap over long stretches of iterations in one bound, safely, with a stored radius as the admission test. The BLA derivation and merge rule show why the skip is conservative rather than a visual shortcut.
Sharpness has a price tag
Anti-aliasing is the one control that multiplies the work. The Quality setting supersamples each pixel on an sub-grid — 2× means four full iterations per pixel, 3× means nine — and averages the resulting colours (in linear floats, before the final rounding, so the average is mathematically honest). It is the biggest single cost knob in the app, which is why it's a choice and not a default.
This ordering matters: each sub-sample completes its own orbit, smoothing, palette and shading pipeline; only then are the floating-point RGB values averaged and packed into one 8-bit pixel. Averaging coordinates or iteration counts first would answer a different mathematical question and blur thin boundaries incorrectly.
Failure must not masquerade as mathematics
A rendering error can look disturbingly plausible — a stale tile resembles a minibrot, and a failed worker can leave a smooth square that looks like interior. The pipeline therefore treats frame identity and health as part of correctness:
- Every result carries a frame id; superseded results are discarded rather than painted into the new view.
- A worker failure releases its slot instead of hanging the frame. One missing tile leaves the preview beneath it; repeated losses surface a rendering degraded warning and a clean frame clears it.
- Shader compilation and GPU device loss fail over to CPU. A broken GPU path never gets to present a blank canvas as a valid fractal.
- Shared constants, pinned CPU images and GPU-versus-CPU screenshots guard the agreement between precision tiers and backends.
The classics this explorer doesn't use
Fractal renderers have a folklore of further tricks, and honesty requires saying why they're absent here:
- Rectangle checking (Mariani–Silver) and border tracing fill a region wholesale when its border shares one iteration count. But under smooth colouring, equal integer count no longer means equal colour — the fractional term differs — so the classic versions trade correctness for speed. The analytic interior tests above already capture the biggest wins without that trade.
- Mirror symmetry (the set is symmetric about the real axis) only helps when the view straddles the axis — rarely, once you're exploring.
- Adaptive anti-aliasing — supersampling only near the boundary, using the distance estimate as the trigger — is the open frontier: most of the quality for a fraction of the cost.