Skip to content

Commit fddfbcf

Browse files
Add screenshot api
1 parent 6cc01c1 commit fddfbcf

File tree

8 files changed

+243
-4
lines changed

8 files changed

+243
-4
lines changed

crates/bevy_render/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ bevy_time = { path = "../bevy_time", version = "0.9.0" }
4444
bevy_transform = { path = "../bevy_transform", version = "0.9.0" }
4545
bevy_window = { path = "../bevy_window", version = "0.9.0" }
4646
bevy_utils = { path = "../bevy_utils", version = "0.9.0" }
47+
bevy_tasks = { path = "../bevy_tasks", version = "0.9.0" }
4748

4849
# rendering
4950
image = { version = "0.24", default-features = false }
@@ -76,3 +77,4 @@ basis-universal = { version = "0.2.0", optional = true }
7677
encase = { version = "0.4", features = ["glam"] }
7778
# For wgpu profiling using tracing. Use `RUST_LOG=info` to also capture the wgpu spans.
7879
profiling = { version = "1", features = ["profile-with-tracing"], optional = true }
80+
async-channel = "1.4"

crates/bevy_render/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ impl Plugin for RenderPlugin {
137137
app.add_asset::<Shader>()
138138
.add_debug_asset::<Shader>()
139139
.init_asset_loader::<ShaderLoader>()
140+
.init_resource::<view::screenshot::ScreenshotManager>()
140141
.init_debug_asset_loader::<ShaderLoader>();
141142

142143
if let Some(backends) = self.wgpu_settings.backends {

crates/bevy_render/src/render_resource/texture.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,14 @@ impl TextureView {
9393
TextureViewValue::SurfaceTexture { texture, .. } => texture.try_unwrap(),
9494
}
9595
}
96+
97+
#[inline]
98+
pub(crate) fn get_surface_texture(&self) -> Option<&wgpu::SurfaceTexture> {
99+
match &self.value {
100+
TextureViewValue::TextureView(_) => None,
101+
TextureViewValue::SurfaceTexture { texture, .. } => Some(&*texture),
102+
}
103+
}
96104
}
97105

