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.
SceneAssembleris 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