Launch OpenGPEX

Architecture Overview

A high-level guide to the OpenGPEX v2 engine architecture — a full WebGPU rewrite that replaces the v1 Canvas2D/wasm-vips rendering stack with a single hardware-accelerated pipeline for both live preview and file export.


Design Mission

OpenGPEX 1.0 established its feature baseline on React + Worker multithreading + Canvas2D / wasm-vips. v2.0 is a "power rewrite": WebGPU is the only core rendering substrate. All pixel compositing, color matrix conversion, filter convolution, and brush computation run directly on the GPU, targeting desktop-native responsiveness (4K / 8K at high frame rates) with end-to-end physically accurate color.

The v1 constraints this removes:

v1 Bottleneck (Canvas2D / vips) Root Cause v2 Solution (WebGPU)
8-bit color depth ceiling Canvas2D forces 8-bit RGBA pixel storage Native rgba16float / rgba32float GPU textures; HDR and wide gamut preserved end to end
Split render/export pipelines Live preview via 8-bit gamma Canvas2D; 16-bit export via vips puzzle-assembly A single RenderGraph serves both screen and file export — "preview ≠ export" is structurally impossible
Compositing performance wall CPU soft rasterization + cross-thread ArrayBuffer copies Parallel fragment-shader hardware blending; 100-layer 4K composites in low single-digit milliseconds
Dual-track filter previews CPU too slow on large images, forced Track A (downscaled) + Track B (deferred Worker) Shader uniforms / LUT sampling: single track, sub-millisecond 4K adjustment previews
Gamma-space blend darkening, clip() seam hacks Canvas2D blends in gamma space; ctx.clip() leaves 0.75px AA gaps Native linear-light compositing; shader-exact alpha masking, no geometric shrink patches
Imperative renderer state machine Implicit timing + interaction flags leaking into draw calls Declarative Scene descriptors compiled into a cached, replayable RenderGraph

Two Chains, One Pipeline

v2 has exactly two execution chains that meet only where source pixels are obtained:

┌─────────────────────────────────────────────────────────────────────┐
│  UI / State layer (React, declarative)                               │
│  stage/ viewport · plugin drawers · core/state (reducer + fast track)│
└───────────────┬─────────────────────────────────┬───────────────────┘
                │                                 │
     ┌──────────▼─────────────┐        ┌──────────▼──────────────────┐
     │ RENDER CHAIN (main      │        │ IO CHAIN (cross-Worker)     │
     │ thread, pure GPU)       │        │                             │
     │                         │        │ PixelFacade → dispatchers   │
     │ SceneAssembler          │        │ → WorkerBridge → Engine     │
     │   → Scene (pure data)   │        │ Worker (decode, resample,   │
     │ WebGpuEngine.render()   │        │ pixel extraction,           │
     │   → SceneCompiler       │        │ histogram — no vips)        │
     │   → RenderGraph         │        │                             │
     │   → passes              │        │ core/files transcode        │
     │   → final sink          │        │ Workers (shared vips        │
     │                         │        │ singleton + on-demand       │
     └──────────┬──────────────┘        │ libraw / resvg / gs / avif) │
                │                       └─────────────────────────────┘
      ┌─────────┴──────────┐
      ▼                    ▼
  Swapchain             Readback
  (display-p3,      (copyTextureToBuffer
   HDR tone map)     → f32 Float32Array)

Key points:

  • Rendering and export share the same RenderGraph; only the final sink differs (swapchain vs. readback buffer). This is the One Pipeline invariant.
  • SceneAssembler is the only module that straddles both chains — it requests source pixels from the IO chain — which is why it lives in the assembly layer (core/engine/pipeline/scene/), not the GPU kernel.
  • The two chains touch only indirectly, through shared caches (SourceBitmapCache / HighDepthTextureCache).

Module Layering

Dependencies flow strictly top-down as an acyclic DAG. External consumers import only through top-level barrels; deep imports are forbidden.

