Rendering Pipeline
OpenGPEX uses a Tiled Rendering architecture to handle images up to 16K resolution without Out-of-Memory crashes, combined with an isomorphic atomic painter that guarantees pixel-perfect consistency between preview and export.
Why Tiled Rendering?
Traditional web editors load the entire image as a single bitmap. This causes:
- OOM crashes — A 10,000×10,000 image uses ~400MB of GPU memory
- Main-thread blocking — Decoding large images freezes the UI
- Wasteful redraws — Every pan/zoom resubmits the entire bitmap
OpenGPEX solves all three by splitting images into 256×256 pixel tiles and only loading tiles visible in the current viewport.
Architecture
┌─────────────────────────────────┐
│ Main Thread │
│ │
│ StageComposer → Display List │
│ │ │
│ ▼ │
│ TileCache (LRU, 500 tiles max) │
│ │ (cache miss) │
│ ▼ │
│ WorkerBridge (Promise RPC) │
└───────┬─────────────────────────┘
│ postMessage
▼
┌─────────────────────────────────┐
│ Web Worker │
│ │
│ Decoder → Mipmap Generator │
│ │ │
│ ▼ │
│ Slicer (256px tiles) │
│ │ │
│ ▼ │
│ ImageBitmap (Transferable) │
└─────────────────────────────────┘
Key Components
| Module | Responsibility |
|---|---|
processor.worker.ts |
Background decoder, mipmap generator, tile slicer |
WorkerBridge.ts |
Promise-based RPC over postMessage |
TileCache.ts |
Main-thread LRU cache (auto-releases GPU memory via bitmap.close()) |
AssetStore.ts |
IndexedDB persistence for Blob + TileMetadata |
StageComposer.ts |
Viewport intersection → RenderCommand queue |
Canvas2dEngine.ts |
Consumes commands, paints visible tiles |
Mipmap Pyramid
The Worker pre-generates multiple resolution levels:
Level 0: 100% (original) — used when zoomed in
Level 1: 50% — used at moderate zoom
Level 2: 25% — used at overview zoom
Level 3: 12.5% — used for thumbnails
The engine selects the appropriate level based on the current camera zoom, ensuring:
- No aliasing when zoomed out (lower levels are pre-filtered)
- Sharp details when zoomed in (higher levels loaded on demand)
Isomorphic Atomic Painter
A single pure function drawLayerInstance() is shared between:
- The main-thread preview engine
- The Web Worker compositor (merge, export)
function drawLayerInstance(
ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
layer: Layer,
source: CanvasImageSource | ImageBitmap | null,
options?: PainterOptions
): void
This eliminates coordinate drift between what users see and what gets exported.
Mask Composition
Masks are applied during tile rendering using ClipDescriptor:
interface ClipDescriptor {
shape: LocalShape; // Rectangle, ellipse, or path
inverted: boolean; // Normal vs inverted mask
}
When a layer has many masks (>10), the engine triggers Mask Baking — an async Worker task that permanently composites masks into the tile bitmaps, restoring rendering performance.
Performance Characteristics
| Metric | Value |
|---|---|
| Active tiles in viewport | 40-60 (constant regardless of image size) |
| Tile size | 256×256 px |
| Cache limit | 500 tiles (auto-eviction) |
| Transfer method | Transferable ImageBitmap (zero-copy) |
| Max supported resolution | 16,384×16,384+ |
Next Steps
- Engine & Workers — PixelService facade and Worker proxy details
- Spatial Service — Coordinate space transforms
- Fast Track — How volatile state feeds the paint loop
Last updated: 2026-06-14