Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/gpui_linux/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions crates/gpui_linux/src/linux/wayland/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<image::RgbaImage> {
self.borrow_mut().renderer.render_to_image(scene)
}

fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
let state = self.borrow();
state.renderer.sprite_atlas().clone()
Expand Down
5 changes: 5 additions & 0 deletions crates/gpui_linux/src/linux/x11/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1796,6 +1796,11 @@ impl PlatformWindow for X11Window {
}
}

#[cfg(feature = "render-to-image")]
fn render_to_image(&self, scene: &Scene) -> anyhow::Result<image::RgbaImage> {
self.0.state.borrow_mut().renderer.render_to_image(scene)
}

fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
let inner = self.0.state.borrow();
inner.renderer.sprite_atlas().clone()
Expand Down
8 changes: 4 additions & 4 deletions crates/gpui_macos/src/wgpu_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<image::RgbaImage> {
anyhow::bail!("rendering to an image with the wgpu renderer needs the test-support feature")
Expand Down
1 change: 1 addition & 0 deletions crates/gpui_platform/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
Expand Down
3 changes: 3 additions & 0 deletions crates/gpui_wgpu/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down
53 changes: 51 additions & 2 deletions crates/gpui_wgpu/src/wgpu_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<()> {
Expand Down
24 changes: 23 additions & 1 deletion crates/gpui_wgpu/src/wgpu_renderer/headless.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -15,6 +19,7 @@ struct OffscreenTarget {
}

impl WgpuRenderer {
#[cfg(feature = "test-support")]
pub(super) fn new_headless(
context: &WgpuContext,
size: Size<DevicePixels>,
Expand Down Expand Up @@ -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<image::RgbaImage> {
// 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())
Expand All @@ -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<OffscreenTarget>,
}

#[cfg(feature = "test-support")]
impl WgpuHeadlessRenderer {
pub fn new() -> anyhow::Result<Self> {
let context = WgpuContext::new_headless(None)?;
Expand Down Expand Up @@ -209,6 +230,7 @@ impl WgpuHeadlessRenderer {
}
}

#[cfg(feature = "test-support")]
impl gpui::PlatformHeadlessRenderer for WgpuHeadlessRenderer {
fn render_scene_to_image(
&mut self,
Expand Down
Loading