Pre-Render Static Layers with Processing PGraphics for Frame-Rate Gains
Move unchanging geometry, grids, and text into a PGraphics offscreen buffer during setup, then composite it with a single image() call each frame. This guide covers buffer creation, renderer matching, resize handling, memory limits, and verification steps.
07 Apr 2026, 01:15 UTC

Problem: Redrawing Unchanged Geometry Every Frame
In a typical Processing sketch, the draw() loop executes 60 times per second. If your scene contains background grids, decorative text, complex static shapes, or loaded images that never change, the CPU and GPU re-submit the same vertex and fill commands on every frame. This wastes fill-rate and keeps the Java thread busy with work that could be done once.
The fix is to render all static content into an offscreen PGraphics buffer during setup(), then composite that buffer with a single image() call inside draw(). The main loop then only processes animation, UI, or physics — work that actually changes each frame.
Prerequisites
- Processing 3.x or 4.x (tested on 4.3)
- Sketch using
P2DorP3Drenderer (the technique works withJAVA2Dbut gains are smaller) - Enough heap and GPU memory for an extra texture the size of your canvas (see Memory Considerations)
Procedure: Create, Populate, and Composite the Buffer
1. Declare and Allocate the Buffer
Create the PGraphics object as a global field so both setup() and draw() can reach it. Match the renderer to the main sketch to avoid pixel-format surprises.
PGraphics staticLayer;
void settings() {
size(1280, 720, P2D); // or P3D
}
2. Initialize in setup()
Call createGraphics() with the same width, height, and renderer. Then wrap all static drawing between beginDraw() and endDraw().
void setup() {
staticLayer = createGraphics(width, height, P2D);
staticLayer.beginDraw();
staticLayer.background(15, 15, 25);
// Example: expensive static grid
staticLayer.stroke(40, 40, 60);
staticLayer.strokeWeight(1);
for (int x = 0; x < width; x += 40) {
staticLayer.line(x, 0, x, height);
}
for (int y = 0; y < height; y += 40) {
staticLayer.line(0, y, width, y);
}
// Static text / logos
staticLayer.fill(200);
staticLayer.textSize(24);
staticLayer.textAlign(CENTER, CENTER);
staticLayer.text("PROCESSING SKETCH", width/2, 60);
staticLayer.endDraw();
}
Critical: Forgetting beginDraw() or endDraw() leaves the buffer blank or in an undefined state. The calls are mandatory — they bind and unbind the offscreen framebuffer.
3. Composite Once Per Frame in draw()
Draw the buffer first, then layer dynamic content on top. The buffer behaves like any PImage.
void draw() {
image(staticLayer, 0, 0); // single texture blit
// Dynamic layer — particles, UI, physics
float t = millis() * 0.001f;
fill(255, 120, 0, 200);
noStroke();
ellipse(width/2 + 200*sin(t), height/2, 60, 60);
}
4. Handle Window Resize (If Resizable)
If your sketch supports surface.setResizable(true), the buffer dimensions must track the canvas. Dispose the old buffer to release native GL resources before creating the new one.
void settings() {
size(1280, 720, P2D);
surface.setResizable(true);
}
void draw() {
if (staticLayer == null || staticLayer.width != width || staticLayer.height != height) {
recreateStaticLayer();
}
image(staticLayer, 0, 0);
// ... dynamic drawing
}
void recreateStaticLayer() {
if (staticLayer != null) {
staticLayer.dispose(); // releases GL texture
}
staticLayer = createGraphics(width, height, P2D);
staticLayer.beginDraw();
// repeat all static drawing commands here (factor into a helper)
drawStaticContent(staticLayer);
staticLayer.endDraw();
}
void drawStaticContent(PGraphics pg) {
pg.background(15, 15, 25);
pg.stroke(40, 40, 60);
pg.strokeWeight(1);
for (int x = 0; x < pg.width; x += 40) pg.line(x, 0, x, pg.height);
for (int y = 0; y < pg.height; y += 40) pg.line(0, y, pg.width, y);
pg.fill(200);
pg.textSize(24);
pg.textAlign(CENTER, CENTER);
pg.text("PROCESSING SKETCH", pg.width/2, 60);
}
Expected Checks: Verify the Optimization Works
- Visual stability: Run the sketch. The grid and title must not flicker, tear, or shift when the animated ellipse moves.
- Frame-rate delta: Add
println(frameRate);at the end ofdraw(). Compare the average before and after moving the grid into the buffer. On a typical integrated GPU, a 1280×720 grid of 1000+ lines drops per-frame CPU time from ~4 ms to <0.5 ms. - Buffer dimensions: Print
staticLayer.widthandstaticLayer.heightafter creation; they must equalwidth/height. - Resize behavior: Drag the window corner. The grid should re-draw once at the new size without stretching artifacts.
Memory Considerations and Limits
| Renderer | Buffer Storage | Typical 1920×1080 Cost | Risk |
|---|---|---|---|
| JAVA2D | Java heap (BufferedImage) | ~8 MB (ARGB) | Heap pressure, GC pauses |
| P2D | OpenGL texture (GPU VRAM) | ~8–16 MB | VRAM exhaustion on integrated graphics |
| P3D | OpenGL texture + depth/stencil | ~16–32 MB | Higher VRAM, context loss on some drivers |
If you allocate multiple large buffers (e.g., separate background, midground, UI layers), monitor Runtime.getRuntime().totalMemory() and the OS GPU memory metric. On a 2 GB integrated GPU, three 1920×1080 P3D buffers can trigger OutOfMemoryError or driver resets.
Mitigation: Downscale the buffer for distant background layers (render at 50% then upscale with image(buffer, 0, 0, width, height)), or reuse a single buffer by clearing and re-drawing only when the static scene actually changes.
Common Failure Modes and Recovery
| Symptom | Likely Cause | Fix |
|---|---|---|
| Blank buffer (transparent or black) | Missing beginDraw()/endDraw() or drawing before createGraphics() | Wrap all buffer drawing in beginDraw()…endDraw() after allocation |
| Color shift or wrong alpha | Renderer mismatch (e.g., sketch P3D, buffer P2D) | Pass the same renderer constant to createGraphics() |
| Stretched/blurry buffer after resize | Old buffer dimensions reused | Call dispose() and recreate in draw() when width/height change |
| Crash on second run (Presentation mode) | Buffer not disposed, native GL context leaked | Implement dispose() in exit() or stop() |
Rollback: Returning to Immediate-Mode Drawing
If the buffer approach introduces bugs you cannot resolve quickly, revert by deleting the PGraphics field, removing the image() call, and pasting the static drawing code directly back into draw(). No persistent state is modified outside the sketch — simply restart the sketch after editing.
When Not to Use This Pattern
- Static content is trivial (a few lines or one image) — the overhead of buffer management outweighs the savings.
- The "static" layer changes every few frames (e.g., a slowly scrolling map). In that case, partial redraw or a dirty-rectangle scheme is more appropriate.
- Target platform has severe VRAM limits (older Raspberry Pi, some Android devices) and you already use large textures elsewhere.
Summary Checklist
- [ ]
PGraphicsfield declared globally - [ ]
createGraphics(w, h, renderer)matches sketch renderer - [ ] All static drawing inside
beginDraw()/endDraw()insetup()(or helper) - [ ]
image(buffer, 0, 0)as first call indraw() - [ ] Resize handler disposes and recreates buffer
- [ ] Frame-rate measured before/after confirms improvement
- [ ]
dispose()called on sketch exit if long-running
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.