98106
impl From<wgpu::TextureView> for TextureView {

crates/bevy_render/src/renderer/graph_runner.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ 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 command_encoder =
6263
render_device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
@@ -66,6 +67,7 @@ impl RenderGraphRunner {
6667
};
6768

6869
Self::run_graph(graph, None, &mut render_context, world, &[])?;
70+
finalizer(&mut render_context.command_encoder);
6971
{
7072
#[cfg(feature = "trace")]
7173
let _span = info_span!("submit_graph_commands").entered();

crates/bevy_render/src/renderer/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,16 @@ pub fn render_system(world: &mut World) {
2727
let graph = world.resource::<RenderGraph>();
2828
let render_device = world.resource::<RenderDevice>();
2929
let render_queue = world.resource::<RenderQueue>();
30+
let windows = world.resource::<ExtractedWindows>();
3031

3132
if let Err(e) = RenderGraphRunner::run(
3233
graph,
3334
render_device.clone(), // TODO: is this clone really necessary?
3435
&render_queue.0,
3536
world,
37+
|encoder| {
38+
crate::view::screenshot::submit_screenshot_commands(windows, encoder);
39+
},
3640
) {
3741
error!("Error running render graph:");
3842
{
@@ -79,6 +83,8 @@ pub fn render_system(world: &mut World) {
7983
);
8084
}
8185

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

crates/bevy_render/src/texture/image_texture_conversion.rs

Lines changed: 13 additions & 0 deletions
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,18 @@ impl Image {
196197
self.data,
197198
)
198199
.map(DynamicImage::ImageRgba8),
200+
TextureFormat::Bgra8UnormSrgb => ImageBuffer::from_raw(
201+
self.texture_descriptor.size.width,
202+
self.texture_descriptor.size.height,
203+
{
204+
let mut data = self.data;
205+
for bgra in data.chunks_exact_mut(4) {
206+
bgra.swap(0, 2);
207+
}
208+
data
209+
},
210+
)
211+
.map(DynamicImage::ImageRgba8),
199212
// Throw and error if conversion isn't supported
200213
texture_format => {
201214
return Err(anyhow!(

crates/bevy_render/src/view/window.rs

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
pub mod screenshot;
2+
13
use crate::{
2-
render_resource::TextureView,
4+
render_resource::{Buffer, TextureView},
35
renderer::{RenderAdapter, RenderDevice, RenderInstance},
6+
texture::TextureFormatPixelInfo,
47
Extract, RenderApp, RenderStage,
58
};
69
use bevy_app::{App, Plugin};
@@ -10,7 +13,9 @@ use bevy_window::{
1013
CompositeAlphaMode, PresentMode, RawHandleWrapper, WindowClosed, WindowId, Windows,
1114
};
1215
use std::ops::{Deref, DerefMut};
13-
use wgpu::TextureFormat;
16+
use wgpu::{BufferUsages, TextureFormat};
17+
18+
use self::screenshot::ScreenshotManager;
1419

1520
/// Token to ensure a system runs on the main thread.
1621
#[derive(Resource, Default)]
@@ -50,6 +55,8 @@ pub struct ExtractedWindow {
5055
pub size_changed: bool,
5156
pub present_mode_changed: bool,
5257
pub alpha_mode: CompositeAlphaMode,
58+
pub screenshot_func: Option<screenshot::ScreenshotFn>,
59+
pub screenshot_buffer: Option<Buffer>,
5360
}
5461

5562
#[derive(Default, Resource)]
@@ -73,6 +80,7 @@ impl DerefMut for ExtractedWindows {
7380

7481
fn extract_windows(
7582
mut extracted_windows: ResMut<ExtractedWindows>,
83+
screenshot_manager: Extract<Res<ScreenshotManager>>,
7684
mut closed: Extract<EventReader<WindowClosed>>,
7785
windows: Extract<Res<Windows>>,
7886
) {
@@ -97,6 +105,8 @@ fn extract_windows(
97105
size_changed: false,
98106
present_mode_changed: false,
99107
alpha_mode: window.alpha_mode(),
108+
screenshot_func: None,
109+
screenshot_buffer: None,
100110
});
101111

102112
// NOTE: Drop the swap chain frame here
@@ -128,6 +138,11 @@ fn extract_windows(
128138
for closed_window in closed.iter() {
129139
extracted_windows.remove(&closed_window.id);
130140
}
141+
for (window, screenshot_func) in screenshot_manager.callbacks.lock().drain() {
142+
if let Some(window) = extracted_windows.get_mut(&window) {
143+
window.screenshot_func = Some(screenshot_func);
144+
}
145+
}
131146
}
132147

133148
struct SurfaceData {
@@ -198,7 +213,7 @@ pub fn prepare_windows(
198213
SurfaceData { surface, format }
199214
});
200215

201-
let surface_configuration = wgpu::SurfaceConfiguration {
216+
let mut surface_configuration = wgpu::SurfaceConfiguration {
202217
format: surface_data.format,
203218
width: window.physical_width,
204219
height: window.physical_height,
@@ -218,6 +233,19 @@ pub fn prepare_windows(
218233
CompositeAlphaMode::Inherit => wgpu::CompositeAlphaMode::Inherit,
219234
},
220235
};
236+
if window.screenshot_func.is_some() {
237+
surface_configuration.usage |= wgpu::TextureUsages::COPY_SRC;
238+
window.screenshot_buffer = Some(render_device.create_buffer(&wgpu::BufferDescriptor {
239+
label: None,
240+
size: screenshot::get_aligned_size(
241+
window.physical_width,
242+
window.physical_height,
243+
surface_data.format.pixel_size() as u32,
244+
) as u64,
245+
usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
246+
mapped_at_creation: false,
247+
}));
248+
}
221249

222250
// A recurring issue is hitting `wgpu::SurfaceError::Timeout` on certain Linux
223251
// mesa driver implementations. This seems to be a quirk of some drivers.
@@ -239,7 +267,11 @@ pub fn prepare_windows(
239267
let not_already_configured = window_surfaces.configured_windows.insert(window.id);
240268

241269
let surface = &surface_data.surface;
242-
if not_already_configured || window.size_changed || window.present_mode_changed {
270+
if not_already_configured
271+
|| window.size_changed
272+
|| window.present_mode_changed
273+
|| window.screenshot_func.is_some()
274+
{
243275
render_device.configure_surface(surface, &surface_configuration);
244276
let frame = surface
245277
.get_current_texture()
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
use std::{num::NonZeroU32, path::Path};
2+
3+
use bevy_ecs::prelude::*;
4+
use bevy_log::info_span;
5+
use bevy_tasks::AsyncComputeTaskPool;
6+
use bevy_utils::HashMap;
7+
use bevy_window::WindowId;
8+
use parking_lot::Mutex;
9+
use thiserror::Error;
10+
use wgpu::{
11+
CommandEncoder, Extent3d, ImageDataLayout, TextureFormat, COPY_BYTES_PER_ROW_ALIGNMENT,
12+
};
13+
14+
use crate::{prelude::Image, texture::TextureFormatPixelInfo};
15+
16+
use super::ExtractedWindows;
17+
18+
pub type ScreenshotFn = Box<dyn FnOnce(Image) + Send + Sync>;
19+
20+
/// A resource which allows for taking screenshots of the window.
21+
#[derive(Resource, Default)]
22+
pub struct ScreenshotManager {
23+
// this is in a mutex to enable extraction with only an immutable reference
24+
pub(crate) callbacks: Mutex<HashMap<WindowId, ScreenshotFn>>,
25+
}
26+
27+
#[derive(Error, Debug)]
28+
#[error("A screenshot for this window has already been requested.")]
29+
pub struct ScreenshotAlreadyRequestedError;
30+
31+
impl ScreenshotManager {
32+
/// Signals the renderer to take a screenshot of this frame.
33+
///
34+
/// The given callback will eventually be called on one of the [`AsyncComputeTaskPool`]s threads.
35+
pub fn screenshot(
36+
&mut self,
37+
window: WindowId,
38+
callback: impl FnOnce(Image) + Send + Sync + 'static,
39+
) -> Result<(), ScreenshotAlreadyRequestedError> {
40+
self.callbacks
41+
.get_mut()
42+
.try_insert(window, Box::new(callback))
43+
.map(|_| ())
44+
.map_err(|_| ScreenshotAlreadyRequestedError)
45+
}
46+
47+
/// Signals the renderer to take a screenshot of this frame.
48+
///
49+
/// The screenshot will eventually be saved to the given path, and the format will be derived from the extension.
50+
pub fn save_screenshot_to_disk(
51+
&mut self,
52+
window: WindowId,
53+
path: impl AsRef<Path>,
54+
) -> Result<(), ScreenshotAlreadyRequestedError> {
55+
let path = path.as_ref().to_owned();
56+
self.screenshot(window, |image| {
57+
image.try_into_dynamic().unwrap().save(path).unwrap();
58+
})
59+
}
60+
}
61+
62+
pub(crate) fn align_byte_size(value: u32) -> u32 {
63+
value + (COPY_BYTES_PER_ROW_ALIGNMENT - (value % COPY_BYTES_PER_ROW_ALIGNMENT))
64+
}
65+
66+
pub(crate) fn get_aligned_size(width: u32, height: u32, pixel_size: u32) -> u32 {
67+
height * align_byte_size(width * pixel_size)
68+
}
69+
70+
pub(crate) fn layout_data(width: u32, height: u32, format: TextureFormat) -> ImageDataLayout {
71+
ImageDataLayout {
72+
bytes_per_row: if height > 1 {
73+
// 1 = 1 row
74+
NonZeroU32::new(get_aligned_size(width, 1, format.pixel_size() as u32))
75+
} else {
76+
None
77+
},
78+
rows_per_image: None,
79+
..Default::default()
80+
}
81+
}
82+
83+
pub(crate) fn submit_screenshot_commands(windows: &ExtractedWindows, encoder: &mut CommandEncoder) {
84+
for (window, texture) in windows
85+
.values()
86+
.filter_map(|w| w.swap_chain_texture.as_ref().map(|t| (w, t)))
87+
{
88+
if let Some(screenshot_buffer) = &window.screenshot_buffer {
89+
let width = window.physical_width;
90+
let height = window.physical_height;
91+
let texture_format = window.swap_chain_texture_format.unwrap();
92+
let texture = &texture.get_surface_texture().unwrap().texture;
93+
94+
encoder.copy_texture_to_buffer(
95+
texture.as_image_copy(),
96+
wgpu::ImageCopyBuffer {
97+
buffer: &screenshot_buffer,
98+
layout: crate::view::screenshot::layout_data(width, height, texture_format),
99+
},
100+
Extent3d {
101+
width,
102+
height,
103+
..Default::default()
104+
},
105+
);
106+
}
107+
}
108+
}
109+
110+
pub(crate) fn collect_screenshots(world: &mut World) {
111+
let _span = info_span!("collect_screenshots");
112+
113+
let mut windows = world.resource_mut::<ExtractedWindows>();
114+
for window in windows.values_mut() {
115+
if let Some(screenshot_func) = window.screenshot_func.take() {
116+
let width = window.physical_width;
117+
let height = window.physical_height;
118+
let texture_format = window.swap_chain_texture_format.unwrap();
119+
let pixel_size = texture_format.pixel_size();
120+
let buffer = window.screenshot_buffer.take().unwrap();
121+
122+
let finish = async move {
123+
let (tx, rx) = async_channel::bounded(1);
124+
let buffer_slice = buffer.slice(..);
125+
// The polling for this map call is done every frame when the command queue is submitted.
126+
buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
127+
let err = result.err();
128+
if err.is_some() {
129+
panic!("{}", err.unwrap().to_string());
130+
}
131+
tx.try_send(()).unwrap();
132+
});
133+
rx.recv().await.unwrap();
134+
let data = buffer_slice.get_mapped_range();
135+
// we immediately move the data to CPU memory to avoid holding the mapped view for long
136+
let mut result = Vec::from(&*data);
137+
drop(data);
138+
drop(buffer_slice);
139+
drop(buffer);
140+
141+
if result.len() != ((width * height) as usize * pixel_size) {
142+
// Our buffer has been padded because we needed to align to a multiple of 256.
143+
// We remove this padding here
144+
let initial_row_bytes = width as usize * pixel_size;
145+
let buffered_row_bytes = align_byte_size(width * pixel_size as u32) as usize;
146+
147+
let mut take_offset = buffered_row_bytes;
148+
let mut place_offset = initial_row_bytes;
149+
for _ in 1..height {
150+
result.copy_within(
151+
take_offset..take_offset + buffered_row_bytes,
152+
place_offset,
153+
);
154+
take_offset += buffered_row_bytes;
155+
place_offset += initial_row_bytes;
156+
}
157+
result.truncate(initial_row_bytes * height as usize);
158+
}
159+
160+
screenshot_func(Image::new(
161+
Extent3d {
162+
width,
163+
height,
164+
depth_or_array_layers: 1,
165+
},
166+
wgpu::TextureDimension::D2,
167+
result,
168+
texture_format,
169+
));
170+
};
171+
172+
AsyncComputeTaskPool::get().spawn(finish).detach();
173+
}
174+
}
175+
}

0 commit comments

Comments
 (0)