Skip to content

Commit

Permalink
Add screenshot api
Browse files Browse the repository at this point in the history
  • Loading branch information
TheRawMeatball committed Jan 11, 2023
1 parent 6cc01c1 commit 7311b14
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 4 deletions.
2 changes: 2 additions & 0 deletions crates/bevy_render/Cargo.toml
Expand Up @@ -44,6 +44,7 @@ bevy_time = { path = "../bevy_time", version = "0.9.0" }
bevy_transform = { path = "../bevy_transform", version = "0.9.0" }
bevy_window = { path = "../bevy_window", version = "0.9.0" }
bevy_utils = { path = "../bevy_utils", version = "0.9.0" }
bevy_tasks = { path = "../bevy_tasks", version = "0.9.0" }

# rendering
image = { version = "0.24", default-features = false }
Expand Down Expand Up @@ -76,3 +77,4 @@ basis-universal = { version = "0.2.0", optional = true }
encase = { version = "0.4", features = ["glam"] }
# For wgpu profiling using tracing. Use `RUST_LOG=info` to also capture the wgpu spans.
profiling = { version = "1", features = ["profile-with-tracing"], optional = true }
async-channel = "1.4"
1 change: 1 addition & 0 deletions crates/bevy_render/src/lib.rs
Expand Up @@ -137,6 +137,7 @@ impl Plugin for RenderPlugin {
app.add_asset::<Shader>()
.add_debug_asset::<Shader>()
.init_asset_loader::<ShaderLoader>()
.init_resource::<view::screenshot::ScreenshotManager>()
.init_debug_asset_loader::<ShaderLoader>();

if let Some(backends) = self.wgpu_settings.backends {
Expand Down
8 changes: 8 additions & 0 deletions crates/bevy_render/src/render_resource/texture.rs
Expand Up @@ -93,6 +93,14 @@ impl TextureView {
TextureViewValue::SurfaceTexture { texture, .. } => texture.try_unwrap(),
}
}

#[inline]
pub(crate) fn get_surface_texture(&self) -> Option<&wgpu::SurfaceTexture> {
match &self.value {
TextureViewValue::TextureView(_) => None,
TextureViewValue::SurfaceTexture { texture, .. } => Some(&*texture),
}
}
}

