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_distBiasdigest 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 throughGPUBlendState; 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