← Extensions to GPUI

Paint and shader extension. Opt in through xgpui-shader; ordinary output is sRGB, with explicit native wide-SDR programs.

Shader paints (experimental)

See choosing a shader rendering model and migrating old names.

Experimental xgpui-shader/portable-shaders. Backend selection stays inside the extension.

Dependency

From an app under apps/:

[dependencies]
xgpui-shader = { path = "../../extensions/crates/xgpui-shader", features = ["portable-shaders"] }
# Add "wgpu" for the optional wgpu renderer. {#add-wgpu-for-the-optional-wgpu-renderer}

wash.wgsl

struct Parameters {
    progress: f32,
    tint: vec3<f32>,
    alpha: f32,
}

fn paint(input: PaintInput) -> vec4<f32> {
    let blend = clamp(input.uv.x + parameters.progress, 0.0, 1.0);
    let accent = vec3(
        0.1,
        clamp(input.position.y / 160.0, 0.0, 1.0),
        input.size.x / 400.0,
    );
    return vec4(mix(parameters.tint, accent, blend), parameters.alpha);
}

Extension-supplied type (do not redeclare it in user source):

struct PaintInput {
    uv: vec2<f32>,        // Full source rectangle, normalized.
    position: vec2<f32>,  // Local logical GPUI pixels: uv * size.
    size: vec2<f32>,      // Full logical source size.
}
// Also supplied: parameters, bindings, fullscreen geometry and stage entry points.

Origin: top-left; +x right, +y down. UV uses raster pixel centers. Clip and border coverage never remap the field; the host applies coverage/opacity.

Rust

use gpui::{prelude::*, *};
use xgpui_shader::{painted, WindowPaintExt};
use xgpui_shader::shader_paint::{ShaderProgram, ShaderParameters};

let program = ShaderProgram::new(include_str!("wash.wgsl"))?;

let parameters_at = |progress| {
    ShaderParameters::new()
        .float("progress", progress)
        .vec3("tint", [0.95, 0.15, 0.35])
        .float("alpha", 1.0)
};

let paint = program.paint(parameters_at(0.15))?;
painted().w(px(320.0)).h(px(240.0)).bg_paint(paint.clone());
painted().border_8().border_paint(paint.clone());
window.paint_path_with_paint(path, paint.clone());

// Keep program in view state. Animate by creating immutable snapshots.
let next = program.paint(parameters_at(0.75))?;
let same = paint.clone(); // Shares parameters + cached output.

Parameter API

Signature summary:

impl ShaderParameters {
    pub fn new() -> Self;
    pub fn float(self, name: impl Into<String>, value: f32) -> Self;
    pub fn vec2(self, name: impl Into<String>, value: [f32; 2]) -> Self;
    pub fn vec3(self, name: impl Into<String>, value: [f32; 3]) -> Self;
    pub fn vec4(self, name: impl Into<String>, value: [f32; 4]) -> Self;
}
// All members required, even unused ones. Repeated setter: last value wins.
// Reflected layout/padding is packed by the extension; no Rust layout casts.
ConstraintShader paints
Source size≤ 64 KiB
Parameter members1–32
Packed parameter size≤ 1024 bytes
Typesf32, vec2/3/4<f32>
ValuesFinite; exact reflected names/types
AllowedMath, helpers, local values, conditionals
RejectedResources/globals, overrides, entry points, textures, storage/compute
RejectedDerivatives, discard, loops, integer/bool parameters, arrays, matrices
OutputStraight-alpha encoded-sRGB SDR; channels clamped to 0..=1
Shader obligationFinite arithmetic; no shape coverage/opacity multiplication
VersioningColor-contract changes require a new version

Compilation and cache

new(source)
  → parse + validate restricted WGSL
  → reflect parameters/offsets
  → verify MSL + HLSL SM5 + SPIR-V translation

paint(parameters)
  → validate names/types/finite values
  → pack reflected layout
  → immutable snapshot

prepare(snapshot, request)
  → backend pipeline (lazy; shared across related snapshots)
  → cached output OR record draw
  → host validation/submission/composition

output key = backend queue/context + exact raster extent + logical source size
budget     = 64 MiB live output/backend; excludes in-flight resource retention
FailureResult
Parse/validationOriginal user line when span available + wrapped diagnostic
Parameter setField-specific error before snapshot creation
Driver compilationGenerated-source diagnostic; full cross-language map deferred
Preparation/deviceFrame error; no paint skipping
Rejected recordingUnsubmitted work discarded on wgpu/D3D11

Raw Metal commands retain the compatibility submission contract. No new normal-path GPU completion waits. Texture parameters, compute/custom geometry, HDR and stable API guarantees are outside this shader-paint contract.

Test and capture

just --justfile apps/xgpui-ext-lab/paint-lab/justfile portable-test
just --justfile apps/xgpui-ext-lab/paint-lab/justfile portable-headless --frames 2 --png /tmp/portable-metal.png
just --justfile apps/xgpui-ext-lab/paint-lab/justfile portable-wgpu --frames 2 --png /tmp/portable-wgpu.png
cargo test --config .cargo/validation.toml \
  --manifest-path extensions/crates/xgpui-shader/Cargo.toml \
  --features wgpu,portable-shaders --target-dir target
Test groupCoverage
CPU/Nagavec3 padding, bad parameters, subset rejection, source diagnostics, target translation
Metal + wgpu/MetalAlpha, opacity, clipping, logical coordinates, logical-size cache invalidation
SnapshotsParameter immutability, sharing, unused uniforms
Windows/VulkanNative paint/gamut backend CI passed on 2026-10-25; Windows labs/lab-test passed in owner testing on 2026-11-04. See scope.

App source: apps/xgpui-ext-lab/paint-lab/src/portable.rs, src/shaders/portable-wash.wgsl. Native D3D11 · wgpu adapter