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


Last updated: 2026-06-14