Jump flooding does not pay until 500 seeds

When a distance field has to be rebuilt every frame from sources that move, the analytic approach stops applying and you have two options: evaluate every source at every pixel, or jump flood. The standard advice is to jump flood. The standard advice does not come with a number.

A live field from moving seeds, banded by distance with cell boundaries drawn. Left: 24 seeds. Right: 220. Both brute force.

The measurement

Both methods, real WebGL2, 960×540, float render targets, median of 70 frames after a 30 frame warm-up, on an Apple M4 Max. Jump flooding is a seed splat plus ten ping-pong passes of nine taps each, which is ceil(log2(960)).

SeedsBrute forceJump floodWinner
40.050 ms0.417 msbrute, 8x
160.050 ms0.400 msbrute, 8x
640.083 ms0.400 msbrute, 4.8x
1280.133 ms0.400 msbrute, 3x
2560.217 ms0.400 msbrute, 1.8x
5120.383 ms0.400 mseven
10240.750 ms0.417 msflood, 1.8x
20481.517 ms0.417 msflood, 3.6x

Reading it

Jump flooding is flat. 0.400ms at four seeds and 0.417ms at two thousand, which is the entire promise of the algorithm delivered exactly. Its cost is ten full-screen passes and it does not care what is in them.

Brute force is linear, at 0.00071ms per seed above a floor of about 0.037ms. The fit is boring: predicted at 256 is 0.218ms, measured 0.217ms.

They cross at about 510 seeds. Below that brute force wins, and near the bottom of the range it wins by eight times while also being about fifteen lines of code with no render targets, no ping-pong, and no approximation.

That number is the useful output here, because the received wisdom is to reach for jump flooding as soon as a field has to be dynamic. For most of what a stylized 2D renderer actually does, forty lights, a hundred particles, a few dozen influence sources, brute force is both faster and simpler and the choice is not close.

Where the crossover moves

It is one number on one machine, so it is worth knowing which way it slides:

The other differences, which are not speed

How this benchmark lied twice before it worked

Both worth repeating because they are the two standard ways a GPU measurement comes out confidently wrong:

  1. The flush read the wrong target. Every pass rendered to a framebuffer object, and the timer's readPixels read the default framebuffer, so it waited for nothing. Brute force measured 0.000ms at every seed count, which looks like a triumph rather than a bug.
  2. The seed pass was brute force in disguise. The first version seeded by looping over every seed at every pixel, which makes the "jump flood" path O(N) per pixel plus ten extra passes. It lost at every count, and the conclusion would have been the exact opposite of the truth.

The tell for the second one was in the data: the supposedly flat method was not flat. An algorithm whose whole property is independence from N is measurable against itself, and a curve where there should be a line is the measurement being wrong rather than the algorithm.

Rules of thumb

  1. Brute force until about 500 moving seeds, on desktop, at 1080p or below. Verify it on your own hardware, it is an afternoon.
  2. Jump flooding costs ceil(log2(maxdim)) full-screen passes and nothing else. It is flat by construction.
  3. Cut flood passes to the influence radius you actually need. Ten passes buys screen-wide reach nobody asked for.
  4. Expensive seeds bring the crossover down proportionally. Point seeds are the cheapest case.
  5. Jump flooding is approximate and grid-quantized. Brute force is exact and continuous.
  6. Benchmark by reading back the target you actually wrote, and distrust a flat curve you did not earn.

All 61 notes How to use them Credits