← Extensions to GPUI

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

Paint preparation and failure contracts

Downstream GPUI integration. Implemented behavior first; deferred designs last.

Existing producers still work

A complete legacy-signature producer that delegates to the portable solid paint:

use anyhow::Result;
use gpui::{CustomContent, DevicePixels, Size};
use std::any::Any;
use xgpui_shader::SolidColor;

#[derive(Debug)]
struct Accent(SolidColor);

impl Accent {
    fn new() -> Result<Self> {
        Ok(Self(SolidColor::new([0.9, 0.2, 0.3, 1.0])?))
    }
}

impl CustomContent for Accent {
    fn prepare(
        &self,
        backend: &dyn Any,
        size: Size<DevicePixels>,
    ) -> Result<Box<dyn Any>> {
        self.0.prepare(backend, size)
    }
}

The provided trait method forwards the exact raster extent:

// Existing GPUI default implementation — no producer migration required.
fn prepare_with_request(
    &self,
    backend: &dyn std::any::Any,
    request: &PreparationRequest,
) -> anyhow::Result<Box<dyn std::any::Any>> {
    self.prepare(backend, request.raster_size())
}

Read logical and raster geometry separately

An optional override inside impl CustomContent:

fn prepare_with_request(
    &self,
    backend: &dyn std::any::Any,
    request: &gpui::PreparationRequest,
) -> anyhow::Result<Box<dyn std::any::Any>> {
    let logical = request.logical_size();   // GPUI Pixels, full unclipped bounds.
    let raster = request.raster_size();     // Exact allocated DevicePixels.
    let scale = request.scale_factor();

    // Do not reconstruct raster by multiplying logical by scale.
    // This solid producer needs only the exact extent.
    self.prepare(backend, raster)
}

Window recording retains logical size and scale in a private wrapper; replay shares it. Direct low-level scene records default to 1x logical geometry.

let request = gpui::PreparationRequest::new(
    size(px(120.0), px(80.0)),
    size(gpui::DevicePixels(180), gpui::DevicePixels(120)),
    1.5,
)?;
assert_eq!(request.scale_factor(), 1.5);

Implemented discardable recording transactions

Internal wgpu/D3D11 flow — pseudocode, not a public API:

for draw in custom_draws:
    attempt = batch.begin()                  // No encoder allocated yet.
    output = producer.prepare(attempt)       // First record() allocates it.
    validate_device_queue_extent(output)
    prepare_composition(output)
    batch.stage(attempt, output)

submit_in_order(batch)                       // Only after all draws succeed.

on any returned error:
    drop(current_attempt)                   // Pending output → abandoned.
    drop(batch)                             // All staged output → abandoned.
    return frame_error                      // No paint skipping yet.
Output state     Reusable by
──────────────   ──────────────────────────────────────────
pending          Its own recording only
staged           Later transactions in the same batch
submitted        Later compatible preparations
abandoned        Nobody

record() callback error → discard entire attempt
producer catches that error → attempt still invalid
cache-only preparation → no recorder, no empty submission
failed later attempt → previously submitted cache stays valid

Retry allocation counts differ by backend: wgpu and D3D11 abandon staged output when a later producer aborts the batch, so the first successful retry regenerates the output once. Compiled pipelines remain reusable. Metal's legacy producer path retains its unsubmitted command and reuses it on retry. After a successful retry, all paths must reuse the submitted output without another preparation; pixels and pipeline compilation counts must remain stable.

No new public GPUI methods. No normal-path GPU completion wait. Native mask work has an independent submission path and is not rolled back. Raw GPU handles remain a cooperative contract; transactions cannot undo externally submitted work.

Metal compatibility path

prepare all outputs
    ↓
validate texture device / format / extent / sampling usage
    ↓
validate every command: exact host queue + NotEnqueued
    ↓
revalidate whole batch before commit
    ↓
deduplicate retained command-buffer identities → commit each once

Invalid batches submit no producer commands. An empty command list is allowed for ready or previously host-submitted cache output; the producer must guarantee readiness. Metal's raw-command compatibility path is not a host recording transaction.

Color and coordinates today

source format       BGRA8Unorm
source encoding     encoded sRGB, SDR
source alpha        straight
stop interpolation  premultiplied encoded sRGB (analytic gradients)

source origin       top-left; +x right; +y down
uv at pixel center  (pixel_index + 0.5) / raster_extent
logical position    uv * logical_size
background / border full outer element rectangle
path                original geometric bounds
clip / opacity      host composition; never remap the source
coverage            native mask red channel; rounded rect analytic coverage
Metal SolidColor / Gradient / MeshGradient
  key       = queue identity + physical width/height
  capacity  = 64 MiB live output textures per producer snapshot
  eviction  = individual least-recently-used entries
  pending   = return the same command buffer until host submission

Eviction drops cache ownership only; retained/in-flight outputs stay valid.

Metal / wgpu / D3D11 coverage cache
  validate  = path vertices <= 1,000,000 before hashing
  lookup    = streamed normalized-geometry fingerprint; no temporary Vec
  equality  = exact stored geometry comparison inside fingerprint bucket
  insert    = retain normalized words only on a successful cache miss
  budget    = mask texture + retained normalized geometry
  CPU cost  = O(vertices); allocation-free does not mean constant-time

Source cache keys include backend ownership and extent; WGSL also keys logical size. Shape coverage has its own cache. Metal coverage and extension live-output budgets are 64 MiB each; in-flight resources are outside those live-cache budgets.

