Launch OpenGPEX

Masks & Effects

OpenGPEX v2's non-destructive masking and color pipeline: GPU-evaluated bitmap and vector masks, a linear-light composite core, and perceptual-domain grading operators.


Non-Destructive Philosophy

All masks and grading are metadata. The composite signature covers masks, adjustments, filters, and asset epochs; the original asset pixels are never touched, and any effect can be toggled or removed losslessly.


Mask Types

Bitmap Masks (bmask)

Freehand raster alpha fields written by Eraser/Restore strokes. Records are split along two axes — family (inverted: Erase contributes α, Restore contributes 1−α) and AA state (hard) — so switching AA never rewrites historical strokes. Visibility folds through an order-independent pure function:

vis = max( prod(Erase αᵢ),  max(Restore (1 − αⱼ)) )

A dedicated Bmask Combine Pass (ping-pong compute shader, r8unorm single-channel textures, 4× VRAM savings vs RGBA) folds all records into one coverage texture before the main composite; the layer shader does a branch-free color.a *= mask.r. Erase strokes atomically "plug" active Restore records (source-over white) inside the same undo transaction.

Vector Masks (vmask)

Analytic geometry evaluated per fragment:

  • Analytic path — a single rect/ellipse with one feather radius: SDF signed distance + fwidth-based anti-aliasing, zero textures
  • Polygon path — arbitrary multi-ring polygons (lasso, wand, fragments): GPU compute rasterization into an exact-size cached texture keyed by mask dims plus a per-submask feather_inverted_aa_distBias digest over the layer-local rings — moving the layer never re-bakes

Coverage priority: feather > 0 → smoothstep; else antiAliased → 1px ramp; else binary pixel-center test. Hard edge is just feather = 0 with AA off.

Fragments

Cut-to-layer fragments route by edge carrier: logical fragments adjust visibleShape.featherPx, degraded fragments adjust the explicit vmask feather, and baked fragments (branch from selection, background removal) feather the bitmap alpha itself.


Composite Core

  • Working space is Linear Display-P3 (rgba16float, D65) — a constant engine invariant. Source gamuts (sRGB/P3/Adobe RGB/ProPhoto/Rec.2020) are mapped by per-fragment matrices derived from chromaticity primaries; out-of-gamut values are never clamped (negative/over-1 floats survive in f16 and round-trip losslessly on export)
  • Premultiplied alpha everywhere; blend math follows W3C in linear light
  • 16 blend modes, split by hardware expressibility: only source-over (plus the opaque-base replace fast path) batches directly through GPUBlendState; the other 15 (Multiply, Screen, Overlay, Soft/Hard Light, Dodge, Burn, Darken, Lighten, Difference, Exclusion, Hue, Saturation, Color, Luminosity) interrupt the batch and ping-pong against the accumulated background in WGSL
Group Modes
Normal Normal
Darken Multiply, Darken, Color Burn
Lighten Screen, Lighten, Color Dodge
Contrast Overlay, Soft Light, Hard Light
Inversion Difference, Exclusion
Component Hue, Saturation, Color, Luminosity

Adjustments (Per-Layer, Non-Destructive)

Grading runs inline in the composite fragment shader for filter-free layers (zero extra passes), or in a pre-pass when compute filters are present. The architecture is TRC wrapping: pixels are decoded to linear light for transport and blending, then encoded into the sRGB perceptual domain for the operator chain, and decoded back:

linear → linear_to_srgb → [ levels → curves → color balance → matrix → 3D LUT ] → srgb_to_linear → linear

Perceptual operators (curves, levels, contrast, color balance) are anchored to the mid-gray pivot and must stay in the encoded domain to preserve Photoshop-compatible slider response; transport, blending, and convolution must stay linear to avoid fringing. The shader guards with an identity fast-path when no adjustment flags are set (zero-cost no-op layers).


Filters (Compute)

Spatial filters run as tiled compute passes over rgba16float linear-light premultiplied pixels:

Filter Implementation
Gaussian Blur Separable H/V compute passes, 64-wide 1D workgroups (GAUSS_WORKGROUP_SIZE = 64) with halo shared-memory tiles; linear-domain premultiplication eliminates dark halos on feathered edges

The FilterDesc union also declares convolve (generic kernel) and pixelate, but scheduling currently admits only gaussianBlur (isEffectiveFilter) — no UI writes the other two yet.

Scheduling rule: grading always runs before convolution (adjust pre-pass → FilterPass H → V → composite with adjustment suppressed). Scratch textures are borrowed from the texture pool for the frame and returned in batch.


Known Boundaries

  • Extreme minification (below ~10%) of very large canvases uses single-tap bilinear without mipmaps and may show slight moiré
  • Mask textures read only the coverage channel — no TRC is ever applied to alpha (coverage is geometry, not light)

Last updated: 2026-10-12