Launch OpenGPEX

Rendering & Engine

How OpenGPEX renders images at 4K+ resolution without freezing the browser — with two distinct rendering paths (screen preview and file export), a unified single-path compositor, Worker-based pixel computation, and an isomorphic paint function shared across all paths.


Dual Rendering Paths

OpenGPEX has two fundamentally different rendering chains that share a common isomorphic painter:

                    ┌──────── User ────────┐
                    │                      │
          (drag/slider/canvas)       (click download)
                    │                      │
                    ▼                      ▼
     ┌────────────────────┐    ┌────────────────────┐
     │ Path A: Screen Live │    │ Path B: File Export │
     │  (Editor Preview)   │    │  (Download/Save)   │
     └────────┬───────────┘    └────────┬───────────┘
              │                         │
              ▼                         ▼
     Canvas2dEngine (Main Thread)   compositeFrame(frame, roi, {precision:8})
     rAF → StageComposer.render     → Canvas2dBackend (Engine Worker)
              │                         │
              ▼                         ▼
     painter.drawLayerInstance      result.toBlob() → files.encode → download
     (same code shared) ◄──────── (compositor also uses painter)

Screen Live Rendering (Path A)

The real-time preview pipeline runs entirely on the main thread via requestAnimationFrame:

User interaction → CanvasStage.tsx (rAF) → StageComposer.render()
    → Canvas2dEngine.beginFrame/pushCommand/flush
        → drawLayerDirect (per layer)
            → painter.drawLayerInstance (atomic leaf)

Offscreen Composition (Blend Isolation)

When a layer requires blend isolation (bitmap masks, non-standard blend modes, blur), the engine routes through an offscreen canvas:

Condition Triggers Offscreen Reason
Bitmap mask exists Needs destination-in/out isolation
Blur > 0 Neighborhood operator can't apply per-tile
BlendMode + tiled layer Prevents tile-seam double-blending
BlendMode + linear TRC Requires manual linear-light blending via ImageData
BlendMode + non-tiled + gamma Single drawImage has no seam issues

Linear-Light Blending

For 16-bit documents (Frame.trc = 'linear'), the engine performs physically-correct linear-light mixing via manual pixel operations — matching Photoshop CC+ behavior and ensuring WYSIWYG consistency with the 16-bit export path.


Tiled Rendering

Traditional web editors load entire images as a single bitmap (causing OOM at ~8K). OpenGPEX splits images into 256×256 pixel tiles and only loads those visible in the current viewport.

Property Value
Tile size 256×256 px
Active tiles in viewport 40–60 (constant, regardless of image size)
Cache limit 500 tiles (LRU, auto-eviction via bitmap.close())
Max supported resolution 4,096×4,096+
Transfer method Transferable ImageBitmap (zero-copy)

Mipmap Pyramid

The Worker pre-generates multiple resolution levels (100%, 50%, 25%, 12.5%). The engine selects the appropriate level based on camera zoom — ensuring no aliasing when zoomed out and sharp details when zoomed in.


File Export: Unified Single-Path Architecture (Path B)

All export/download operations use a single compositing path — compositeFrame() via Canvas2dBackend in the Engine Worker, followed by format encoding:

download command (commands.ts)
    │
    ▼
compositeFrame(frame, roi, { precision: 8 })
    │
    ▼
CompositorHandler → Canvas2dBackend (Engine Worker)
    → painter.drawLayerInstance (same code as screen path!)
    → OffscreenCanvas 8-bit composited result
    │
    ▼
result.toBlob()
    │
    ▼
files.encode(bitmap, format, opts)
    ├── PNG/JPG/WebP/BMP: browser-native convertToBlob
    ├── TIFF: vips-worker encodeTiff
    └── AVIF: vips-worker encodeAvif
    │
    ▼
pixels.utils.download(blob, filename)

Design Principles

  • Single compositing backend — All composition goes through Canvas2dBackend (8-bit). No routing decisions, no lane selection.
  • Extreme simplicity — The export command is just compositeFrame + files.encode + download. No strategy objects, no route resolution.
  • WebGPU upgrade path — When WebGPU lands, only the backend implementation changes (Canvas2dBackendWebGpuBackend). The export command, encoding pipeline, and painter remain identical. 16-bit compositing will be re-enabled natively via GPU float16 textures.

