GIMP's GEGL Pipeline: Architecture, Trust Boundaries, and Operational Reality
Architecture note on GIMP's GEGL pipeline: DAG-based pull model for constant memory, PDB trust boundary, operational checks for tile cache and color fidelity, failure modes (ICC clipping, OOM, ABI breaks) triggering redesign toward GPU compute or scene-referred compositing.
18 Jan 2026, 05:57 UTC

Requirements Driving the Design
GIMP 2.10+ must handle images from 8-bit sRGB JPEGs to 32-bit floating-point EXR files in wide-gamut spaces like Rec.2020 or ACES, while supporting non-destructive filter layers that don't duplicate full-resolution buffers at each step. The legacy pixel-iterator model (GIMP 2.8 and earlier) copied entire buffers for every operation, making 16-bit and 32-bit workflows impractical on typical workstations. The requirement set therefore centers on three constraints: constant memory footprint regardless of image dimensions, bit-depth and color-space agnosticism, and a plug-in boundary that prevents third-party code from corrupting core state.
Smallest Suitable Design: Graph-Based Pull Evaluation
GEGL (Generic Graphics Library) models each operation as a node in a directed acyclic graph (DAG). An image is not a buffer but a root node that produces tiles on demand. The scheduler pulls 256×256 pixel tiles (configurable) from the output node, which recursively requests required input tiles from upstream nodes. This pull model means a 100-megapixel image at 32-bit float (4 bytes × 4 channels × 100M ≈ 1.6 GB) never exists in RAM as a single allocation; only the active tile set lives in the cache.
Each node declares its input/output color space (via babl formats) and required region of interest. The scheduler fuses adjacent point operations (e.g., curves + levels) into a single kernel when possible, reducing tile passes. Auto-threading (GEGL 0.4+) partitions tiles across CPU cores without plug-in awareness.
Trust and Data Boundaries: The PDB Contract
The Procedural Database (PDB) is the only supported interface between GIMP core and plug-ins (C, Python-Fu, Script-Fu, Lua). Plug-ins receive GeglNode references and tile iterators (gegl_node_blit, gegl_buffer_iterator_new), never raw GeglBuffer* pointers or pixel arrays. This boundary enforces:
- Memory safety: plug-ins cannot dereference freed tiles or write outside their negotiated ROI.
- Version isolation: core can change tile layout, thread pooling, or cache eviction without breaking compiled plug-ins, provided the PDB opcode signatures remain stable.
- Semantic opacity: a plug-in calling
gegl:gaussian-blurtreats it as a black box; the core may replace the implementation (SIMD, OpenCL, Vulkan) without plug-in recompilation.
The PDB does not enforce semantic versioning. An operation renamed from gegl:gaussian-blur to gegl:blur-gaussian (hypothetical) or a property type changed from float to double will cause runtime failures in existing plug-ins. This is a known ABI fragility between GIMP 2.10 (GEGL 0.4) and 3.0 (GEGL 0.6).
Operational Verification: Bounding Memory and Confirming Fidelity
Tile Cache Sizing
GIMP's tile cache lives in Edit → Preferences → System Resources → Tile Cache Size. A safe starting point is 10% of physical RAM. For a 32 GB workstation, set 3.2 GB. The cache uses LRU eviction with priority for dirty (modified) tiles.
Verification procedure:
- Set Tile Cache Size to 10% of RAM.
- Open a 10,000 × 10,000 pixel, 32-bit float RGB image (≈ 1.6 GB uncompressed).
- Apply a live filter: Filters → Blur → Gaussian Blur, enable Show Preview.
- Monitor RSS with
top -p $(pidof gimp)(Linux) or Activity Monitor (macOS). Memory should stabilize near the cache limit + ~200 MB overhead for UI, undo stack, and babl tables.
If RSS exceeds cache + 500 MB consistently, check for plug-ins that allocate private buffers (common in older Python-Fu scripts using gimp.pdb.gimp_drawable_get_pixel_rgn).
Color Management Fidelity Check
Silent clipping occurs when the display path (8-bit sRGB) cannot represent the working space (e.g., Rec.2020 linear float). Mitigation:
- Enable Edit → Preferences → Color Management → Enable color management.
- Set Display Filter → Use display profile to your calibrated monitor ICC.
- Import a known-reference wide-gamut test image (e.g., a Rec.2020 gradient from color.org).
- Enable View → Soft Proof and compare values in a colorimeter-verified viewer (DisplayCAL, HCFR).
If soft-proof values deviate > 2 ΔE from reference, verify babl was built with the same ICC database as the display profile. Custom ICC profiles require rebuilding babl or using run-time babl:icc-load (slower, not thread-safe in GEGL < 0.6).
Programmatic Graph Sanity Test
A minimal Python-Fu script confirms the PDB-to-GEGL bridge works without segfaults:
#!/usr/bin/env python3
# gimp-gegl-smoke.py
from gimpfu import *
import gegl
def test_gegl_graph():
# Create a 100x100 RGB float buffer
buf = gegl.Buffer.new(
gegl.Rectangle.new(0, 0, 100, 100),
babl.format("R'G'B' float")
)
# Fill with gradient
for y in range(100):
for x in range(100):
buf.set(x, y, 1, [x/99.0, y/99.0, 0.5])
# Build graph: buffer -> gaussian-blur -> output
graph = gegl.Node()
src = graph.create_child("gegl:buffer-source", buffer=buf)
blur = graph.create_child("gegl:gaussian-blur", std_dev_x=2.0, std_dev_y=2.0)
src.connect_to("output", blur, "input")
out = gegl.Buffer.new(
gegl.Rectangle.new(0, 0, 100, 100),
babl.format("R'G'B' float")
)
sink = graph.create_child("gegl:write-buffer", buffer=out)
blur.connect_to("output", sink, "input")
sink.process(gegl.Rectangle.new(0, 0, 100, 100))
# Spot-check center pixel (should be ~0.5, 0.5, 0.5 after blur)
px = out.get(50, 50, 1, 3)
print(f"Center pixel: {px}")
assert all(abs(v - 0.5) < 0.05 for v in px), "Blurred gradient mismatch"
print("OK")
register(
"python-fu-gegl-smoke",
"GEGL graph smoke test",
"", "", "", "2026",
"/Filters/Test/GEGL Smoke Test",
"",
[],
[],
test_gegl_graph
)
main()
Run via Filters → Test → GEGL Smoke Test. No output window appears; check Filters → Python-Fu → Console for "OK" or traceback.
Failure Modes
ICC Profile Mismatch → Silent Clipping
When the working space (e.g., Rec.2020 linear) exceeds the display profile (sRGB), babl converts to the display space for on-screen rendering. If Use display profile is disabled, the conversion uses a built-in sRGB assumption, clipping out-of-gamut data without warning. The pixel data in the GEGL buffer remains intact; only the display path is wrong. This is a display-referred legacy assumption that conflicts with scene-referred workflows.
OOM on 32-bit Builds or Undersized Cache
The DAG design assumes a 64-bit address space. On 32-bit builds (rare but possible on older Linux distros), a 200 MP image with a 1 GB tile cache fragments the address space, triggering OOM kills during cache eviction. Even on 64-bit, setting Tile Cache Size > 75% of RAM starves the OS page cache, causing thrashing. The eviction algorithm is non-deterministic under pressure: two identical runs can produce different tile-recompute patterns, making performance profiling irreproducible.
Plug-in ABI Breakage Across GEGL Versions
A plug-in compiled against GEGL 0.4 (GIMP 2.10) calling gegl:noise-hsv with property holdness (float) will crash on GEGL 0.6 (GIMP 3.0) where the op is renamed gegl:noise-hsv-v2 and holdness becomes double. The PDB returns a generic "procedure not found" or "type mismatch" error with no migration path. Mitigation: vendor GEGL op names and property types in plug-in code, or use gegl_node_new_childv with GParamSpec introspection at runtime.
Conditions That Would Trigger a Redesign
GPU-Backed Compute Migration
Current GEGL ops are pure CPU. An OpenCL/Vulkan backend would require explicit synchronization boundaries in the DAG: each GPU op needs a clEnqueueAcquireGLObjects / vkQueueSubmit fence before the next CPU op can pull its output tiles. The current pull model has no concept of "this tile is on GPU, wait for fence." A redesign would introduce execution contexts per node and a scheduler that inserts barrier nodes automatically.
Scene-Referred Linear-Light Compositing as Default
GIMP 2.10 composites in the display-referred space (sRGB with gamma). A shift to scene-referred (linear light, unbounded) as the default compositing space would invert the color pipeline: legacy plug-ins expecting gamma-encoded input would produce incorrect results. This would require a compat layer that auto-inserts gegl:color-space conversion nodes at plug-in boundaries, plus a major version bump (3.0 → 4.0) with explicit opt-in for old behavior.
Tile Size Re-evaluation for HDR/Deep Compositing
256×256 tiles are optimal for 8/16-bit CPU caches. For 32-bit float HDR with 4+ channels (RGBA + depth + normals), tile memory per node rises to 256×256×4×8 = 2 MB. A 10-node graph needs 20 MB just for active tiles, stressing L3 cache. A redesign might adopt variable tile sizes (64×64 for high-bit-depth, 512×512 for 8-bit) or a tile-pool allocator with NUMA awareness.
Quick Reference: Version Assumptions and Verification Commands
| Component | GIMP 2.10.x | GIMP 2.99/3.0-dev |
|---|---|---|
| GEGL version | ≥ 0.4.30 | ≥ 0.6.0 |
| babl version | ≥ 0.1.70 | ≥ 0.1.90 |
| Auto-threading | Opt-in (GEGL_THREADS=1) | Default on |
| Python-Fu | Python 2 or 3 | Python 3 only |
Verify at runtime:
gimp --version
gegl --version
babl --version
Mismatched versions (e.g., GIMP 2.10.38 with GEGL 0.6.2) indicate a distro packaging error and will cause PDB crashes.
Limitations of This Analysis
The research reflects GEGL 0.4/0.6 semantics as of late 2024. GIMP 3.0 final may adjust op names, property types, or threading defaults. The Python-Fu smoke test uses gegl:gaussian-blur which exists in both versions but may accept different property sets. The tile cache stress test (64 MB cache, 200 MP image, 10 live filters) is a heuristic; actual RSS depends on window manager, compositor, and undo history depth. Always verify on target hardware before committing to a production workflow.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.