Skip to content

Deep zoom: why it usually stops, and how it keeps going

Zoom into most fractal programs and somewhere around a span of 101310^{-13} the image goes wrong — first subtly gritty, then blocky, then a mosaic of solid squares. Nothing is wrong with the mathematics; the set has infinite detail. What ran out is the numbers.

The pixel-grid problem

A computer's standard number — the IEEE double — carries 53 binary digits of precision, about 15–16 decimal digits. Near 11, the gap between one representable double and the next is 2522.2×10162^{-52} \approx 2.2 \times 10^{-16}: numbers closer together than that are the same number to the machine. Now picture a view centred near c0.74c \approx -0.74 with a span of 101510^{-15}: two neighbouring pixels differ only beyond the 16th digit, so they round to the identical double, iterate the identical orbit, and paint the identical colour. Whole blocks of pixels collapse onto one representable value each. That's the mosaic.

Deep seahorse valley spirals in synthwave pinks and blues
Deep Seahorse Valley. A widely-shared deep coordinate — at this span, plain screen arithmetic still holds, but the floor is only a few powers of ten away. c ≈ −0.743643887037151 + 0.13182590420533i · span 3×10⁻⁴ · open this view in Mandala →

The precision ladder

Mandala watches the size of a pixel in complex units and switches arithmetic as you descend — automatically, per frame (the current tier shows in the HUD).

f64 — to about 10⁻¹³

Plain hardware doubles, on the GPU in a single shader pass or across a pool of WebAssembly workers. Fast, and fine for almost all sightseeing.

Double-double — to about 10⁻²⁷

Every number becomes an unevaluated pair of doubles (h,)(h, \ell) whose exact sum h+h + \ell carries roughly twice the digits (~31 significant decimals). Error-free transformations — tricks like two-sum, which recovers the exact rounding error of an addition into \ell — keep the pair honest through every add and multiply. Several times slower than raw doubles, still entirely practical.

Perturbation — below 10⁻²⁷, and past 10⁻³⁰⁰

The clever one. Computing every pixel in hundreds-of-digits arithmetic would be ruinous. Instead, compute one orbit at the view's centre — the reference orbit Z0,Z1,Z2,Z_0, Z_1, Z_2, \dots — in arbitrary precision, as many digits as the depth demands. Every pixel is then expressed as a tiny difference from the reference: zn=Zn+Δnz_n = Z_n + \Delta_n, where the pixel's cc is the reference's CC plus a tiny offset δ\delta. Substitute into zn+1=zn2+cz_{n+1} = z_n^2 + c:

Zn+1+Δn+1=(Zn+Δn)2+C+δ=Zn2+C=  Zn+1+  2ZnΔn+Δn2+δ.Z_{n+1} + \Delta_{n+1} = (Z_n + \Delta_n)^2 + C + \delta = \underbrace{Z_n^2 + C}_{=\;Z_{n+1}} + \;2 Z_n \Delta_n + \Delta_n^2 + \delta.

The reference terms cancel exactly, leaving the perturbation recurrence:

Δn+1=2ZnΔn+Δn2+δ.\Delta_{n+1} = 2\,Z_n\,\Delta_n + \Delta_n^2 + \delta.

Everything in that equation is smallΔ\Delta and δ\delta are pixel-scale — so plain doubles handle it again until their exponent range is exhausted. One expensive orbit, a million cheap ones.

Rebasing: changing the ruler before it fails

Perturbation is accurate only while the pixel orbit remains a small correction to the reference. Near a minibrot the two can decorrelate: ZnZ_n and Δn\Delta_n become individually large while their sum znz_n is small, so the subtraction implicit in zn=Zn+Δnz_n=Z_n+\Delta_n loses digits. That is the classic deep-zoom glitch.

Mandala detects the dangerous geometry — roughly, when the true value becomes smaller than the correction, Zn+Δn<Δn|Z_n+\Delta_n|<|\Delta_n| — and rebases. It promotes the current true value to a fresh delta against the beginning of the same stored reference orbit, resets the reference index, and carries on. No second arbitrary-precision orbit is needed; only the ruler changes.

BLA: turning many iterations into one

Perturbation restores cheap arithmetic, but a pixel near the boundary may still need hundreds of thousands of iterations. Bivariate linear approximation (BLA) removes most of those steps.

Start from the exact quadratic delta recurrence:

Δn+1=2ZnΔn+Δn2+δ.\Delta_{n+1}=2Z_n\Delta_n+\Delta_n^2+\delta.

While Δn|\Delta_n| is sufficiently small, the quadratic term is negligible. One step is then an affine function of two independent small variables — the current orbit delta Δ\Delta and the pixel's constant parameter delta δ\delta:

Δn+1AnΔn+Bnδ,An=2Zn,Bn=1.\Delta_{n+1}\approx A_n\Delta_n+B_n\delta, \qquad A_n=2Z_n,\quad B_n=1.

