Skip to content

Commit 9db70da

Browse files
Add screenshot api (bevyengine#7163)
Fixes bevyengine#1207 # Objective Right now, it's impossible to capture a screenshot of the entire window without forking bevy. This is because - The swapchain texture never has the COPY_SRC usage - It can't be accessed without taking ownership of it - Taking ownership of it breaks *a lot* of stuff ## Solution - Introduce a dedicated api for taking a screenshot of a given bevy window, and guarantee this screenshot will always match up with what gets put on the screen. --- ## Changelog - Added the `ScreenshotManager` resource with two functions, `take_screenshot` and `save_screenshot_to_disk`
1 parent 9fd867a commit 9db70da

File tree

12 files changed

+557
-53
lines changed

12 files changed

+557
-53
lines changed

Diff for: Cargo.toml

+10
Original file line numberDiff line numberDiff line change
@@ -1909,6 +1909,16 @@ description = "Illustrates how to customize the default window settings"
19091909
category = "Window"
19101910
wasm = true
19111911

1912+
[[example]]
1913+
name = "screenshot"
1914+
path = "examples/window/screenshot.rs"
1915+
1916+
[package.metadata.example.screenshot]
1917+
name = "Screenshot"
1918+
description = "Shows how to save screenshots to disk"
1919+
category = "Window"
1920+
wasm = false
1921+
19121922
[[example]]
19131923
name = "transparent_window"
19141924
path = "examples/window/transparent_window.rs"

Diff for: crates/bevy_render/src/camera/camera.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ impl NormalizedRenderTarget {
423423
match self {
424424
NormalizedRenderTarget::Window(window_ref) => windows
425425
.get(&window_ref.entity())
426-
.and_then(|window| window.swap_chain_texture.as_ref()),
426+
.and_then(|window| window.swap_chain_texture_view.as_ref()),
427427
NormalizedRenderTarget::Image(image_handle) => {
428428
images.get(image_handle).map(|image| &image.texture_view)
429429
}

Diff for: crates/bevy_render/src/camera/camera_driver_node.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ impl Node for CameraDriverNode {
5252
continue;
5353
}
5454

55-
let Some(swap_chain_texture) = &window.swap_chain_texture else {
55+
let Some(swap_chain_texture) = &window.swap_chain_texture_view else {
5656
continue;
5757
};
5858

Diff for: crates/bevy_render/src/render_resource/texture.rs

+25-42
Original file line numberDiff line numberDiff line change
@@ -51,31 +51,21 @@ define_atomic_id!(TextureViewId);
5151
render_resource_wrapper!(ErasedTextureView, wgpu::TextureView);
5252
render_resource_wrapper!(ErasedSurfaceTexture, wgpu::SurfaceTexture);
5353

54-
/// This type combines wgpu's [`TextureView`](wgpu::TextureView) and
55-
/// [`SurfaceTexture`](wgpu::SurfaceTexture) into the same interface.
56-
#[derive(Clone, Debug)]
57-
pub enum TextureViewValue {
58-
/// The value is an actual wgpu [`TextureView`](wgpu::TextureView).
59-
TextureView(ErasedTextureView),
60-
61-
/// The value is a wgpu [`SurfaceTexture`](wgpu::SurfaceTexture), but dereferences to
62-
/// a [`TextureView`](wgpu::TextureView).
63-
SurfaceTexture {
64-
// NOTE: The order of these fields is important because the view must be dropped before the
65-
// frame is dropped
66-
view: ErasedTextureView,
67-
texture: ErasedSurfaceTexture,
68-
},
69-
}
70-
7154
/// Describes a [`Texture`] with its associated metadata required by a pipeline or [`BindGroup`](super::BindGroup).
72-
///
73-
/// May be converted from a [`TextureView`](wgpu::TextureView) or [`SurfaceTexture`](wgpu::SurfaceTexture)
74-
/// or dereferences to a wgpu [`TextureView`](wgpu::TextureView).
7555
#[derive(Clone, Debug)]
7656
pub struct TextureView {
7757
id: TextureViewId,
78-
value: TextureViewValue,
58+
value: ErasedTextureView,
59+
}
60+
61+
pub struct SurfaceTexture {
62+
value: ErasedSurfaceTexture,
63+
}
64+
65+
impl SurfaceTexture {
66+
pub fn try_unwrap(self) -> Option<wgpu::SurfaceTexture> {
67+
self.value.try_unwrap()
68+
}
7969
}
8070

8171
impl TextureView {
@@ -84,34 +74,21 @@ impl TextureView {
8474
pub fn id(&self) -> TextureViewId {
8575
self.id
8676
}
87-
88-
/// Returns the [`SurfaceTexture`](wgpu::SurfaceTexture) of the texture view if it is of that type.
89-
#[inline]
90-
pub fn take_surface_texture(self) -> Option<wgpu::SurfaceTexture> {
91-
match self.value {
92-
TextureViewValue::TextureView(_) => None,
93-
TextureViewValue::SurfaceTexture { texture, .. } => texture.try_unwrap(),
94-
}
95-
}
9677
}
9778

9879
impl From<wgpu::TextureView> for TextureView {
9980
fn from(value: wgpu::TextureView) -> Self {
10081
TextureView {
10182
id: TextureViewId::new(),
102-
value: TextureViewValue::TextureView(ErasedTextureView::new(value)),
83+
value: ErasedTextureView::new(value),
10384
}
10485
}
10586
}
10687

107-
impl From<wgpu::SurfaceTexture> for TextureView {
88+
impl From<wgpu::SurfaceTexture> for SurfaceTexture {
10889
fn from(value: wgpu::SurfaceTexture) -> Self {
109-
let view = ErasedTextureView::new(value.texture.create_view(&Default::default()));
110-
let texture = ErasedSurfaceTexture::new(value);
111-
112-
TextureView {
113-
id: TextureViewId::new(),
114-
value: TextureViewValue::SurfaceTexture { texture, view },
90+
SurfaceTexture {
91+
value: ErasedSurfaceTexture::new(value),
11592
}
11693
}
11794
}
@@ -121,10 +98,16 @@ impl Deref for TextureView {
12198

12299
#[inline]
123100
fn deref(&self) -> &Self::Target {
124-
match &self.value {
125-
TextureViewValue::TextureView(value) => value,
126-
TextureViewValue::SurfaceTexture { view, .. } => view,
127-
}
101+
&self.value
102+
}
103+
}
104+
105+
impl Deref for SurfaceTexture {
106+
type Target = wgpu::SurfaceTexture;
107+
108+
#[inline]
109+
fn deref(&self) -> &Self::Target {
110+
&self.value
128111
}
129112
}
130113

Diff for: crates/bevy_render/src/renderer/graph_runner.rs

+3
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,12 @@ impl RenderGraphRunner {
5757
render_device: RenderDevice,
5858
queue: &wgpu::Queue,
5959
world: &World,
60+
finalizer: impl FnOnce(&mut wgpu::CommandEncoder),
6061
) -> Result<(), RenderGraphRunnerError> {
6162
let mut render_context = RenderContext::new(render_device);
6263
Self::run_graph(graph, None, &mut render_context, world, &[], None)?;
64+
finalizer(render_context.command_encoder());
65+
6366
{
6467
#[cfg(feature = "trace")]
6568
let _span = info_span!("submit_graph_commands").entered();

Diff for: crates/bevy_render/src/renderer/mod.rs

+7-2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ pub fn render_system(world: &mut World) {
3535
render_device.clone(), // TODO: is this clone really necessary?
3636
&render_queue.0,
3737
world,
38+
|encoder| {
39+
crate::view::screenshot::submit_screenshot_commands(world, encoder);
40+
},
3841
) {
3942
error!("Error running render graph:");
4043
{
@@ -66,8 +69,8 @@ pub fn render_system(world: &mut World) {
6669

6770
let mut windows = world.resource_mut::<ExtractedWindows>();
6871
for window in windows.values_mut() {
69-
if let Some(texture_view) = window.swap_chain_texture.take() {
70-
if let Some(surface_texture) = texture_view.take_surface_texture() {
72+
if let Some(wrapped_texture) = window.swap_chain_texture.take() {
73+
if let Some(surface_texture) = wrapped_texture.try_unwrap() {
7174
surface_texture.present();
7275
}
7376
}
@@ -81,6 +84,8 @@ pub fn render_system(world: &mut World) {
8184
);
8285
}
8386

87+
crate::view::screenshot::collect_screenshots(world);
88+
8489
// update the time and send it to the app world
8590
let time_sender = world.resource::<TimeSender>();
8691
time_sender.0.try_send(Instant::now()).expect(

Diff for: crates/bevy_render/src/texture/image_texture_conversion.rs

+15
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ impl Image {
174174
/// - `TextureFormat::R8Unorm`
175175
/// - `TextureFormat::Rg8Unorm`
176176
/// - `TextureFormat::Rgba8UnormSrgb`
177+
/// - `TextureFormat::Bgra8UnormSrgb`
177178
///
178179
/// To convert [`Image`] to a different format see: [`Image::convert`].
179180
pub fn try_into_dynamic(self) -> anyhow::Result<DynamicImage> {
@@ -196,6 +197,20 @@ impl Image {
196197
self.data,
197198
)
198199
.map(DynamicImage::ImageRgba8),
200+
// This format is commonly used as the format for the swapchain texture
201+
// This conversion is added here to support screenshots
202+
TextureFormat::Bgra8UnormSrgb => ImageBuffer::from_raw(
203+
self.texture_descriptor.size.width,
204+
self.texture_descriptor.size.height,
205+
{
206+
let mut data = self.data;
207+
for bgra in data.chunks_exact_mut(4) {
208+
bgra.swap(0, 2);
209+
}
210+
data
211+
},
212+
)
213+
.map(DynamicImage::ImageRgba8),
199214
// Throw and error if conversion isn't supported
200215
texture_format => {
201216
return Err(anyhow!(

0 commit comments

Comments
 (0)