diff --git a/crates/gpui_linux/Cargo.toml b/crates/gpui_linux/Cargo.toml index e4cd0ed7fd..f830d2dbc9 100644 --- a/crates/gpui_linux/Cargo.toml +++ b/crates/gpui_linux/Cargo.toml @@ -15,6 +15,9 @@ path = "src/gpui_linux.rs" [features] default = ["wayland", "x11"] test-support = ["gpui/test-support"] +# Reads a window’s frame back through the WGPU renderer’s offscreen readback +# (`Window::render_to_image`) without the rest of `test-support`. +render-to-image = ["gpui/render-to-image", "gpui_wgpu?/render-to-image"] wayland = [ "bitflags", "gpui_wgpu", diff --git a/crates/gpui_linux/src/linux/wayland/window.rs b/crates/gpui_linux/src/linux/wayland/window.rs index 5b6fd583ac..fdef53d765 100644 --- a/crates/gpui_linux/src/linux/wayland/window.rs +++ b/crates/gpui_linux/src/linux/wayland/window.rs @@ -2148,6 +2148,11 @@ impl PlatformWindow for WaylandWindow { self.0.schedule_frame(); } + #[cfg(feature = "render-to-image")] + fn render_to_image(&self, scene: &Scene) -> anyhow::Result { + self.borrow_mut().renderer.render_to_image(scene) + } + fn sprite_atlas(&self) -> Arc { let state = self.borrow(); state.renderer.sprite_atlas().clone() diff --git a/crates/gpui_linux/src/linux/x11/window.rs b/crates/gpui_linux/src/linux/x11/window.rs index 499d237a3f..c7a85a3989 100644 --- a/crates/gpui_linux/src/linux/x11/window.rs +++ b/crates/gpui_linux/src/linux/x11/window.rs @@ -1796,6 +1796,11 @@ impl PlatformWindow for X11Window { } } + #[cfg(feature = "render-to-image")] + fn render_to_image(&self, scene: &Scene) -> anyhow::Result { + self.0.state.borrow_mut().renderer.render_to_image(scene) + } + fn sprite_atlas(&self) -> Arc { let inner = self.0.state.borrow(); inner.renderer.sprite_atlas().clone() diff --git a/crates/gpui_macos/src/wgpu_renderer.rs b/crates/gpui_macos/src/wgpu_renderer.rs index 00e3e88f0d..6b3cd55507 100644 --- a/crates/gpui_macos/src/wgpu_renderer.rs +++ b/crates/gpui_macos/src/wgpu_renderer.rs @@ -123,10 +123,10 @@ impl MacWgpuRenderer { self.renderer.render_to_image(scene) } - // WGPU's offscreen rendering needs gpui_wgpu's test-support, which the - // `test-support` feature enables. `render-to-image` deliberately does not - // (it exists to render without test-support's frame scheduling), so with - // the `wgpu` renderer it reports the gap rather than failing to compile. + // WGPU’s offscreen readback needs gpui_wgpu’s `test-support` or + // `render-to-image`, and this crate’s `render-to-image` forwards neither, + // so with the `wgpu` renderer it reports the gap rather than failing to + // compile. #[cfg(all(any(test, feature = "render-to-image"), not(feature = "test-support")))] pub fn render_to_image(&mut self, _scene: &Scene) -> anyhow::Result { anyhow::bail!("rendering to an image with the wgpu renderer needs the test-support feature") diff --git a/crates/gpui_platform/Cargo.toml b/crates/gpui_platform/Cargo.toml index 2800bb2d1c..7f07628e28 100644 --- a/crates/gpui_platform/Cargo.toml +++ b/crates/gpui_platform/Cargo.toml @@ -29,6 +29,7 @@ test-support = [ render-to-image = [ "gpui/render-to-image", "gpui_apple/render-to-image", + "gpui_linux/render-to-image", "gpui_macos/render-to-image", "gpui_windows/render-to-image", ] diff --git a/crates/gpui_wgpu/Cargo.toml b/crates/gpui_wgpu/Cargo.toml index 9488b9215b..ffdf9de875 100644 --- a/crates/gpui_wgpu/Cargo.toml +++ b/crates/gpui_wgpu/Cargo.toml @@ -15,6 +15,9 @@ path = "src/gpui_wgpu.rs" [features] default = [] test-support = ["gpui/test-support", "dep:image"] +# Compiles `WgpuRenderer::render_to_image`, the offscreen readback, without the +# rest of `test-support`, so a window can read its own frame back. +render-to-image = ["dep:image"] custom-gpu = ["gpui/custom-gpu"] wgpu-surfaces = ["gpui/wgpu-surfaces"] diff --git a/crates/gpui_wgpu/src/wgpu_renderer.rs b/crates/gpui_wgpu/src/wgpu_renderer.rs index 069246b160..fd10f20264 100644 --- a/crates/gpui_wgpu/src/wgpu_renderer.rs +++ b/crates/gpui_wgpu/src/wgpu_renderer.rs @@ -18,7 +18,10 @@ mod buffers; mod drawing; mod filters; mod frame; -#[cfg(all(feature = "test-support", not(target_family = "wasm")))] +#[cfg(all( + any(feature = "test-support", feature = "render-to-image"), + not(target_family = "wasm") +))] mod headless; mod path_types; mod pipelines; @@ -261,7 +264,10 @@ impl WgpuRenderer { frame::render_to_view(self, scene, frame_view, None).is_some() } - #[cfg(all(feature = "test-support", not(target_family = "wasm")))] + #[cfg(all( + any(feature = "test-support", feature = "render-to-image"), + not(target_family = "wasm") + ))] fn render_to_view_with_readback( &mut self, scene: &Scene, @@ -494,6 +500,49 @@ mod tests { Ok(()) } + #[cfg(all(feature = "test-support", not(target_family = "wasm")))] + #[test] + fn render_to_image_refuses_without_gpu_resources() -> anyhow::Result<()> { + let context = WgpuContext::new_headless(None)?; + let size = Size { + width: DevicePixels(2), + height: DevicePixels(2), + }; + let mut renderer = WgpuRenderer::new_headless(&context, size)?; + let mut scene = Scene::default(); + scene.finish(); + // What a window’s renderer looks like while it recovers a lost device. + renderer.destroy(); + let error = renderer + .render_to_image(&scene) + .expect_err("a renderer without GPU resources has no frame to read back"); + assert!(error.to_string().contains("live GPU device"), "{error}"); + Ok(()) + } + + #[cfg(all(feature = "test-support", not(target_family = "wasm")))] + #[test] + fn render_to_image_refuses_once_the_device_is_lost() -> anyhow::Result<()> { + let context = WgpuContext::new_headless(None)?; + let size = Size { + width: DevicePixels(2), + height: DevicePixels(2), + }; + let mut renderer = WgpuRenderer::new_headless(&context, size)?; + let mut scene = Scene::default(); + scene.finish(); + // A window between the device-lost callback and the draw that recovers it. + renderer + .faults + .device_lost + .store(true, std::sync::atomic::Ordering::Relaxed); + let error = renderer + .render_to_image(&scene) + .expect_err("a lost device has no frame to read back"); + assert!(error.to_string().contains("live GPU device"), "{error}"); + Ok(()) + } + #[cfg(all(feature = "test-support", not(target_family = "wasm")))] #[test] fn configurable_dashes_reach_both_wgpu_quad_pipelines() -> anyhow::Result<()> { diff --git a/crates/gpui_wgpu/src/wgpu_renderer/headless.rs b/crates/gpui_wgpu/src/wgpu_renderer/headless.rs index beff866c98..29c8d013dc 100644 --- a/crates/gpui_wgpu/src/wgpu_renderer/headless.rs +++ b/crates/gpui_wgpu/src/wgpu_renderer/headless.rs @@ -1,10 +1,14 @@ +#[cfg(feature = "test-support")] use std::sync::Arc; use gpui::{DevicePixels, Scene, Size}; +#[cfg(feature = "test-support")] use crate::{WgpuAtlas, WgpuContext}; -use super::{WgpuRenderer, WgpuSurfaceConfig}; +use super::WgpuRenderer; +#[cfg(feature = "test-support")] +use super::WgpuSurfaceConfig; struct OffscreenTarget { texture: wgpu::Texture, @@ -15,6 +19,7 @@ struct OffscreenTarget { } impl WgpuRenderer { + #[cfg(feature = "test-support")] pub(super) fn new_headless( context: &WgpuContext, size: Size, @@ -127,6 +132,20 @@ impl WgpuRenderer { /// Renders through the normal scene path and reads back without presenting. pub fn render_to_image(&mut self, scene: &Scene) -> anyhow::Result { + // A window’s renderer drops its GPU resources while it recovers a lost device. + anyhow::ensure!( + self.resources.is_some() && !self.device_lost(), + "render_to_image unavailable without a live GPU device" + ); + // The readback copies 8-bit RGBA or BGRA rows; a window surface may use another format. + let format = self.target.format(); + anyhow::ensure!( + matches!( + format, + wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Rgba8Unorm + ), + "render_to_image cannot read back a {format:?} target" + ); let target = self.create_offscreen_target(); let submission = self .render_to_view_with_readback(scene, &target.view, target.readback_copy()) @@ -148,11 +167,13 @@ impl OffscreenTarget { } /// Surface-free renderer used by GPUI visual tests and benchmarks. +#[cfg(feature = "test-support")] pub struct WgpuHeadlessRenderer { renderer: WgpuRenderer, target: Option, } +#[cfg(feature = "test-support")] impl WgpuHeadlessRenderer { pub fn new() -> anyhow::Result { let context = WgpuContext::new_headless(None)?; @@ -209,6 +230,7 @@ impl WgpuHeadlessRenderer { } } +#[cfg(feature = "test-support")] impl gpui::PlatformHeadlessRenderer for WgpuHeadlessRenderer { fn render_scene_to_image( &mut self,