That is why the name is bivariate: linear in the two perturbations Δ\Delta and δ\delta. Suppose one precomputed block applies x(Δ)=AxΔ+Bxδx(\Delta)=A_x\Delta+B_x\delta and the next applies y(Δ)=AyΔ+Byδy(\Delta)=A_y\Delta+B_y\delta. Their composition is still the same kind of map:

y(x(Δ))=AyAxAΔ+(AyBx+By)Bδ.y(x(\Delta)) =\underbrace{A_yA_x}_{A}\Delta +\underbrace{(A_yB_x+B_y)}_{B}\delta.

So two steps collapse into one pair (A,B)(A,B); two such pairs collapse into four steps, then eight, sixteen, and so on. When a render worker receives a reference orbit, Mandala builds those power-of-two blocks into a binary table once for that orbit, not once per tile. At runtime a pixel asks for the longest aligned block that fits its remaining iteration budget.

The safety radius

Dropping Δ2\Delta^2 cannot be allowed everywhere. Every table entry therefore stores a conservative radius rr: the approximation may run only while Δr|\Delta|\le r. At the single-step level Mandala uses a relative tolerance ε=224\varepsilon=2^{-24}; for the quadratic map the radius is based on ε2Zn\varepsilon|2Z_n|. When two blocks are merged, the parent must be valid for both children. If block xx runs before block yy, its radius is restricted to

r=min ⁣(rx,ryBxδmaxAx),r=\min\!\left(r_x, \frac{r_y-|B_x|\,\delta_{\max}}{|A_x|}\right),

clamped at zero. Here δmax\delta_{\max} bounds every pixel offset in the whole frame. This is the important contract: a smaller radius merely skips less; it never changes the answer. As a pixel approaches escape and outgrows the stored radius, lookup fails safely and the engine takes an exact perturbation step.

The cubic Multibrot uses the same table machinery with the level-zero derivative An=3Zn2A_n=3Z_n^2 and a radius suited to the dropped quadratic and cubic terms. In a Julia view δ=0\delta=0, so the BδB\delta half simply vanishes. Burning Ship, Tricorn and Celtic contain folds rather than a single complex- linear derivative, while Phoenix carries a two-value state; their exact perturbation recurrences do not fit this single-Δ\Delta table and therefore do not use BLA.

WHAT BLA DOES — AND DOES NOT DO

BLA is a speed technique, not a source of precision. The arbitrary-precision reference and perturbation recurrence make the deep zoom correct; the BLA table proves when a run of those steps can be replaced by one conservative affine step. Mandala's BLA-vs-plain-perturbation tests pin that equivalence.

Past the exponent floor: floatexp

Perturbation has one more numerical floor. An IEEE double holds full precision only down to its smallest normal value, about 1030810^{-308}; below that a narrow band of subnormal doubles limps on with fewer and fewer significant bits, until past roughly 1032410^{-324} the pixel offset is exactly zero — and an offset that has collapsed toward zero simply rides the reference orbit instead of carrying its own detail. More significant digits do not help when the exponent range has run out.

Mandala starts such pixels in an extended-range representation

x=m2e,x=m\,2^e,

with a double mantissa mm and a separate integer exponent ee. This floatexp value does not try to make every operation arbitrary precision; it only keeps extremely tiny deltas alive. The engine runs the family's exact delta recurrence in extended range until the value grows into normal-double territory, converts it to f64f64, and hands the rest to the fast perturbation loop. BLA begins after that handoff.

Every escape-time family has its own warm-up in both planes: the absolute-value families retain their sign-continuous identities, and Phoenix carries both its current and previous deltas. This is what lets all six escape-time families continue beyond 1030010^{-300} rather than turning every initial offset into zero.

Fine spiral detail rendered beyond double precision depth
Past the f64 floor. A span of 6×10⁻¹⁴ — past the reach of ordinary double precision. This frame is computed on the double-double tier (~31 digits); the picture just keeps going. c ≈ −0.7436438870371587… + 0.1318259042053119…i · span 6×10⁻¹⁴ · open this view in Mandala →

There is one more quiet ingredient: the coordinates themselves. At a span of 1030010^{-300} the centre of your view needs hundreds of digits just to be written down, so Mandala carries view centres as arbitrary-precision decimals everywhere — including in share links and bookmarks. A link to a deep view reopens exactly there, to the last digit.

What this buys you

All six escape-time families ride this ladder and floatexp warm-up in both planes. Mandelbrot and the cubic Multibrot additionally use BLA; the non-analytic families take exact per-step perturbation paths. The two root-finding families cap at the double-double tier, around 102710^{-27}. For scale: at 1030010^{-300}, a single pixel is to the full set roughly what a proton is to the observable universe — with about 260260 orders of magnitude to spare. The explorer's job is to make that depth boring to reach: scroll, and the ladder climbs under you.