Compositing API

compositeFrame(frame, roi, opts)     ← Full frame composite with region-of-interest
flattenLayers(layers, frame, opts)   ← For merge/rasterize commands

Results include a unified WorkerResult (blob + bitmap + hash + tileMeta), enabling direct asset injection without extra decode round-trips.

Note: 16-bit export compositing (formerly Lane A/B via VipsBackend) has been removed in the current architecture. It will return when WebGPU enables native float16/float32 compositing without the complexity of a separate vips-based compositor.


Engine: Zero Main-Thread Pixel Computation

All pixel operations are dispatched through a thin PixelFacade to Workers via specialized dispatchers:

Dispatcher Responsibility
Composite Layer merging, blend modes, export composition
Filter Color adjustments (curves, levels, channel mixer)
Decode Image decoding + bitmap caching
Resample Image scaling/resampling
Rasterize Text/vector → bitmap conversion

Worker Architecture

Three physically isolated Workers, orchestrated by the main thread:

Worker Lifecycle Purpose
Engine Worker Singleton, always running Compositing, merge, rasterize, filter, TIFF/AVIF encode/decode + ICC (via lazy-loaded wasm-vips singleton)
Resvg Worker On-demand, terminate after use SVG → PNG rasterization
GS Worker On-demand, terminate after use EPS → PNG via Ghostscript

Note: wasm-vips (used for TIFF/AVIF encoding and ICC color conversion) runs as a lazy-loaded singleton inside the Engine Worker — not as a separate process. It initializes on first use and shares the Worker's lifetime.

Workers never communicate directly — all cross-worker coordination flows through the main thread.

Precision Model

Precision Backend Status
8-bit Canvas2dBackend (OffscreenCanvas, GPU-accelerated) ✅ Active — all compositing
16-bit (planned: WebGpuBackend, float16 textures) ⏸ Paused — awaiting WebGPU
32-bit (planned: WebGpuBackend, float32 textures) ⏸ Paused — awaiting WebGPU

Currently all composition runs through a single Canvas2dBackend at 8-bit precision. The architecture preserves a precision field in the composite request for future WebGPU activation.


Isomorphic Painter

A single pure function drawLayerInstance() is shared between:

  • The main-thread Canvas2dEngine (screen preview)
  • The Engine Worker's Canvas2dBackend (file export compositing)

This guarantees pixel-perfect consistency between what users see on screen and what gets exported.

Painter responsibilities (in execution order):

  1. setTransform(matrix) — World matrix
  2. ctx.filter = getAdjustmentsData() — Brightness/contrast/saturation via CSS filter
  3. globalAlpha — Opacity × fill
  4. globalCompositeOperation — Blend mode
  5. applyClipSequence() — Vector masks (with feathered clip support)
  6. drawLayerContent() — fillRect / fillText / drawImage (tiles or single source)

Hard constraint: painter.ts may only import pure types and geometry utilities — never Worker bridges, caches, or anything that would pull Worker module graphs into the main thread bundle.


Three Coordinate Spaces

Space Origin Use Case
Screen Browser viewport top-left Mouse events, DOM positioning
World Canvas (0,0) at zoom=1 Layer positioning, canvas math
Local Layer's own top-left corner Mask coordinates, brush strokes

Transform chain: Screen ↔ (camera matrix) ↔ World ↔ (layer matrix) ↔ Local

The GeometryService provides coordinate conversion methods used throughout the interaction and rendering systems.


WASM Modules

Module Purpose Loading
wasm-vips TIFF/HEIC/AVIF encode/decode + ICC color conversion Lazy, single shared instance
libraw-wasm RAW format decoding (CR2/NEF/ARW/DNG) Lazy, on first import
resvg-wasm SVG rasterization Lazy, on first import

All WASM modules run inside the Worker, never blocking the main thread.


Last updated: 2026-08-08