impl From<wgpu::TextureView> for TextureView {
Expand Down
2 changes: 2 additions & 0 deletions crates/bevy_render/src/renderer/graph_runner.rs
Expand Up @@ -57,6 +57,7 @@ impl RenderGraphRunner {
render_device: RenderDevice,
queue: &wgpu::Queue,
world: &World,
finalizer: impl FnOnce(&mut wgpu::CommandEncoder),
) -> Result<(), RenderGraphRunnerError> {
let command_encoder =
render_device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
Expand All @@ -66,6 +67,7 @@ impl RenderGraphRunner {
};

Self::run_graph(graph, None, &mut render_context, world, &[])?;
finalizer(&mut render_context.command_encoder);
{
#[cfg(feature = "trace")]
let _span = info_span!("submit_graph_commands").entered();
Expand Down
6 changes: 6 additions & 0 deletions crates/bevy_render/src/renderer/mod.rs
Expand Up @@ -27,12 +27,16 @@ pub fn render_system(world: &mut World) {
let graph = world.resource::<RenderGraph>();
let render_device = world.resource::<RenderDevice>();
let render_queue = world.resource::<RenderQueue>();
let windows = world.resource::<ExtractedWindows>();

if let Err(e) = RenderGraphRunner::run(
graph,
render_device.clone(), // TODO: is this clone really necessary?
&render_queue.0,
world,
|encoder| {
crate::view::screenshot::submit_screenshot_commands(windows, encoder);
},
) {
error!("Error running render graph:");
{
Expand Down Expand Up @@ -79,6 +83,8 @@ pub fn render_system(world: &mut World) {
);
}

crate::view::screenshot::collect_screenshots(world);

// update the time and send it to the app world
let time_sender = world.resource::<TimeSender>();
time_sender.0.try_send(Instant::now()).expect(
Expand Down
13 changes: 13 additions & 0 deletions crates/bevy_render/src/texture/image_texture_conversion.rs
Expand Up @@ -174,6 +174,7 @@ impl Image {
/// - `TextureFormat::R8Unorm`
/// - `TextureFormat::Rg8Unorm`
/// - `TextureFormat::Rgba8UnormSrgb`
/// - `TextureFormat::Bgra8UnormSrgb`
///
/// To convert [`Image`] to a different format see: [`Image::convert`].
pub fn try_into_dynamic(self) -> anyhow::Result<DynamicImage> {
Expand All @@ -196,6 +197,18 @@ impl Image {
self.data,
)
.map(DynamicImage::ImageRgba8),
TextureFormat::Bgra8UnormSrgb => ImageBuffer::from_raw(
self.texture_descriptor.size.width,
self.texture_descriptor.size.height,
{
let mut data = self.data;
for bgra in data.chunks_exact_mut(4) {
bgra.swap(0, 2);
}
data
},
)
.map(DynamicImage::ImageRgba8),
// Throw and error if conversion isn't supported
texture_format => {
return Err(anyhow!(
Expand Down
40 changes: 36 additions & 4 deletions crates/bevy_render/src/view/window.rs
@@ -1,6 +1,9 @@
pub mod screenshot;

use crate::{
render_resource::TextureView,
render_resource::{Buffer, TextureView},
renderer::{RenderAdapter, RenderDevice, RenderInstance},
texture::TextureFormatPixelInfo,
Extract, RenderApp, RenderStage,
};
use bevy_app::{App, Plugin};
Expand All @@ -10,7 +13,9 @@ use bevy_window::{
CompositeAlphaMode, PresentMode, RawHandleWrapper, WindowClosed, WindowId, Windows,
};
use std::ops::{Deref, DerefMut};
use wgpu::TextureFormat;
use wgpu::{BufferUsages, TextureFormat};

use self::screenshot::ScreenshotManager;

/// Token to ensure a system runs on the main thread.
#[derive(Resource, Default)]
Expand Down Expand Up @@ -50,6 +55,8 @@ pub struct ExtractedWindow {
pub size_changed: bool,
pub present_mode_changed: bool,
pub alpha_mode: CompositeAlphaMode,
pub screenshot_func: Option<screenshot::ScreenshotFn>,
pub screenshot_buffer: Option<Buffer>,
}

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

fn extract_windows(
mut extracted_windows: ResMut<ExtractedWindows>,
screenshot_manager: Extract<Res<ScreenshotManager>>,
mut closed: Extract<EventReader<WindowClosed>>,
windows: Extract<Res<Windows>>,
) {
Expand All @@ -97,6 +105,8 @@ fn extract_windows(
size_changed: false,
present_mode_changed: false,
alpha_mode: window.alpha_mode(),
screenshot_func: None,
screenshot_buffer: None,
});

// NOTE: Drop the swap chain frame here
Expand Down Expand Up @@ -128,6 +138,11 @@ fn extract_windows(
for closed_window in closed.iter() {
extracted_windows.remove(&closed_window.id);
}
for (window, screenshot_func) in screenshot_manager.callbacks.lock().drain() {
if let Some(window) = extracted_windows.get_mut(&window) {
window.screenshot_func = Some(screenshot_func);
}
}
}

struct SurfaceData {
Expand Down Expand Up @@ -198,7 +213,7 @@ pub fn prepare_windows(
SurfaceData { surface, format }
});

let surface_configuration = wgpu::SurfaceConfiguration {
let mut surface_configuration = wgpu::SurfaceConfiguration {
format: surface_data.format,
width: window.physical_width,
height: window.physical_height,
Expand All @@ -218,6 +233,19 @@ pub fn prepare_windows(
CompositeAlphaMode::Inherit => wgpu::CompositeAlphaMode::Inherit,
},
};
if window.screenshot_func.is_some() {
surface_configuration.usage |= wgpu::TextureUsages::COPY_SRC;
window.screenshot_buffer = Some(render_device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: screenshot::get_aligned_size(
window.physical_width,
window.physical_height,
surface_data.format.pixel_size() as u32,
) as u64,
usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
mapped_at_creation: false,
}));
}

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

let surface = &surface_data.surface;
if not_already_configured || window.size_changed || window.present_mode_changed {
if not_already_configured
|| window.size_changed
|| window.present_mode_changed
|| window.screenshot_func.is_some()
{
render_device.configure_surface(surface, &surface_configuration);
let frame = surface
.get_current_texture()
Expand Down
175 changes: 175 additions & 0 deletions crates/bevy_render/src/view/window/screenshot.rs
@@ -0,0 +1,175 @@
use std::{num::NonZeroU32, path::Path};

use bevy_ecs::prelude::*;
use bevy_log::info_span;
use bevy_tasks::AsyncComputeTaskPool;
use bevy_utils::HashMap;
use bevy_window::WindowId;
use parking_lot::Mutex;
use thiserror::Error;
use wgpu::{
CommandEncoder, Extent3d, ImageDataLayout, TextureFormat, COPY_BYTES_PER_ROW_ALIGNMENT,
};

use crate::{prelude::Image, texture::TextureFormatPixelInfo};

use super::ExtractedWindows;

pub type ScreenshotFn = Box<dyn FnOnce(Image) + Send + Sync>;

/// A resource which allows for taking screenshots of the window.
#[derive(Resource, Default)]
pub struct ScreenshotManager {
// this is in a mutex to enable extraction with only an immutable reference
pub(crate) callbacks: Mutex<HashMap<WindowId, ScreenshotFn>>,
}

#[derive(Error, Debug)]
#[error("A screenshot for this window has already been requested.")]
pub struct ScreenshotAlreadyRequestedError;

impl ScreenshotManager {
/// Signals the renderer to take a screenshot of this frame.
///
/// The given callback will eventually be called on one of the [`AsyncComputeTaskPool`]s threads.
pub fn take_screenshot(
&mut self,
window: WindowId,
callback: impl FnOnce(Image) + Send + Sync + 'static,
) -> Result<(), ScreenshotAlreadyRequestedError> {
self.callbacks
.get_mut()
.try_insert(window, Box::new(callback))
.map(|_| ())
.map_err(|_| ScreenshotAlreadyRequestedError)
}

/// Signals the renderer to take a screenshot of this frame.
///
/// The screenshot will eventually be saved to the given path, and the format will be derived from the extension.
pub fn save_screenshot_to_disk(
&mut self,
window: WindowId,
path: impl AsRef<Path>,
) -> Result<(), ScreenshotAlreadyRequestedError> {
let path = path.as_ref().to_owned();
self.take_screenshot(window, |image| {
image.try_into_dynamic().unwrap().save(path).unwrap();
})
}
}

pub(crate) fn align_byte_size(value: u32) -> u32 {
value + (COPY_BYTES_PER_ROW_ALIGNMENT - (value % COPY_BYTES_PER_ROW_ALIGNMENT))
}

pub(crate) fn get_aligned_size(width: u32, height: u32, pixel_size: u32) -> u32 {
height * align_byte_size(width * pixel_size)
}

pub(crate) fn layout_data(width: u32, height: u32, format: TextureFormat) -> ImageDataLayout {
ImageDataLayout {
bytes_per_row: if height > 1 {
// 1 = 1 row
NonZeroU32::new(get_aligned_size(width, 1, format.pixel_size() as u32))
} else {
None
},
rows_per_image: None,
..Default::default()
}
}

pub(crate) fn submit_screenshot_commands(windows: &ExtractedWindows, encoder: &mut CommandEncoder) {
for (window, texture) in windows
.values()
.filter_map(|w| w.swap_chain_texture.as_ref().map(|t| (w, t)))
{
if let Some(screenshot_buffer) = &window.screenshot_buffer {
let width = window.physical_width;
let height = window.physical_height;
let texture_format = window.swap_chain_texture_format.unwrap();
let texture = &texture.get_surface_texture().unwrap().texture;

encoder.copy_texture_to_buffer(
texture.as_image_copy(),
wgpu::ImageCopyBuffer {
buffer: &screenshot_buffer,
layout: crate::view::screenshot::layout_data(width, height, texture_format),
},
Extent3d {
width,
height,
..Default::default()
},
);
}
}
}

pub(crate) fn collect_screenshots(world: &mut World) {
let _span = info_span!("collect_screenshots");

let mut windows = world.resource_mut::<ExtractedWindows>();
for window in windows.values_mut() {
if let Some(screenshot_func) = window.screenshot_func.take() {
let width = window.physical_width;
let height = window.physical_height;
let texture_format = window.swap_chain_texture_format.unwrap();
let pixel_size = texture_format.pixel_size();
let buffer = window.screenshot_buffer.take().unwrap();

let finish = async move {
let (tx, rx) = async_channel::bounded(1);
let buffer_slice = buffer.slice(..);
// The polling for this map call is done every frame when the command queue is submitted.
buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
let err = result.err();
if err.is_some() {
panic!("{}", err.unwrap().to_string());
}
tx.try_send(()).unwrap();
});
rx.recv().await.unwrap();
let data = buffer_slice.get_mapped_range();
// we immediately move the data to CPU memory to avoid holding the mapped view for long
let mut result = Vec::from(&*data);
drop(data);
drop(buffer_slice);
drop(buffer);

if result.len() != ((width * height) as usize * pixel_size) {
// Our buffer has been padded because we needed to align to a multiple of 256.
// We remove this padding here
let initial_row_bytes = width as usize * pixel_size;
let buffered_row_bytes = align_byte_size(width * pixel_size as u32) as usize;

let mut take_offset = buffered_row_bytes;
let mut place_offset = initial_row_bytes;
for _ in 1..height {
result.copy_within(
take_offset..take_offset + buffered_row_bytes,
place_offset,
);
take_offset += buffered_row_bytes;
place_offset += initial_row_bytes;
}
result.truncate(initial_row_bytes * height as usize);
}

screenshot_func(Image::new(
Extent3d {
width,
height,
depth_or_array_layers: 1,
},
wgpu::TextureDimension::D2,
result,
texture_format,
));
};

AsyncComputeTaskPool::get().spawn(finish).detach();
}
}
}

0 comments on commit 7311b14

Please sign in to comment.