Directory Layer Responsibility
core/engine/color L1 Pure color math foundation: TRC, matrices, LUTs, cube LUT, float16 packing
core/engine/sources / raster L0 / L0.5 Color sources, source bitmap cache; CPU/Canvas2D rasterization
core/engine/pipeline L2 IEngine · WebGpuEngine · RenderGraph, SceneCompiler, passes; Scene + SceneAssembler
core/engine/gpu L3 Device + capabilities, TexturePool, WGSL shader library (TypeScript-inlined constants)
core/engine/dispatch / worker — Composite / Export / Image dispatchers; Engine Worker (decode / resample / extract-pixels / histogram — no vips, no compositor)
core/files — Format handlers, metadata, ICC, declarative ingest/egest decision engine, self-owned transcode Workers
core/storage/asset — Asset SSOT: three-key IndexedDB topology, memory pool, protected GC
core/state · stage/ — Reducer store + volatile fast track; canvas mount, rAF render loop, interaction

Outer shells (workspace/ layout, plugins/*) depend inward only, via stable service contexts.


Core Architectural Aspects

# Aspect Design Choice
1 State & Data Flow Normalized store + volatile fast track + JSON Patch TimeTravel, sharded per-frame persistence
2 Rendering & Engine WebGPU render graph, compose-once/view-many, tiled density-band compositing, f32 readback
3 Color Pipeline Linear Display-P3 working space, gamut matrices on GPU, no-clamp out-of-gamut preservation
4 Plugin System Metadata-driven IoC registry with slot-based UI injection (carried over from v1, extended)
5 File Format & Storage Declarative ingest/egest decisions, three-key asset store, .gpex ZIP container
6 Cloud Integration Explicit user-driven sync plus anonymous update-check telemetry

Key Design Decisions

1. One Pipeline (Single Source of Truth)

One WGSL / RenderGraph serves both screen presentation and file export; the baked pixels are bit-identical to the on-screen preview. Readback returns premultiplied linear Display-P3 Float32Array (f32), which acts as the common superset for both terminal encodings (view shader and export encode).

2. Declarative First

The UI/state layer only produces pure data descriptors — layer JSON, stroke point streams, adjustment parameters. The engine compiles them into GPU commands; side effects are contained entirely inside the engine, making frames cacheable and replayable.

3. Fixed Working Color Space

All compositing happens in Linear Display-P3 at rgba16float, regardless of source encoding. Source gamuts are aligned per-sample in the fragment shader via 3×3 matrices, with no clamping — out-of-gamut values survive as negative / >1.0 floats and can be recovered losslessly on export.

4. Depth Bifurcation with a Single Convergence Point

Storage honors each layer's true source bit depth (8-bit sources stay rgba8unorm, 16/32-bit sources become float textures); all math converges at rgba16float linear light; export depth is decided independently by the document. Three invariants (store / compute / export) keep this tractable — see Rendering & Engine.

5. Zero-Compromise Fallback

v2 ships no Canvas2D degradation patches. Environments without WebGPU are directed to the v1.x LTS maintenance line instead of silently losing fidelity.


Comparison with v1

Aspect OpenGPEX v1 OpenGPEX v2
Render substrate Canvas2D + wasm-vips WebGPU only (declarative render graph)
Bit depth 8-bit compositing; 16-bit via vips export 8/16/32-bit storage, float16 compute, f32 readback
Working color space sRGB / Display-P3 gamma, import-time one-shot conversion Linear Display-P3, per-sample GPU conversion, out-of-gamut preserved
Preview vs export Two backends, isomorphic painter contract Bit-identical by construction (one RenderGraph, two sinks)
Wide-gamut 8-bit export Forced 16-bit upscale or P3 clamp Direct raw-8 channel, byte-exact round trip
Filters CPU Worker, dual-track preview Compute shaders, single-track real-time
Undo/state JSON Patches on a single store Same patch model; per-frame sharded persistence

Last updated: 2026-10-12