2026-05-18
Stage 2: the frame never leaves the GPU
A week later the CPU round trip is gone. Not optimised — gone.
Servo still paints into its own offscreen framebuffer, through a rendering context our fork builds from the host's display handle with a surface nobody composites, so GPUI keeps owning the drawable the screen actually shows. What changed is what happens next. When Servo reports a frame ready, tick() asks that context to export the framebuffer and gets back a handle: a dma-buf file descriptor for the GPU memory, and a fence that says when the writes have landed. Both are ours to own. The handle goes into a slot shared with the GPUI side.
The other half lives in GPUI's wgpu backend. draw_external_sprites picks up the handle, imports the dma-buf as a wgpu::Texture through wgpu::hal::vulkan and ash, and draws it as one more sprite in the scene. As far as GPUI is concerned the web page is a texture like any other. The stage 1 path — the readback, the red/blue swap, a RenderImage upload every frame — is deleted, not disabled. Nothing bounces through system memory any more.
The fence is the fiddly part. A sync fd is single-use: once anyone has waited on it, waiting again has no defined meaning. So we hand it to the first consumer in a frame and let the rest fall back to a coarser wait. The dma-buf handle itself we keep cached, which is why a frame where Servo painted nothing still shows the last good one instead of flickering.
Mouse and scroll forwarding landed the same day. That is when the page first answered a click. Until then it was a very expensive screenshot that happened to update.