Caching Static Drawings with PGraphics in Processing to Boost Frame Rate
Use PGraphics to draw static content once into an off-screen buffer and blit it each frame with image(), turning expensive per-frame redraws into a single cheap texture copy.
13 Jul 2025, 22:04 UTC

If your sketch redraws the same complex background every frame — hundreds of shapes, a grid, a textured backdrop — most of your frame budget is spent painting pixels that never change. The fix is to draw that content once into an off-screen buffer with PGraphics and then blit the finished image each frame with image(). Sketches that struggle at 20 FPS while redrawing static geometry often run at a steady 60 FPS after this change, because per-frame work drops to a single texture copy.
What PGraphics actually is
PGraphics is an off-screen graphics surface with the same drawing API as your main sketch window. Anything you can call in draw() — ellipse(), line(), text(), image() — works on a PGraphics object. The key difference is that nothing appears on screen until you draw the buffer itself onto the main canvas.
You create one with createGraphics(width, height, renderer). The renderer should match your sketch's renderer (P2D or P3D for GPU-accelerated sketches, or the default Java2D renderer) so the buffer shares the same drawing pipeline. With P2D/P3D, the buffer lives on the GPU as a texture, which is why displaying it with image() is so cheap.
A worked example: cached background
This sketch draws an expensive static starfield into a buffer once, then animates a moving circle on top. Run it in the Processing IDE (tested against Processing 4.x behavior; the API is stable back to Processing 2).
PGraphics bg;
void setup() {
size(800, 600, P2D);
bg = createGraphics(width, height, P2D);
bg.beginDraw();
bg.background(15, 18, 30);
bg.stroke(255);
for (int i = 0; i < 2000; i++) {
float x = random(width);
float y = random(height);
bg.strokeWeight(random(1, 3));
bg.point(x, y);
}
bg.endDraw();
}
void draw() {
image(bg, 0, 0); // one texture copy, not 2000 points
fill(255, 120, 40);
noStroke();
ellipse(mouseX, mouseY, 60, 60);
}Two rules make this work:
- Every drawing call on the buffer must sit between
beginDraw()andendDraw(). These calls set up and tear down the render state. Forgetting them is the most common cause of a mysteriously blank buffer. - Draw the buffer with
image(bg, 0, 0)indraw(). The buffer persists between frames, so you only pay the drawing cost once.
Verifying the speedup
Add surface.setTitle("FPS: " + nfc(frameRate, 1)); at the end of draw(). Then compare against a variant that moves the starfield loop directly into draw() without a buffer. The buffered version should show a higher and more stable frame rate; the unbuffered version typically drops as the shape count grows. If you see no difference, check that your static content is actually expensive — caching a dozen rectangles gains nothing measurable.
Updating only what changed
The buffer doesn't have to be frozen forever. A common pattern is to redraw the buffer only when the underlying data changes:
boolean bgDirty = true;
void draw() {
if (bgDirty) {
renderBackground(); // beginDraw/endDraw around the drawing
bgDirty = false;
}
image(bg, 0, 0);
// ... animated foreground ...
}Set bgDirty = true from whatever event invalidates the background — a key press, a new data load, a settings change. This gives you correct output without paying the redraw cost every frame.
Limits and common mistakes
Memory scales with buffer size
Each pixel costs 4 bytes (RGBA), so an 800×600 buffer uses roughly 1.9 MB, but a 4000×3000 buffer uses about 48 MB. On machines with limited RAM or a small Java heap, several large buffers can trigger an OutOfMemoryError. You can raise the heap in Processing's Preferences ("Increase maximum available memory"), but the better fix is usually to size buffers to what you actually display. To confirm memory is the constraint, double the buffer dimensions and watch for the error, then shrink back.
Resizing the sketch does not resize the buffer
If your sketch uses surface.setResizable(true), the buffer keeps its original dimensions when the window changes, producing stretched or cropped output. Detect the size change and recreate the buffer:
void draw() {
if (bg == null || bg.width != width || bg.height != height) {
bg = createGraphics(width, height, P2D);
bgDirty = true;
}
// ...
}Recreating a buffer every frame defeats the purpose, so guard it with a real size check, not just a redraw flag.
PGraphics is not thread-safe
With the OpenGL renderers, the buffer shares the GL context with the main sketch. Drawing to it from another thread — for example, from a Thread loading data in the background — can crash the sketch or corrupt the display. Do all buffer drawing on the animation thread; have worker threads only prepare data and set a flag that draw() picks up.
Style state leaks between buffer and canvas
PGraphics keeps its own style stack (fill, stroke, text size), separate from the main canvas. Setting bg.fill(255, 0, 0) does not change the canvas fill, and vice versa. This is usually convenient, but it surprises people who set a font or color once and expect it to apply everywhere. Configure styles explicitly inside each beginDraw()/endDraw() block.
Blank output checklist
If image(bg, 0, 0) shows nothing, check in order: (1) beginDraw() was called before any drawing, (2) endDraw() was called afterward, (3) you didn't draw with a fully transparent fill or a background matching the canvas, (4) the buffer wasn't recreated after your drawing ran, wiping it. Printing bg.width and a sample pixel via bg.get(x, y) after endDraw() narrows it down quickly.
When not to bother
If your whole scene changes every frame, a cache buys nothing and costs memory. PGraphics pays off when a meaningful share of the frame is static or changes rarely — backgrounds, HUD chrome, pre-rendered text, layered compositions where only one layer animates. Profile first with the frame-rate title trick above, then cache the expensive, unchanging part.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.