Using PGraphics for Off-Screen Buffers in Processing: Layered Drawing and Performance Gains
PGraphics lets you cache static drawing layers in an off-screen buffer, then composite them each frame with a single image() call. This guide shows a complete worked example, explains memory limits, and lists the common mistakes that cause crashes or visual artifacts.
08 Apr 2026, 04:26 UTC

The Problem and the Takeaway
When a Processing sketch redraws the same static background every frame — grids, maps, UI panels, or procedural scenery — the CPU and GPU waste cycles recomputing pixels that never change. PGraphics solves this by giving you an off-screen graphics buffer: draw the static layer once, then composite it each frame with a single image() call. The result is a measurable frame-rate improvement, especially on integrated graphics or when the static layer contains thousands of draw calls.
This article explains how to create and use a PGraphics buffer, shows a complete worked example, and covers the memory limits and common mistakes that turn a performance win into a crash or visual glitch.
How PGraphics Works
A PGraphics object is a separate drawing surface that lives in GPU or CPU memory, depending on the renderer you choose. You create it with createGraphics(width, height, renderer), where renderer can be P2D (OpenGL 2D), P3D (OpenGL 3D), or JAVA2D (software). Once created, you draw into it exactly like the main canvas — but you must wrap drawing commands between beginDraw() and endDraw() so the renderer knows when to flush commands to the buffer.
After endDraw(), the buffer is a texture you can draw anywhere with image(pg, x, y). Because the buffer is already rasterized, the compositor only needs to blit one quad per frame, no matter how complex the original scene was.
Worked Example: Static Background, Animated Foreground
The sketch below creates an 800×600 window, builds a PGraphics buffer filled with 100 random ellipses once in setup(), then draws a moving circle on top in draw(). The ellipses never redraw; only the foreground circle updates.
PGraphics bg;
void setup() {
size(800, 600, P2D);
bg = createGraphics(800, 600, P2D);
bg.beginDraw();
bg.background(30);
for (int i = 0; i < 100; i++) {
bg.fill(random(255), random(255), random(255));
bg.noStroke();
bg.ellipse(random(width), random(height), 20, 20);
}
bg.endDraw();
}
void draw() {
image(bg, 0, 0); // blit the cached background
fill(255, 200, 0);
noStroke();
ellipse(mouseX, mouseY, 40, 40); // animated foreground
}Run this in the Processing 4.x desktop IDE. The background ellipses stay fixed while the yellow circle follows the mouse smoothly. If you move the for loop into draw() without the buffer, frame rate drops noticeably on most machines once the ellipse count reaches a few hundred.
When to Update the Buffer
Only call beginDraw()…endDraw() again when the static layer actually changes — for example, when a map zoom level changes, a UI panel toggles, or a procedural terrain regenerates. Updating the buffer every frame defeats the purpose; you might as well draw directly to the main canvas.
If the buffer content must change partially (say, one tile in a tile map), you can call beginDraw(), draw only the changed region, then endDraw(). The rest of the buffer remains intact.
Memory Limits and Renderer Choice
Each PGraphics buffer allocates a texture the size of its pixel dimensions. An 800×600 P2D buffer consumes roughly 800 × 600 × 4 bytes ≈ 1.9 MB of VRAM. A 4K buffer (3840×2160) needs ~33 MB. On a typical laptop GPU with 2–4 GB VRAM, a handful of full-screen buffers is fine; dozens will trigger OutOfMemoryError or force the driver to swap to system RAM, causing severe stutter.
Choose the renderer deliberately:
- P2D — best for 2D off-screen buffers; hardware accelerated, supports
blendMode(),filter(), and shaders. - P3D — required if the buffer contains 3D geometry; heavier memory footprint.
- JAVA2D — software rasterizer; no GPU memory used, but much slower for large buffers. Useful for headless export or PDF/ SVG renderers that don't support OpenGL.
Do not mix renderers between the main canvas and the buffer unless you understand the coordinate-system implications (see Common Mistakes).
Common Mistakes
Forgetting beginDraw() / endDraw()
Drawing methods called on a PGraphics object outside that pair are ignored silently in some renderers, or throw an exception in others. Always wrap:
pg.beginDraw();
pg.background(0);
pg.line(0, 0, 100, 100);
pg.endDraw();Accumulating Frames by Not Clearing
If you update a buffer repeatedly without calling background() or clear() at the start of each update, previous drawings stack up. This is a frequent source of "ghost" artifacts. Treat every buffer update like a fresh frame:
pg.beginDraw();
pg.clear(); // or pg.background(...)
// draw new content
pg.endDraw();Coordinate System Mismatch
The buffer's coordinate system is independent of the main canvas. If you create a buffer at a different size than the window, or translate/rotate the buffer's matrix inside beginDraw()…endDraw(), the content will appear shifted or scaled when you image(pg, 0, 0). Keep buffer dimensions identical to the display area unless you intentionally want scaling.
Using Unsupported Renderers
PDF, SVG, and FX2D renderers cannot be used with createGraphics() for off-screen buffering in the same way. Attempting createGraphics(w, h, PDF, "file.pdf") produces a buffer that writes vector commands to a file, not a raster texture you can image() onto the screen. Stick to P2D, P3D, or JAVA2D for compositing workflows.
Practical Verification
To confirm the performance benefit in your own sketch:
- Add
println(frameRate)at the end ofdraw(). - Run the buffered version for 30 seconds; note the average FPS.
- Comment out the
image(bg, 0, 0)line and move the ellipse-drawing loop intodraw()(withoutbeginDraw()/endDraw()). - Run again under identical conditions; compare the averages.
On a 2021 MacBook Pro (M1 Pro, Processing 4.3), the buffered version holds ~60 FPS with 10,000 ellipses, while the unbuffered version drops to ~18 FPS. Your numbers will vary, but the buffered version should always be equal or faster once the static layer exceeds a few dozen draw calls.
When Not to Use PGraphics
- The static layer changes every frame (e.g., a particle system where every particle moves).
- You need per-pixel interaction with the static layer (hit-testing, pixel reading) —
get()on a PGraphics works but adds latency. - Memory is extremely constrained (embedded Linux board, browser export via p5.js where WebGL texture memory is limited).
Summary Checklist
- Create buffer once in
setup()with matching renderer. - Draw static content inside
beginDraw()…endDraw(). - Call
clear()orbackground()at the start of every buffer update. - Composite with
image(pg, x, y)indraw(). - Monitor VRAM usage if you allocate multiple large buffers.
- Verify FPS gain with a before/after test.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.