Compositor uniform uploads

payload      64 bytes/draw; bounds, clip, radii, viewport, opacity, source flags
wgpu         persistent aligned arena; one slot/draw; one packed upload/batch
             grow capacity only; upload only after preparation succeeds
             submit frame before reuse; queue ordering protects in-flight reads
D3D11        one DYNAMIC constant buffer/compositor; WRITE_DISCARD per draw
             fixed-size stack packing; driver renames discarded backing storage
# Growth, queued frame snapshots and uniform buffer creation counts. {#growth-queued-frame-snapshots-and-uniform-buffer-creation-counts}
cargo test --offline --config .cargo/validation.toml \
  --manifest-path gpui/crates/gpui-wgpu/Cargo.toml --target-dir target \
  --features test-support --lib wgpu_custom_uniform
# Windows: same command with gpui-windows and filter dx11_custom_uniform. {#windows-same-command-with-gpui-windows-and-filter-dx11customuniform}

# Per-draw values, failed-frame recovery, native batches after custom draws. {#per-draw-values-failed-frame-recovery-native-batches-after-custom-draws}
cargo test --offline --config .cargo/validation.toml \
  --manifest-path apps/xgpui-ext-lab/Cargo.toml --bin paint-lab --target-dir target \
  --features wgpu,portable-shaders uniform_frames

wgpu still allocates staging memory for its batch upload and creates per-draw texture views/bind groups. Effect-producer uniforms are separate. No public API change or GPU completion wait. D3D11 execution requires Windows/WARP validation.

Low-alpha compositor checks

MTL_DEBUG_LAYER=1 cargo test --offline --config .cargo/validation.toml \
  --manifest-path apps/xgpui-ext-lab/Cargo.toml --bin paint-lab --target-dir target \
  --features wgpu,portable-shaders low_alpha_saturated_edges -- --nocapture
sources     saturated RGB solids + antialiased stripes; sRGB/Oklab gradients
alpha       0, 1e-5, 5e-4, 1/255, 2/255, 0.02, 0.2 (solid/stripe cases)
backdrops   #101010 / #f0f0f0
geometry    fractional origin/extent; bilinear sampling; rounded AA edges
reference   quantized native source → CPU bilinear filter → premultiplied source-over

Metal + wgpu/Metal: 188 cases/backend; maximum error 1/255 per RGB channel.
No shader/blend changes. No demonstrated need for an epsilon cutoff or immediate
premultiplied compositor migration in this 8-bit path.

Gradient alpha is inferred from black/white readbacks, with up to one level of source-alpha uncertainty. These tests cannot resolve sub-8-bit errors or establish HDR, Vulkan or Windows runtime behavior. Premultiplied output remains a separate color-contract change, not a correction established by these results.

Deferred — not callable APIs

ProposalRequired decision/test
Skip failed paint, bounded diagnosticsUnrelated UI survives; no stale animation fallback
Metal host recordingPreserve legacy producers before deprecating raw handles
Color descriptorsPrimaries, transfer function, alpha association, validated output
Linear premultiplied filteringSaturated translucent-edge references; preserve legacy output
Shared paint rectangle/transformCoordinate space, sampling rules and cache dependencies
High gamut/HDRSeparate window/display negotiation; not enabled by float textures

Run the contract tests

# Native/portable geometry, alpha, clipping, replay and legacy-producer recovery. {#nativeportable-geometry-alpha-clipping-replay-and-legacy-producer-recovery}
just --justfile apps/xgpui-ext-lab/paint-lab/justfile metal-test
just --justfile apps/xgpui-ext-lab/paint-lab/justfile portable-test

# Request validation, old signature forwarding and fractional-scale geometry. {#request-validation-old-signature-forwarding-and-fractional-scale-geometry}
cargo test --offline --config .cargo/validation.toml \
  --manifest-path gpui/crates/gpui/Cargo.toml --target-dir target \
  --features test-support --lib preparation_

# Built-in Metal cache: size alternation, LRU eviction, pending reuse, queues. {#built-in-metal-cache-size-alternation-lru-eviction-pending-reuse-queues}
MTL_DEBUG_LAYER=1 cargo test --offline --config .cargo/validation.toml \
  --manifest-path extensions/crates/xgpui-shader/Cargo.toml \
  --target-dir target --lib metal_cache

# Allocation counts, forced collisions, normalization and early path limits. {#allocation-counts-forced-collisions-normalization-and-early-path-limits}
cargo test --offline --config .cargo/validation.toml \
  --manifest-path gpui/crates/gpui-wgpu/Cargo.toml --target-dir target \
  --features test-support --lib coverage_cache
# Same filter on gpui-apple and gpui-windows for their private implementations. {#same-filter-on-gpui-apple-and-gpui-windows-for-their-private-implementations}

# Recording errors, staged reuse, abandonment and submission ordering. {#recording-errors-staged-reuse-abandonment-and-submission-ordering}
WGPU_BACKEND=metal MTL_DEBUG_LAYER=1 cargo test --offline \
  --config .cargo/validation.toml \
  --manifest-path gpui/crates/gpui-wgpu/Cargo.toml --target-dir target \
  --features test-support --lib custom_draw

Local wgpu tests use Metal. D3D11/WARP and Vulkan runtime acceptance require their platforms; cross-compilation and shader translation do not establish it.

Application API · D3D11 recording notes