Copied to clipboard
Ways to extend · Shove

Bring your own pixels.

A surface is content GPUI did not draw: a video frame from a hardware decoder, a 3D viewport on its own device, or a texture computed off-thread. surface() and gpu_canvas() composite it as an ordinary element — no seam to implement, no install hook, no CPU round-trip.

2 Doors surface() and gpu_canvas()
3 Transports CoreVideo, Direct3D, dma-buf
0 Seams replaced It sits above the facade
0.15–0.8 ms Export cost Measured by interop_bevy
Two doors

What you already hold decides the door.

If your producer already has a handle GPUI can import, hand it to surface(). If it needs the window's own device to make one, open a gpu_canvas() and paint through the context it gives you.

use gpui::{ObjectFit, surface};

// a decoded frame, a shared texture, or a dma-buf you already hold
let picture = surface(source)
    .object_fit(ObjectFit::Contain);
use gpui::{GpuCanvasContext, gpu_canvas};

let viewport = gpu_canvas(|gpu: &mut GpuCanvasContext| {
    // `frame` is a SurfaceSource from your producer
    gpu.paint_surface(frame);
});

Both return ordinary elements. surface() takes a SurfaceSource and fits it into the element's bounds with the same ObjectFit modes an image uses. gpu_canvas() is a Div in every respect — styling, layout and interactivity are forwarded to the box — whose paint phase adds one primitive after the box. There is no with_surface and nothing to install.

What travels

One enum, cfg-gated to the platform.

SurfaceSource names only the transport this build is compiled against, so the engine stays free of every graphics API but one. Each variant reaches the renderer as a view it samples; the renderer cannot tell how the view was made.

The transports one variant per platform
platform source payload what it is
macOS · iOS CoreVideo CFRetained<CVPixelBuffer> an IOSurface-backed image buffer
Windows DirectX DirectXSource a Direct3D 11 texture, or a view a producer already made
Linux DmaBuf DmaBufHandle an fd plus a DRM fourcc, modifier, stride and offset

A PaintSurface carries the source with its bounds, content mask and draw order. Its to_quad_record is the same quad record a styled element builds, which is why a surface clips and stacks like any other layer.

Same device or exported handle

The short path, and the portable one.

A producer that renders on the window renderer's own device never leaves it: paint_texture samples the texture view and queue ordering is the only synchronization. A producer with its own device exports an OS handle and a fence, and paint_surface imports it.

use gpui::{Corners, gpu_canvas};
use gpui_wgpu::WgpuRenderer;

let viewport = gpu_canvas(|gpu| {
    // the device and queue this window already draws with
    let (device, queue) = gpu.device::<WgpuRenderer>();

    // draw into a texture on `device`, then composite it
    gpu.paint_texture(handle, Corners::default(), 1.0, false);
});

gpui_wgpu is the published bite-gp-wgpu. On Apple the renderer type is MetalRenderer and on Windows DirectXRenderer; the device each lends is that backend's own handle.

  • The same-device path is the default for a reason. There is no export, no fence and no hand-off. It is what interop_bevy uses: Bevy renders into an image on the window renderer's wgpu device and GPUI samples its view.
  • The exported path costs a hand-off per frame. In the Bevy example, exporting the dma-buf and importing it on the consumer side adds ~0.15–0.8 ms/frame; the producer's own timings barely move, which is why the cost shows up on a tight frame budget rather than in a throughput number.
  • The device is the renderer's, not a new one. device::<R>() returns the handle the window already draws with — a metal::Device, an ID3D11Device, or a wgpu device and queue — and try_device is the fallible form for a producer that can degrade instead of asserting the backend.
Who does it

The producers are crates, not features.

Nothing about a surface names a video decoder or a 3D engine. The crates that do are public and sit above the facade, exactly where the architecture puts them.

gpui_interop

published · 0.2

Bridges a producer's own device: dma-buf on Linux, a shared texture and fence on Windows, IOSurface planned on macOS. It is the crate that makes an external renderer or decoder a surface source.

  • Vulkan, wgpu, Direct3D 12 producers
  • the same-device fast path as well as the export
  • input and guest-mode follow-ons live here
crates.io/crates/gpui_interop →

gpui_video

public · pre-release

A hardware-decoded video player on the same path: a VA-API decoder renders into driver-tiled NV12 surfaces, exports them as dma-bufs, and the element paces playback with HTML5-shaped controls.

  • Linux / VA-API is the implemented backend
  • macOS and Windows designed, not built
  • not published under these names
bite-gpui/gpui_video →

interop_bevy

example

A Bevy scene inside a GPUI window, with Bevy built headless: no winit, no window, no event loop of its own. GPUI owns the cadence and Bevy renders into the window renderer's own device.

  • one Bevy tick per GPUI paint
  • paint_texture by default; --transfer for the dma-buf shape
  • ~4–6 ms/frame on the test laptop
examples/interop_bevy →

The same primitive carries four kinds of work: hardware video (a decoder exports NV12 buffers), embedded 3D viewports (a renderer on its own device), emulators and capture pipelines (frames leave another process), and computed textures (a simulation or a chart rendered off-thread). None of them is a seam in this engine; all of them are a source the renderer samples.

What it is not

Not a swap, and not a wrap.

  • No trait is implemented. A swap implements a seam and is installed at bootstrap; a surface does not replace a shaper, a solver or a pipeline. It adds a primitive to a frame.
  • No frame is decorated. A wrap forwards passes; a surface is painted by the element that owns it and is done before the frame's own bookkeeping.
  • It sits above the facade. The commitment is to the published payload types a source is built from — the token and the handle — not to a trait that would freeze a backend.
  • It does not copy through the CPU. That is the whole point. A producer that can only hand over bytes should upload them itself; a surface exists for the cases where the pixels never need to leave the GPU.
Boundaries

What a producer has to get right.

  • The decoder must share the renderer's GPU. A dma-buf is importable only on the GPU that exported it, so a VA-API device and the wgpu renderer must be the same physical card.
  • The handle has to match what the renderer imports. A dma-buf declares its DRM format and modifier; a Direct3D texture must be shareable; an IOSurface is the Apple equivalent. The importer refuses what it cannot sample rather than guessing.
  • Cross-API means a fence. Two devices, two queues: the producer exports a fence with the frame and the consumer waits on it. On the same device, queue ordering is the fence.
  • The platform matrix is not symmetric yet. Linux and Windows have the full imported path; macOS has the CoreVideo variant and gpui_interop's IOSurface support is planned.
The rest of the ways

Ways to extend has all four, and Bring your own platform is the host side. If what you want to change is the algorithm a seam runs, that is a swap.