Launch OpenGPEX

OpenGPEX 1.x documentation (legacy editor).

v2 Docs

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 512 tiles (LRU, auto-eviction via bitmap.close())
Max supported resolution 4,096×4,096+
Transfer method Transferable ImageBitmap (zero-copy)

Mipmap Pyramid

The Worker builds a halving pyramid per asset on demand (Level 0 = full resolution, Level N = 2^N downscale), generated down to the 256px tile size and cached per asset. 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 (Canvas2dBackend → WebGpuBackend). 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
compositeLayers(layers, frame, roi, opts) ← For merge/rasterize commands (union bounds)

Results are returned as a unified CompositeResult (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)
Image Image decoding + bitmap caching, and resampling/scaling (former Decode/Resample dispatchers, consolidated)
Rasterize Text/vector → bitmap conversion
File I/O TIFF decode/encode, AVIF encode, ICC conversion (via wasm-vips)

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. globalAlpha — Opacity × fill
  3. globalCompositeOperation — Blend mode
  4. applyClipSequence() — Vector masks (with feathered clip support via offscreen compositing)
  5. drawLayerContent() — fillRect / fillText / drawImage (tiles or single source)

Adjustments (brightness/contrast/saturation) are not applied inside the painter — they are pre-baked into the source bitmap by the FilterFastTrack pipeline (synchronous LUT/matrix preview + async Worker full-res cache) before drawLayerInstance runs; blur is handled by the engine's offscreen composite path.

Hard constraint: painter2d.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