When is migrating to WEBGL justified for heavy pixel-array work in p5.js?
0 reputation · 24 Feb 2020, 13:35 UTC
0 reputation · 24 Feb 2020, 13:35 UTC
Goal: Determine whether the default 2D renderer’s loadPixels()/pixels[] workflow is causing a measurable draw‑loop bottleneck before attempting any optimization, using frameRate() to monitor FPS and timing the pixel‑array access versus other draw() logic.
Uncertainty: The decision to stay with the 2D canvas or migrate to the WEBGL renderer with a custom shader involves trade‑offs such as pixelDensity scaling, version‑specific pixels[] layout, and the overhead of setting up shaders, yet the library offers no clear rule for when the migration cost pays off.
Specific questions: Does measuring frameRate() inside draw() reliably reveal a pixel‑array bottleneck at typical canvas sizes? Which thresholds of frameRate drop or pixel‑array length justify switching to WEBGL and createShader()? How can I isolate the pixel‑array work from other calculations to confirm the source of slowdown without adding profiling overhead?
35275 reputation · 24 Feb 2020, 21:24 UTC
Migrate only after measurement shows the CPU-side pixel loop (loadPixels() / pixels[] / updatePixels()) is eating most of your frame budget. A practical threshold: if the timed pixel section exceeds roughly half of the 16.6 ms frame budget (about 8 ms) at your target canvas size, and the per-pixel math doesn't depend on sequential CPU state, a fragment shader via createShader() will almost always pay off. Below that, optimizing the 2D loop is cheaper than migrating.
frameRate() is a usable but coarse signal — it returns a smoothed average, so a single reading proves nothing. Use it as a toggle test: run draw() with the pixel work enabled, then comment it out, and compare. A drop from ~60 to below ~30 FPS when the loop runs strongly implicates it.
For a precise number without adding profiling overhead, time just the pixel section:
let acc = 0, n = 0;
function draw() {
const t0 = performance.now();
loadPixels();
// ... your per-pixel math ...
updatePixels();
acc += performance.now() - t0;
if (++n === 60) { console.log(acc / 60, 'ms avg'); acc = 0; n = 0; }
}Also run loadPixels() + updatePixels() with an empty loop. That separates the copy overhead (which scales with resolution) from your actual per-pixel math. If the empty-loop version is already slow, the problem is the canvas-to-CPU copy itself — which WEBGL eliminates entirely.
Before concluding anything, log pixelDensity() and pixels.length. The array length is width × height × 4 × density², so on a retina display (density 2) you're iterating 4× more data than the canvas dimensions suggest. Note that pixels[] layout and density behavior have shifted across p5.js versions — code written assuming density 1 can silently process the wrong region on high-DPI displays. Verify against your installed version.
A fragment shader runs your per-pixel math on every texel in parallel on the GPU, turning O(width×height) CPU work into effectively constant per-frame cost. It suits color transforms, noise, filters, and texture-feedback effects. The costs are real: rewriting the algorithm in GLSL, passing inputs as uniforms, the centered/flipped WEBGL coordinate system, and losing some 2D APIs.
Do not migrate if your algorithm needs random access to arbitrary pixels, sequential CPU logic, or per-frame readback of results into JS — GPU readback (get() and friends) is slow enough to erase the gains. Keep results on the GPU.
frameRate() drops sharply when it runs.performance.now() over ~60 frames; is it over ~8 ms?pixelDensity() and pixels.length; is the array hundreds of thousands of pixels or more?Four yeses: prototype a minimal fragment shader on a small canvas and compare FPS at your real target resolution. Any no: stay in 2D and optimize the loop (typed-array math, avoiding per-frame allocations, lowering pixelDensity(1)) instead.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.