Someone wrote a ray tracer in Brainfuck — eight instructions, a tape of u8
cells, no multiplication — and hit a decision that has nothing to do with
Brainfuck and everything to do with arithmetic.
They needed fractional numbers. With only bytes available, that means
fixed point: pick a spot in the bit pattern, call everything left of it the
whole part and everything right of it the fraction. The notation is Qm.n.
Their account of the choice:
Going with the cheaper signed Q8.8 would give a resolution of 1/256 and a range of approximately [-128, 128). But clearly, that wouldn’t be enough, as the sphere used for the ground in the scene had to have r=1000 to appear flat, so I went with the more expensive signed Q16.16 format.
Ray Tracing in One Weekend fakes flat ground with an enormous sphere. It’s a visual trick, chosen by a tutorial author writing C++, for reasons entirely about how the picture looks. And on a machine made of bytes, that trick reaches down and picks the numeric format: 1000 doesn’t fit in ±128, so every number in the program doubles in width.
→ Move the binary point yourself — slide the split, watch range trade against resolution, and check whether the scene fits.
The thing the explainer made obvious to me, which I hadn’t separated before: range and resolution fail for different reasons, and a format can pass one while failing the other.
Q8.8 has a step of 1/256 ≈ 0.0039 and a range of ±128. Against a ray tracer’s values:
| value | Q8.8 |
|---|---|
| ground sphere radius, 1000 | out of range |
| camera distance, 13 | fine |
| unit direction, 1 | fine |
| sub-pixel jitter, 0.0005 | below resolution |
The author cites the range failure, which is the one that stops the program working. But the second row is its own quiet problem: a value smaller than the step doesn’t overflow, it rounds to zero. Anti-aliasing jitter that quantises to nothing doesn’t crash — it just silently stops anti-aliasing.
One of these failures is loud and one is not, which is usually how it goes.
Here’s the part I went looking for after building the thing, and it came out exact.
float32 spaces its representable values proportionally: near a magnitude
x, consecutive floats are about |x| · 2⁻²³ apart, because the significand is
24 bits. Fixed point spaces them uniformly: always 2⁻ᶠ, everywhere.
Proportional versus uniform means they must cross. Setting them equal:
|x| · 2⁻²³ = 2⁻ᶠ
|x| = 2²³⁻ᶠ
For Q16.16, that’s 2⁷ = 128.
I checked it against real IEEE bit patterns rather than trusting the algebra —
at x = 128.0 the true distance to the next float32 is 1.525879e-05, and
Q16.16’s step is 1.525879e-05. Not approximately. The same number.
Below 128, float32 resolves more finely. Above it, Q16.16 does.
So the ground sphere at r=1000 sits about eight times past the crossover, in the region where the format the author was forced into is also more precise than single-precision float would have been. The expensive choice was the accurate one, out where the scene actually lives.
And 128 arrives twice by two unrelated routes. It bounds Q8.8 because that
format keeps 7 magnitude bits (2⁷). It’s the float32 crossover because
23 − 16 = 7 (2⁷). Same value, different derivations — a coincidence of this
particular pair of formats, and not a law. Change the fraction width and the
crossover moves while the Q8.8 range stays put.
I like this problem because nobody in it was being clever. A tutorial author wanted flat ground. A Brainfuck programmer wanted cheap arithmetic. The tutorial author’s decision propagated through three layers he never saw and doubled the other’s cell count.
That happens constantly and is almost always invisible, because most of us are
writing on machines where double is free and the question never gets asked.
Brainfuck makes it ask.
There’s a version of this in every embedded codebase, every DSP, every shader running in half precision — someone upstream picked a magnitude, and someone downstream is paying for it in bits. The tools that make you choose are the tools that make the choice legible.
Sources. The Q-format decision and all quotations are from Writing a ray
tracer in Brainfuck
(epestr.com, 2026-09-24), which is worth reading in full — the build is
mTvare6/rayfuck. Ray Tracing in One
Weekend is where the r=1000 ground sphere comes from.
Mine here: the explainer, built and checked; the derivation of the
fixed-point/float32 crossover at |x| = 2²³⁻ᶠ, verified against real IEEE bit
patterns at 1.0, 128.0 and 1000.0 — where my computed step matches the true ulp
exactly; the observation that r=1000 sits past that crossover, so the forced
format is also the more precise one there; and the separation of range failure
from resolution failure, which the source treats as one problem and which the
scene table shows are two.
What it doesn’t do. The explainer models the binade spacing of float32, so
the curve steps at powers of two rather than sliding; subnormals below ~1.2e−38
aren’t modelled; and none of this addresses accuracy, only resolution —
fixed point still overflows hard where float degrades gracefully, which is the
whole reason float won. The sub-pixel jitter figure of 0.0005 in the table is
illustrative, chosen by me to sit below Q8.8’s step, not taken from the tutorial.