diff --git a/.gitignore b/.gitignore index 1dfa3a6e4..f74c76ac1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +capture_hi_res/ +simple_capture/ target/ **/*.rs.bk Cargo.lock diff --git a/Cargo.toml b/Cargo.toml index b95021977..7407e4300 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ rusttype = "0.8" serde = "1" serde_derive = "1" serde_json = "1" +threadpool = "1" toml = "0.5" walkdir = "2" wgpu = "0.4" @@ -69,6 +70,9 @@ path = "examples/simple_audio.rs" name = "simple_audio_file" path = "examples/simple_audio_file.rs" [[example]] +name = "simple_capture" +path = "examples/simple_capture.rs" +[[example]] name = "simple_draw" path = "examples/simple_draw.rs" [[example]] diff --git a/examples/capture_hi_res.rs b/examples/capture_hi_res.rs new file mode 100644 index 000000000..ef670543a --- /dev/null +++ b/examples/capture_hi_res.rs @@ -0,0 +1,200 @@ +// A demonstration of drawing to a very large texture, capturing the texture in its original size +// as a PNG and displaying a down-scaled version of the image within the window each frame. + +use nannou::prelude::*; + +fn main() { + nannou::app(model).update(update).exit(exit).run(); +} + +struct Model { + // The texture that we will draw to. + texture: wgpu::Texture, + // Create a `Draw` instance for drawing to our texture. + draw: nannou::Draw, + // The type used to render the `Draw` vertices to our texture. + renderer: nannou::draw::Renderer, + // The type used to capture the texture. + texture_capturer: wgpu::TextureCapturer, + // The type used to resize our texture to the window texture. + texture_reshaper: wgpu::TextureReshaper, +} + +fn model(app: &App) -> Model { + // Lets write to a 4K UHD texture. + let texture_size = [3_840, 2_160]; + + // Create the window. + let [win_w, win_h] = [texture_size[0] / 4, texture_size[1] / 4]; + let w_id = app + .new_window() + .size(win_w, win_h) + .title("nannou") + .view(view) + .build() + .unwrap(); + let window = app.window(w_id).unwrap(); + + // Retrieve the wgpu device. + let device = window.swap_chain_device(); + + // Create our custom texture. + let sample_count = window.msaa_samples(); + let texture = wgpu::TextureBuilder::new() + .size(texture_size) + // Our texture will be used as the OUTPUT_ATTACHMENT for our `Draw` render pass. + // It will also be SAMPLED by the `TextureCapturer` and `TextureResizer`. + .usage(wgpu::TextureUsage::OUTPUT_ATTACHMENT | wgpu::TextureUsage::SAMPLED) + // Use nannou's default multisampling sample count. + .sample_count(sample_count) + // Use a spacious 16-bit linear sRGBA format suitable for high quality drawing. + .format(wgpu::TextureFormat::Rgba16Unorm) + // Build it! + .build(device); + + // Create our `Draw` instance and a renderer for it. + let draw = nannou::Draw::new(); + let descriptor = texture.descriptor(); + let renderer = nannou::draw::Renderer::from_texture_descriptor(device, descriptor); + + // Create the texture capturer. + let texture_capturer = wgpu::TextureCapturer::with_num_threads(4); + + // Create the texture reshaper. + let texture_view = texture.create_default_view(); + let src_multisampled = texture.sample_count() > 1; + let dst_format = Frame::TEXTURE_FORMAT; + let texture_reshaper = wgpu::TextureReshaper::new( + device, + &texture_view, + src_multisampled, + sample_count, + dst_format, + ); + + // Make sure the directory where we will save images to exists. + std::fs::create_dir_all(&capture_directory(app)).unwrap(); + + Model { + texture, + draw, + renderer, + texture_capturer, + texture_reshaper, + } +} + +fn update(app: &App, model: &mut Model, _update: Update) { + // First, reset the `draw` state. + let draw = &model.draw; + draw.reset(); + + // Create a `Rect` for our texture to help with drawing. + let [w, h] = model.texture.size(); + let r = geom::Rect::from_w_h(w as f32, h as f32); + + // Use the frame number to animate, ensuring we get a constant update time. + let elapsed_frames = app.main_window().elapsed_frames(); + let t = elapsed_frames as f32 / 60.0; + + // Draw like we normally would in the `view`. + draw.background().color(BLACK); + let n_points = 10; + let weight = 8.0; + let hz = 6.0; + let vertices = (0..n_points) + .map(|i| { + let x = map_range(i, 0, n_points - 1, r.left(), r.right()); + let fract = i as f32 / n_points as f32; + let amp = (t + fract * hz * TAU).sin(); + let y = map_range(amp, -1.0, 1.0, r.bottom() * 0.75, r.top() * 0.75); + pt2(x, y) + }) + .enumerate() + .map(|(i, p)| { + let fract = i as f32 / n_points as f32; + let r = (t + fract) % 1.0; + let g = (t + 1.0 - fract) % 1.0; + let b = (t + 0.5 + fract) % 1.0; + let rgba = srgba(r, g, b, 1.0); + (p, rgba) + }); + draw.polyline() + .weight(weight) + .join_round() + .colored_points(vertices); + + // Draw frame number and size in bottom left. + let string = format!("Frame {} - {:?}", elapsed_frames, [w, h]); + let text = text(&string) + .font_size(48) + .left_justify() + .align_bottom() + .build(r.pad(r.h() * 0.05)); + draw.path().fill().color(WHITE).events(text.path_events()); + + // Render our drawing to the texture. + let window = app.main_window(); + let device = window.swap_chain_device(); + let ce_desc = wgpu::CommandEncoderDescriptor::default(); + let mut encoder = device.create_command_encoder(&ce_desc); + model + .renderer + .render_to_texture(device, &mut encoder, draw, &model.texture); + + // Take a snapshot of the texture. The capturer will do the following: + // + // 1. Resolve the texture to a non-multisampled texture if necessary. + // 2. Convert the format to non-linear 8-bit sRGBA ready for image storage. + // 3. Copy the result to a buffer ready to be mapped for reading. + let snapshot = model + .texture_capturer + .capture(device, &mut encoder, &model.texture); + + // Submit the commands for our drawing and texture capture to the GPU. + window + .swap_chain_queue() + .lock() + .unwrap() + .submit(&[encoder.finish()]); + + // Submit a function for writing our snapshot to a PNG. + // + // + // NOTE: It is essential that the commands for capturing the snapshot are `submit`ted before we + // attempt to read the snapshot - otherwise we will read a blank texture! + // + // NOTE: You can also use `read` instead of `read_threaded` if you want to read the texture on + // the current thread. This will slow down the main thread, but will allow the PNG writing to + // keep up with the main thread. + let path = capture_directory(app) + .join(elapsed_frames.to_string()) + .with_extension("png"); + snapshot.read_threaded(move |result| { + let image = result.expect("failed to map texture memory"); + image + .save(&path) + .expect("failed to save texture to png image"); + }); +} + +// Draw the state of your `Model` into the given `Frame` here. +fn view(_app: &App, model: &Model, frame: Frame) { + // Sample the texture and write it to the frame. + let mut encoder = frame.command_encoder(); + model + .texture_reshaper + .encode_render_pass(frame.texture_view(), &mut *encoder); +} + +// Wait for capture to finish. +fn exit(_app: &App, model: Model) { + println!("Waiting for PNG writing to complete..."); + model.texture_capturer.finish(); + println!("Done!"); +} + +// The directory where we'll save the frames. +fn capture_directory(app: &App) -> std::path::PathBuf { + app.project_dir().join(app.exe_name().unwrap()) +} diff --git a/examples/simple_capture.rs b/examples/simple_capture.rs new file mode 100644 index 000000000..be54a9b0d --- /dev/null +++ b/examples/simple_capture.rs @@ -0,0 +1,56 @@ +// This example is a copy of the `simple_draw.rs` example, but captures each frame and writes them +// as a PNG image file to `//nannou/simple_capture/.png` + +use nannou::prelude::*; + +fn main() { + nannou::sketch(view); +} + +fn view(app: &App, frame: Frame) { + let draw = app.draw(); + + draw.background().color(CORNFLOWERBLUE); + + let win = app.window_rect(); + draw.tri() + .points(win.bottom_left(), win.top_left(), win.top_right()) + .color(VIOLET); + + let t = app.time; + draw.ellipse() + .x_y(app.mouse.x * t.cos(), app.mouse.y) + .radius(win.w() * 0.125 * t.sin()) + .color(RED); + + draw.line() + .weight(10.0 + (t.sin() * 0.5 + 0.5) * 90.0) + .caps_round() + .color(PALEGOLDENROD) + .points(win.top_left() * t.sin(), win.bottom_right() * t.cos()); + + draw.quad() + .x_y(-app.mouse.x, app.mouse.y) + .color(DARKGREEN) + .rotate(t); + + draw.rect() + .x_y(app.mouse.y, app.mouse.x) + .w(app.mouse.x * 0.25) + .hsv(t, 1.0, 1.0); + + draw.to_frame(app, &frame).unwrap(); + + // Create a path that we want to save this frame to. + let file_path = app + .project_dir() + // Capture all frames to a directory called `//nannou/simple_capture`. + .join(app.exe_name().unwrap()) + // Name each file after the number of the frame. + .join(frame.nth().to_string()) + // The extension will be PNG. We also support tiff, bmp, gif, jpeg, webp and some others. + .with_extension("png"); + + // Capture the frame! + app.main_window().capture_frame(file_path); +} diff --git a/examples/wgpu/wgpu_teapot/wgpu_teapot.rs b/examples/wgpu/wgpu_teapot/wgpu_teapot.rs index d5707ea44..b1979562c 100644 --- a/examples/wgpu/wgpu_teapot/wgpu_teapot.rs +++ b/examples/wgpu/wgpu_teapot/wgpu_teapot.rs @@ -122,7 +122,7 @@ fn view(app: &App, model: &Model, frame: Frame) { let depth_size = g.depth_texture.size(); let frame_size = frame.texture_size(); let device = frame.device_queue_pair().device(); - if frame_size != [depth_size.width, depth_size.height] { + if frame_size != depth_size { let depth_format = g.depth_texture.format(); let sample_count = frame.texture_msaa_samples(); g.depth_texture = create_depth_texture(device, frame_size, depth_format, sample_count); diff --git a/examples/wgpu/wgpu_teapot_camera/wgpu_teapot_camera.rs b/examples/wgpu/wgpu_teapot_camera/wgpu_teapot_camera.rs index 6e8ac987e..9ffaa34d2 100644 --- a/examples/wgpu/wgpu_teapot_camera/wgpu_teapot_camera.rs +++ b/examples/wgpu/wgpu_teapot_camera/wgpu_teapot_camera.rs @@ -255,7 +255,7 @@ fn view(_app: &App, model: &Model, frame: Frame) { let depth_size = g.depth_texture.size(); let frame_size = frame.texture_size(); let device = frame.device_queue_pair().device(); - if frame_size != [depth_size.width, depth_size.height] { + if frame_size != depth_size { let depth_format = g.depth_texture.format(); let sample_count = frame.texture_msaa_samples(); g.depth_texture = create_depth_texture(device, frame_size, depth_format, sample_count); diff --git a/src/app.rs b/src/app.rs index d5813d5d4..2e9565117 100644 --- a/src/app.rs +++ b/src/app.rs @@ -488,7 +488,6 @@ impl App { pub const ASSETS_DIRECTORY_NAME: &'static str = "assets"; pub const DEFAULT_EXIT_ON_ESCAPE: bool = true; pub const DEFAULT_FULLSCREEN_ON_SHORTCUT: bool = true; - pub const DEFAULT_DRAW_DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float; // Create a new `App`. pub(super) fn new( @@ -726,13 +725,11 @@ impl App { let frame_dims: [u32; 2] = window.tracked_state.physical_size.into(); let msaa_samples = window.msaa_samples(); let target_format = crate::frame::Frame::TEXTURE_FORMAT; - let depth_format = Self::DEFAULT_DRAW_DEPTH_FORMAT; let renderer = draw::backend::wgpu::Renderer::new( device, frame_dims, msaa_samples, target_format, - depth_format, ); RefCell::new(renderer) }) @@ -769,6 +766,27 @@ impl App { pub fn fps(&self) -> f32 { self.duration.updates_per_second() } + + /// The name of the nannou executable that is currently running. + pub fn exe_name(&self) -> std::io::Result { + let string = std::env::current_exe()? + .file_stem() + .expect("exe path contained no file stem") + .to_string_lossy() + .to_string(); + Ok(string) + } + + /// The path to the current project directory. + /// + /// The current project directory is considered to be the directory containing the cargo + /// manifest (aka the `Cargo.toml` file). + /// + /// **Note:** Be careful not to rely on this directory for apps or sketches that you wish to + /// distribute! This directory is mostly useful for local sketches, experiments and testing. + pub fn project_dir(&self) -> &std::path::Path { + std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + } } impl Proxy { @@ -809,14 +827,7 @@ impl<'a> Draw<'a> { ); let scale_factor = window.tracked_state.scale_factor as _; let mut renderer = self.renderer.borrow_mut(); - let frame_dims = frame.texture_size(); - renderer.render_to_frame( - window.swap_chain_device(), - &self.draw, - scale_factor, - frame_dims, - frame, - ); + renderer.render_to_frame(window.swap_chain_device(), &self.draw, scale_factor, frame); Ok(()) } } @@ -983,7 +994,7 @@ fn run_loop( let window = windows .get(&window_id) .expect("failed to find window for redraw request"); - let render_data = &window.frame_render_data; + let frame_data = &window.frame_data; // Construct and emit a frame via `view` for receiving the user's graphics commands. let sf = window.tracked_state.scale_factor; @@ -1008,13 +1019,13 @@ fn run_loop( match window_view { Some(window::View::Sketch(view)) => { - let r_data = render_data.as_ref().expect("missing `render_data`"); - let frame = Frame::new_empty(raw_frame, r_data); + let data = frame_data.as_ref().expect("missing `frame_data`"); + let frame = Frame::new_empty(raw_frame, &data.render, &data.capture); view(&app, frame); } Some(window::View::WithModel(view)) => { - let r_data = render_data.as_ref().expect("missing `render_data`"); - let frame = Frame::new_empty(raw_frame, r_data); + let data = frame_data.as_ref().expect("missing `frame_data`"); + let frame = Frame::new_empty(raw_frame, &data.render, &data.capture); let view = view .to_fn_ptr::() .expect("unexpected model argument given to window view function"); @@ -1028,13 +1039,15 @@ fn run_loop( } None => match default_view { Some(View::Sketch(view)) => { - let r_data = render_data.as_ref().expect("missing `render_data`"); - let frame = Frame::new_empty(raw_frame, r_data); + let data = frame_data.as_ref().expect("missing `frame_data`"); + let frame = + Frame::new_empty(raw_frame, &data.render, &data.capture); view(&app, frame); } Some(View::WithModel(view)) => { - let r_data = render_data.as_ref().expect("missing `render_data`"); - let frame = Frame::new_empty(raw_frame, r_data); + let data = frame_data.as_ref().expect("missing `frame_data`"); + let frame = + Frame::new_empty(raw_frame, &data.render, &data.capture); view(&app, &model, frame); } None => raw_frame.submit(), diff --git a/src/draw/backend/wgpu/mod.rs b/src/draw/backend/wgpu/mod.rs index bf1aa276d..e4d3dcdf0 100644 --- a/src/draw/backend/wgpu/mod.rs +++ b/src/draw/backend/wgpu/mod.rs @@ -85,12 +85,44 @@ impl Vertex { } impl Renderer { + /// The default depth format + pub const DEFAULT_DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float; + + /// Create a **Renderer** targeting an output attachment texture of the given description. + pub fn from_texture_descriptor( + device: &wgpu::Device, + descriptor: &wgpu::TextureDescriptor, + ) -> Self { + Self::new( + device, + [descriptor.size.width, descriptor.size.height], + descriptor.sample_count, + descriptor.format, + ) + } + /// Construct a new `Renderer`. pub fn new( device: &wgpu::Device, output_attachment_size: [u32; 2], msaa_samples: u32, output_attachment_color_format: wgpu::TextureFormat, + ) -> Self { + Self::with_depth_format( + device, + output_attachment_size, + msaa_samples, + output_attachment_color_format, + Self::DEFAULT_DEPTH_FORMAT, + ) + } + + /// The same as **new**, but allows for manually specifying the depth format. + pub fn with_depth_format( + device: &wgpu::Device, + output_attachment_size: [u32; 2], + msaa_samples: u32, + output_attachment_color_format: wgpu::TextureFormat, depth_format: wgpu::TextureFormat, ) -> Self { // Load shader modules. @@ -137,15 +169,22 @@ impl Renderer { } } + /// Encode a render pass with the given **Draw**ing to the given `output_attachment`. + /// + /// If the **Draw**ing has been scaled for handling DPI, specify the necessary `scale_factor` + /// for scaling back to the `output_attachment_size` (physical dimensions). + /// + /// If the `output_attachment` is multisampled and should be resolved to another texture, + /// include the `resolve_target`. pub fn encode_render_pass( &mut self, device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, draw: &draw::Draw, scale_factor: f32, output_attachment_size: [u32; 2], output_attachment: &wgpu::TextureView, resolve_target: Option<&wgpu::TextureView>, - encoder: &mut wgpu::CommandEncoder, ) where S: BaseFloat, { @@ -161,7 +200,7 @@ impl Renderer { // Resize the depth texture if the output attachment size has changed. let depth_size = depth_texture.size(); - if output_attachment_size != [depth_size.width, depth_size.height] { + if output_attachment_size != depth_size { let depth_format = depth_texture.format(); let sample_count = depth_texture.sample_count(); *depth_texture = @@ -229,27 +268,56 @@ impl Renderer { render_pass.draw_indexed(index_range, start_vertex, instance_range); } + /// Encode the necessary commands to render the contents of the given **Draw**ing to the given + /// **Texture**. + pub fn render_to_texture( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + draw: &draw::Draw, + texture: &wgpu::Texture, + ) where + S: BaseFloat, + { + let size = texture.size(); + let view = texture.create_default_view(); + // TODO: Should we expose this for rendering to textures? + let scale_factor = 1.0; + let resolve_target = None; + self.encode_render_pass( + device, + encoder, + draw, + scale_factor, + size, + &view, + resolve_target, + ); + } + + /// Encode the necessary commands to render the contents of the given **Draw**ing to the given + /// **Frame**. pub fn render_to_frame( &mut self, device: &wgpu::Device, draw: &draw::Draw, scale_factor: f32, - frame_dims: [u32; 2], frame: &Frame, ) where S: BaseFloat, { + let size = frame.texture().size(); let attachment = frame.texture_view(); - let resolve_target = frame.resolve_target(); + let resolve_target = None; let mut command_encoder = frame.command_encoder(); self.encode_render_pass( device, + &mut *command_encoder, draw, scale_factor, - frame_dims, + size, attachment, resolve_target, - &mut *command_encoder, ); } } diff --git a/src/draw/mod.rs b/src/draw/mod.rs index 36ef3bbd5..c1a650581 100644 --- a/src/draw/mod.rs +++ b/src/draw/mod.rs @@ -11,6 +11,7 @@ use std::cell::{Ref, RefCell}; use std::collections::HashMap; use std::{fmt, mem, ops}; +pub use self::backend::wgpu::Renderer; pub use self::background::Background; pub use self::drawing::{Drawing, DrawingContext}; pub use self::mesh::intermediary::{ diff --git a/src/frame/mod.rs b/src/frame/mod.rs index 96c3a1092..5cd8775a2 100644 --- a/src/frame/mod.rs +++ b/src/frame/mod.rs @@ -3,6 +3,8 @@ use crate::color::IntoLinSrgba; use crate::wgpu; use std::ops; +use std::path::PathBuf; +use std::sync::Mutex; pub mod raw; @@ -18,7 +20,8 @@ pub use self::raw::RawFrame; /// intermediary image. pub struct Frame<'swap_chain> { raw_frame: RawFrame<'swap_chain>, - data: &'swap_chain RenderData, + render_data: &'swap_chain RenderData, + capture_data: &'swap_chain CaptureData, } /// Data specific to the intermediary textures. @@ -28,7 +31,14 @@ pub struct RenderData { msaa_samples: u32, size: [u32; 2], // For writing the intermediary linear sRGBA texture to the swap chain texture. - texture_format_converter: wgpu::TextureFormatConverter, + texture_reshaper: wgpu::TextureReshaper, +} + +/// Data related to the capturing of a frame. +#[derive(Debug, Default)] +pub(crate) struct CaptureData { + pub(crate) next_frame_path: Mutex>, + texture_capturer: wgpu::TextureCapturer, } /// Intermediary textures used as a target before resolving multisampling and writing to the @@ -61,40 +71,79 @@ impl<'swap_chain> Frame<'swap_chain> { // Initialise a new empty frame ready for "drawing". pub(crate) fn new_empty( raw_frame: RawFrame<'swap_chain>, - data: &'swap_chain RenderData, + render_data: &'swap_chain RenderData, + capture_data: &'swap_chain CaptureData, ) -> Self { - Frame { raw_frame, data } + Frame { + raw_frame, + render_data, + capture_data, + } } // The private implementation of `submit`, allowing it to be called during `drop` if submission // has not yet occurred. fn submit_inner(&mut self) { let Frame { - ref data, + ref capture_data, + ref render_data, ref mut raw_frame, } = *self; // Resolve the MSAA if necessary. - if let Some((_, ref msaa_texture_view)) = data.intermediary_lin_srgba.msaa_texture { + if let Some((_, ref msaa_texture_view)) = render_data.intermediary_lin_srgba.msaa_texture { let mut encoder = raw_frame.command_encoder(); wgpu::resolve_texture( msaa_texture_view, - &data.intermediary_lin_srgba.texture_view, + &render_data.intermediary_lin_srgba.texture_view, &mut *encoder, ); } + // Check to see if the user specified capturing the frame. + let mut snapshot_capture = None; + if let Ok(mut guard) = capture_data.next_frame_path.lock() { + if let Some(path) = guard.take() { + let device = raw_frame.device_queue_pair().device(); + let mut encoder = raw_frame.command_encoder(); + let snapshot = capture_data.texture_capturer.capture( + device, + &mut *encoder, + &render_data.intermediary_lin_srgba.texture, + ); + snapshot_capture = Some((path, snapshot)); + } + } + // Convert the linear sRGBA image to the swapchain image. // // To do so, we sample the linear sRGBA image and draw it to the swapchain image using // two triangles and a fragment shader. { let mut encoder = raw_frame.command_encoder(); - data.texture_format_converter + render_data + .texture_reshaper .encode_render_pass(raw_frame.swap_chain_texture(), &mut *encoder); } + // Submit all commands on the device queue. raw_frame.submit_inner(); + + // If the user did specify capturing the frame, submit the asynchronous read. + if let Some((path, snapshot)) = snapshot_capture { + snapshot.read_threaded(move |result| match result { + Err(e) => eprintln!("failed to asynchronously read captured frame: {:?}", e), + Ok(image) => { + if let Err(e) = image.save(&path) { + eprintln!( + "failed to save captured frame to \"{}\": {}", + path.display(), + e + ); + } + } + }); + } } /// The texture to which all use graphics should be drawn this frame. @@ -115,32 +164,32 @@ impl<'swap_chain> Frame<'swap_chain> { /// After the texture has been resolved if necessary, it will then be used as a shader input /// within a graphics pipeline used to draw the swapchain texture. pub fn texture(&self) -> &wgpu::Texture { - self.data + self.render_data .intermediary_lin_srgba .msaa_texture .as_ref() .map(|(tex, _)| tex) - .unwrap_or(&self.data.intermediary_lin_srgba.texture) + .unwrap_or(&self.render_data.intermediary_lin_srgba.texture) } /// A full view into the frame's texture. /// /// See `texture` for details. pub fn texture_view(&self) -> &wgpu::TextureView { - self.data + self.render_data .intermediary_lin_srgba .msaa_texture .as_ref() .map(|(_, view)| view) - .unwrap_or(&self.data.intermediary_lin_srgba.texture_view) + .unwrap_or(&self.render_data.intermediary_lin_srgba.texture_view) } /// Returns the resolve target texture in the case that MSAA is enabled. pub fn resolve_target(&self) -> Option<&wgpu::TextureView> { - if self.data.msaa_samples <= 1 { + if self.render_data.msaa_samples <= 1 { None } else { - Some(&self.data.intermediary_lin_srgba.texture_view) + Some(&self.render_data.intermediary_lin_srgba.texture_view) } } @@ -152,12 +201,12 @@ impl<'swap_chain> Frame<'swap_chain> { /// The number of MSAA samples of the `Frame`'s intermediary linear sRGBA texture. pub fn texture_msaa_samples(&self) -> u32 { - self.data.msaa_samples + self.render_data.msaa_samples } /// The size of the frame's texture in pixels. pub fn texture_size(&self) -> [u32; 2] { - self.data.size + self.render_data.size } /// Short-hand for constructing a `wgpu::RenderPassColorAttachmentDescriptor` for use within a @@ -170,8 +219,8 @@ impl<'swap_chain> Frame<'swap_chain> { pub fn color_attachment_descriptor(&self) -> wgpu::RenderPassColorAttachmentDescriptor { let load_op = wgpu::LoadOp::Load; let store_op = wgpu::StoreOp::Store; - let attachment = match self.data.intermediary_lin_srgba.msaa_texture { - None => &self.data.intermediary_lin_srgba.texture_view, + let attachment = match self.render_data.intermediary_lin_srgba.msaa_texture { + None => &self.render_data.intermediary_lin_srgba.texture_view, Some((_, ref msaa_texture_view)) => msaa_texture_view, }; let resolve_target = None; @@ -229,14 +278,18 @@ impl RenderData { ) -> Self { let intermediary_lin_srgba = create_intermediary_lin_srgba(device, swap_chain_dims, msaa_samples); - let texture_format_converter = wgpu::TextureFormatConverter::new( + let swap_chain_sample_count = 1; + let src_multisampled = false; + let texture_reshaper = wgpu::TextureReshaper::new( device, &intermediary_lin_srgba.texture_view, + src_multisampled, + swap_chain_sample_count, swap_chain_format, ); RenderData { intermediary_lin_srgba, - texture_format_converter, + texture_reshaper, size: swap_chain_dims, msaa_samples, } diff --git a/src/wgpu/mod.rs b/src/wgpu/mod.rs index 5a739dce8..135823dfb 100644 --- a/src/wgpu/mod.rs +++ b/src/wgpu/mod.rs @@ -15,10 +15,18 @@ pub use self::device_map::{ ActiveAdapter, AdapterMap, AdapterMapKey, DeviceMap, DeviceMapKey, DeviceQueuePair, }; pub use self::sampler_builder::SamplerBuilder; -pub use self::texture::format_converter::FormatConverter as TextureFormatConverter; -pub use self::texture::image::format_from_image_color_type as texture_format_from_image_color_type; +pub use self::texture::capturer::{ + Capturer as TextureCapturer, Rgba8AsyncMapping, Snapshot as TextureSnapshot, +}; +pub use self::texture::image::{ + format_from_image_color_type as texture_format_from_image_color_type, BufferImage, + ImageAsyncMapping, +}; +pub use self::texture::reshaper::Reshaper as TextureReshaper; pub use self::texture::{ - format_size_bytes as texture_format_size_bytes, Builder as TextureBuilder, Texture, + descriptor_eq as texture_descriptor_eq, extent_3d_eq, + format_size_bytes as texture_format_size_bytes, BufferBytes, Builder as TextureBuilder, + Texture, }; #[doc(inline)] pub use wgpu::{ diff --git a/src/wgpu/texture/capturer.rs b/src/wgpu/texture/capturer.rs new file mode 100644 index 000000000..7731f3314 --- /dev/null +++ b/src/wgpu/texture/capturer.rs @@ -0,0 +1,300 @@ +use crate::wgpu; +use std::ops::Deref; +use std::sync::{Arc, Mutex}; +use threadpool::ThreadPool; + +/// A type dedicated to capturing a texture as a non-linear sRGBA image that can be read on the +/// CPU. +/// +/// Calling **capture** will return a **Snapshot** that may be read after the given command encoder +/// has been submitted. **Snapshot**s can be read on the current thread via **read** or on a thread +/// pool via **read_threaded**. +/// +/// If the **Capturer** is dropped while threaded callbacks are still being processed, the drop +/// implementation will block the current thread. +#[derive(Debug)] +pub struct Capturer { + converter_data_pair: Mutex>, + thread_pool: Arc>>>, + num_threads: usize, +} + +/// A snapshot captured by a **Capturer**. +/// +/// A snapshot is a thin wrapper around a **wgpu::BufferImage** that knows that the image format is +/// specifically non-linear sRGBA8. +pub struct Snapshot { + buffer: wgpu::BufferImage, + thread_pool: Arc>>>, + num_threads: usize, +} + +/// A wrapper around a slice of bytes representing a non-linear sRGBA image. +/// +/// An **ImageAsyncMapping** may only be created by reading from a **Snapshot** returned by a +/// `Texture::to_image` call. +pub struct Rgba8AsyncMapping<'a> { + mapping: wgpu::ImageAsyncMapping<'a>, +} + +#[derive(Debug)] +struct ConverterDataPair { + src_descriptor: wgpu::TextureDescriptor, + reshaper: wgpu::TextureReshaper, + resolved_src_texture: Option, + dst_texture: wgpu::Texture, +} + +/// An alias for the image buffer that can be read from a captured **Snapshot**. +pub struct Rgba8AsyncMappedImageBuffer<'a>( + image::ImageBuffer, Rgba8AsyncMapping<'a>>, +); + +impl Capturer { + /// The format to which textures will be converted before being mapped back to the CPU. + pub const DST_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb; + + /// Create a new **TextureCapturer**. + /// + /// Note that a **TextureCapturer** must only be used with a single texture. If you require + /// capturing multiple textures, you may create multiple **TextureCapturers**. + pub fn new() -> Self { + Self::with_num_threads(1) + } + + /// The same as **new** but allows for specifying the number of threads to use when processing + /// callbacks submitted to `read_threaded` on produced snapshots. + /// + /// By default, **Capturer** uses a single dedicated thread. This reduces the chance that the + /// thread will interfere with the core running the main event loop, but also reduces the + /// amount of processing power applied to processing callbacks in turn increasing the chance + /// that the thread may fall behind under heavy load. This constructor is provided to allow for + /// users to choose how to handle this trade-off. + pub fn with_num_threads(num_threads: usize) -> Self { + Self { + converter_data_pair: Default::default(), + thread_pool: Default::default(), + num_threads, + } + } + + /// Capture the given texture at the state of the given command encoder. + pub fn capture( + &self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + src_texture: &wgpu::Texture, + ) -> Snapshot { + let buffer_image = if src_texture.format() != Self::DST_FORMAT { + let mut converter_data_pair = self + .converter_data_pair + .lock() + .expect("failed to lock converter"); + + // Create converter and target texture if they don't exist. + let converter_data_pair = converter_data_pair + .get_or_insert_with(|| create_converter_data_pair(device, src_texture)); + + // If the texture has changed in some way, recreate the converter. + if !wgpu::texture_descriptor_eq( + src_texture.descriptor(), + &converter_data_pair.src_descriptor, + ) { + *converter_data_pair = create_converter_data_pair(device, src_texture); + } + + // If the src is multisampled, add the resolve command. + if let Some(ref resolved_src_texture) = converter_data_pair.resolved_src_texture { + let src_view = src_texture.create_default_view(); + let resolved_view = resolved_src_texture.create_default_view(); + wgpu::resolve_texture(&src_view, &resolved_view, encoder); + } + + // Encode the texture format conversion. + let dst_view = converter_data_pair.dst_texture.create_default_view(); + converter_data_pair + .reshaper + .encode_render_pass(&dst_view, encoder); + + converter_data_pair + .dst_texture + .to_image(device, encoder) + .expect("texture has unsupported format") + } else { + src_texture + .to_image(device, encoder) + .expect("texture has unsupported format") + }; + + Snapshot { + buffer: buffer_image, + thread_pool: self.thread_pool.clone(), + num_threads: self.num_threads, + } + } + + /// Finish capturing and wait for any threaded callbacks to complete if there are any. + pub fn finish(self) { + self.finish_inner() + } + + fn finish_inner(&self) { + let mut guard = self + .thread_pool + .lock() + .expect("failed to acquire thread handle"); + if let Some(thread_pool) = guard.take() { + thread_pool.join(); + } + } +} + +impl Snapshot { + /// Reads the non-linear sRGBA image from mapped memory. + /// + /// Specifically, this asynchronously maps the buffer of bytes from GPU to host memory and, + /// once mapped, calls the given user callback with the data represented as an + /// `Rgba8AsyncMapping`. + /// + /// Note: The given callback will not be called until the memory is mapped and the device is + /// polled. You should not rely on the callback being called immediately. + /// + /// The given callback will be called on the current thread. If you would like the callback to + /// be processed on a thread pool, see the `read_threaded` method. + pub fn read(&self, callback: F) + where + F: 'static + FnOnce(Result), + { + let [width, height] = self.buffer.size(); + self.buffer.read(move |result| { + let result = result.map(move |mapping| { + let mapping = Rgba8AsyncMapping { mapping }; + Rgba8AsyncMappedImageBuffer( + image::ImageBuffer::from_raw(width, height, mapping) + .expect("image buffer dimensions did not match mapping"), + ) + }); + callback(result); + }) + } + + /// Similar to `read`, but rather than delivering the mapped memory directly to the callback, + /// this method will first clone the mapped data, send it to another thread and then call the + /// callback from the other thread. + /// + /// This is useful when the callback performs an operation that could take a long or unknown + /// amount of time (e.g. writing the image to disk). + /// + /// Note however that if this method is called repeatedly (e.g. every frame) and the given + /// callback takes longer than the interval between calls, then the underlying thread will fall + /// behind and may take a while to complete by the time the application has exited. + pub fn read_threaded(&self, callback: F) + where + F: 'static + Send + FnOnce(Result, Vec>, ()>), + { + let mut guard = self + .thread_pool + .lock() + .expect("failed to acquire thread handle"); + let thread_pool = guard.get_or_insert_with(|| Arc::new(ThreadPool::new(self.num_threads))); + let thread_pool = thread_pool.clone(); + self.read(move |result| { + let result = result.map(|img| img.to_owned()); + thread_pool.execute(|| callback(result)); + }); + } +} + +impl<'a> Rgba8AsyncMappedImageBuffer<'a> { + /// Convert the mapped image buffer to an owned buffer. + pub fn to_owned(&self) -> image::ImageBuffer, Vec> { + let vec = self.as_flat_samples().as_slice().to_vec(); + let (width, height) = self.dimensions(); + image::ImageBuffer::from_raw(width, height, vec) + .expect("image buffer dimensions do not match vec len") + } +} + +impl Default for Capturer { + fn default() -> Self { + Self::new() + } +} + +impl Drop for Capturer { + fn drop(&mut self) { + self.finish_inner() + } +} + +impl<'a> Deref for Rgba8AsyncMapping<'a> { + type Target = [u8]; + fn deref(&self) -> &Self::Target { + self.as_ref() + } +} + +impl<'a> Deref for Rgba8AsyncMappedImageBuffer<'a> { + type Target = image::ImageBuffer, Rgba8AsyncMapping<'a>>; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'a> AsRef<[u8]> for Rgba8AsyncMapping<'a> { + fn as_ref(&self) -> &[u8] { + &self.mapping.mapping().data + } +} + +// Create the format converter and the target texture. +fn create_converter_data_pair( + device: &wgpu::Device, + src_texture: &wgpu::Texture, +) -> ConverterDataPair { + // If the src is multisampled, it must be resolved first. + let resolved_src_texture = if src_texture.sample_count() > 1 { + let texture = wgpu::TextureBuilder::from(src_texture.descriptor_cloned()) + .sample_count(1) + .usage(wgpu::TextureUsage::OUTPUT_ATTACHMENT | wgpu::TextureUsage::SAMPLED) + .build(device); + Some(texture) + } else { + None + }; + + // Create the destination format texture. + let dst_texture = wgpu::TextureBuilder::from(src_texture.descriptor_cloned()) + .sample_count(1) + .format(Capturer::DST_FORMAT) + .usage(wgpu::TextureUsage::OUTPUT_ATTACHMENT | wgpu::TextureUsage::COPY_SRC) + .build(device); + + // If we have a resolved texture, use it as the conversion src. Otherwise use `src_texture`. + let src_view = resolved_src_texture + .as_ref() + .map(|tex| tex.create_default_view()) + .unwrap_or_else(|| src_texture.create_default_view()); + + // Create the converter. + let dst_format = dst_texture.format(); + let src_multisampled = src_texture.sample_count() > 1; + let dst_sample_count = 1; + let reshaper = wgpu::TextureReshaper::new( + device, + &src_view, + src_multisampled, + dst_sample_count, + dst_format, + ); + + // Keep track of the `src_descriptor` to check if we need to recreate the converter. + let src_descriptor = src_texture.descriptor_cloned(); + + ConverterDataPair { + src_descriptor, + reshaper, + resolved_src_texture, + dst_texture, + } +} diff --git a/src/wgpu/texture/image.rs b/src/wgpu/texture/image.rs index 9a15bbfb3..cfbd8611d 100644 --- a/src/wgpu/texture/image.rs +++ b/src/wgpu/texture/image.rs @@ -2,6 +2,8 @@ //! textures from the wgpu crate (images in GPU memory). use crate::wgpu; +use std::path::Path; +use std::slice; /// The set of pixel types from the image crate that can be loaded directly into a texture. /// @@ -14,6 +16,24 @@ pub trait Pixel: image::Pixel { const TEXTURE_FORMAT: wgpu::TextureFormat; } +/// A wrapper around a wgpu buffer that contains an image of a known size and `image::ColorType`. +#[derive(Debug)] +pub struct BufferImage { + color_type: image::ColorType, + size: [u32; 2], + buffer: wgpu::BufferBytes, +} + +/// A wrapper around a slice of bytes representing an image. +/// +/// An `ImageAsyncMapping` may only be created by reading from a `BufferImage` returned by a +/// `Texture::to_image` call. +pub struct ImageAsyncMapping<'a> { + color_type: image::ColorType, + size: [u32; 2], + mapping: wgpu::BufferAsyncMapping<&'a [u8]>, +} + impl wgpu::TextureBuilder { /// Produce a texture descriptor from an image. /// @@ -119,6 +139,127 @@ impl wgpu::Texture { { encode_load_texture_array_from_image_buffers(device, encoder, usage, buffers) } + + /// Write the contents of the texture into a new image buffer. + /// + /// Commands will be added to the given encoder to copy the entire contents of the texture into + /// the buffer. + /// + /// Returns a buffer from which the image can be read asynchronously via `read`. + /// + /// Returns `None` if there is no directly compatible `image::ColorType` for the texture's format. + /// + /// NOTE: `read` should not be called on the returned buffer until the encoded commands have + /// been submitted to the device queue. + pub fn to_image( + &self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + ) -> Option { + let color_type = image_color_type_from_format(self.format())?; + let size = self.size(); + let buffer = self.to_buffer_bytes(device, encoder); + Some(BufferImage { + color_type, + size, + buffer, + }) + } +} + +impl BufferImage { + /// The dimensions of the image stored within the buffer. + pub fn size(&self) -> [u32; 2] { + self.size + } + + /// The color type of the image stored within the buffer. + pub fn color_type(&self) -> image::ColorType { + self.color_type + } + + /// Asynchronously maps the buffer of bytes from GPU to host memory and, once mapped, calls the + /// given user callback with the data represented as an `ImageAsyncMapping`. + /// + /// Note: The given callback will not be called until the memory is mapped and the device is + /// polled. You should not rely on the callback being called immediately. + pub fn read(&self, callback: F) + where + F: 'static + FnOnce(Result), + { + let size = self.size; + let color_type = self.color_type; + self.buffer.read(move |result| { + let result = result.map(|mapping| ImageAsyncMapping { + color_type, + size, + mapping, + }); + callback(result); + }) + } +} + +impl<'a> ImageAsyncMapping<'a> { + /// Produce the color type of an image, compatible with the `image` crate. + pub fn color_type(&self) -> image::ColorType { + self.color_type + } + + /// The dimensions of the image. + pub fn size(&self) -> [u32; 2] { + self.size + } + + /// The raw image data as a slice of bytes. + pub fn mapping(&self) -> &wgpu::BufferAsyncMapping<&[u8]> { + &self.mapping + } + + /// Saves the buffer to a file at the specified path. + /// + /// The image format is derived from the file extension. + pub fn save(&self, path: &Path) -> image::ImageResult<()> { + let [width, height] = self.size(); + image::save_buffer(path, &self.mapping.data, width, height, self.color_type) + } + + /// Saves the buffer to a file at the specified path. + pub fn save_with_format( + &self, + path: &Path, + format: image::ImageFormat, + ) -> image::ImageResult<()> { + let [width, height] = self.size(); + image::save_buffer_with_format( + path, + &self.mapping.data, + width, + height, + self.color_type, + format, + ) + } + + /// Attempt to cast this image ref to an `ImageBuffer` of the specified pixel type. + /// + /// Returns `None` if the specified pixel type does not match the inner `color_type`. + pub fn as_image_buffer

(&self) -> Option> + where + P: 'static + Pixel, + { + if P::COLOR_TYPE != self.color_type { + return None; + } + let [width, height] = self.size(); + let len_pixels = (width * height) as usize; + let subpixel_data_ptr = self.mapping.data.as_ptr() as *const _; + let subpixel_data: &[P::Subpixel] = + unsafe { slice::from_raw_parts(subpixel_data_ptr, len_pixels) }; + let img_buffer = image::ImageBuffer::from_raw(width, height, subpixel_data) + .expect("failed to construct image buffer from raw data"); + Some(img_buffer) + } } impl Pixel for image::Bgra { @@ -193,6 +334,26 @@ pub fn format_from_image_color_type(color_type: image::ColorType) -> Option Option { + let color_type = match format { + // TODO: Should we add branches for other same-size formats? e.g. R8Snorm, R8Uint, etc? + wgpu::TextureFormat::R8Unorm => image::ColorType::L8, + wgpu::TextureFormat::Rg8Unorm => image::ColorType::La8, + wgpu::TextureFormat::Rgba8UnormSrgb => image::ColorType::Rgba8, + wgpu::TextureFormat::R16Unorm => image::ColorType::L16, + wgpu::TextureFormat::Rg16Unorm => image::ColorType::La16, + wgpu::TextureFormat::Rgba16Unorm => image::ColorType::Rgba16, + wgpu::TextureFormat::Bgra8UnormSrgb => image::ColorType::Bgra8, + _ => return None, + }; + Some(color_type) +} + /// Produce a texture descriptor from any type implementing `image::GenericImageView` whose `Pixel` /// type implements `Pixel`. /// @@ -265,7 +426,7 @@ where // Submit command for copying pixel data to the texture. let buffer_copy_view = texture.create_default_buffer_copy_view(&buffer); let texture_copy_view = texture.create_default_copy_view(); - let extent = texture.size(); + let extent = texture.extent(); encoder.copy_buffer_to_texture(buffer_copy_view, texture_copy_view, extent); texture @@ -345,7 +506,7 @@ where let buffer_copy_view = texture.create_default_buffer_copy_view(&buffer); let mut texture_copy_view = texture.create_default_copy_view(); texture_copy_view.array_layer = layer as u32; - let extent = texture.size(); + let extent = texture.extent(); encoder.copy_buffer_to_texture(buffer_copy_view, texture_copy_view, extent); } diff --git a/src/wgpu/texture/mod.rs b/src/wgpu/texture/mod.rs index 1a7b6fab3..edd16e16c 100644 --- a/src/wgpu/texture/mod.rs +++ b/src/wgpu/texture/mod.rs @@ -1,7 +1,9 @@ -use crate::wgpu::TextureHandle; +use crate::wgpu::{self, TextureHandle}; +use std::ops::Deref; -pub mod format_converter; +pub mod capturer; pub mod image; +pub mod reshaper; /// A convenient wrapper around a handle to a texture on the GPU along with its descriptor. /// @@ -25,6 +27,13 @@ pub struct Builder { descriptor: wgpu::TextureDescriptor, } +/// A wrapper around a `wgpu::Buffer` containing bytes of a known length. +#[derive(Debug)] +pub struct BufferBytes { + buffer: wgpu::Buffer, + len_bytes: wgpu::BufferAddress, +} + impl Texture { // `wgpu::TextureDescriptor` accessor methods. @@ -33,8 +42,28 @@ impl Texture { &self.descriptor } - /// The full extent of the texture in three dimensions. - pub fn size(&self) -> wgpu::Extent3d { + /// The inner descriptor from which this **Texture** was constructed. + pub fn descriptor_cloned(&self) -> wgpu::TextureDescriptor { + wgpu::TextureDescriptor { + size: self.extent(), + array_layer_count: self.array_layer_count(), + mip_level_count: self.mip_level_count(), + sample_count: self.sample_count(), + dimension: self.dimension(), + format: self.format(), + usage: self.usage(), + } + } + + /// The width and height of the texture. + /// + /// See the `extent` method for producing the full width, height and *depth* of the texture. + pub fn size(&self) -> [u32; 2] { + [self.descriptor.size.width, self.descriptor.size.height] + } + + /// The width, height and depth of the texture. + pub fn extent(&self) -> wgpu::Extent3d { self.descriptor.size } @@ -87,6 +116,88 @@ impl Texture { // Custom common use methods. + /// Write the contents of the texture into a new buffer. + /// + /// Commands will be added to the given encoder to copy the entire contents of the texture into + /// the buffer. + /// + /// The buffer is returned alongside its size in bytes. + /// + /// If the texture has a sample count greater than one, it will first be resolved to a + /// non-multisampled texture before being copied to the buffer. + /// `copy_texture_to_buffer` command has been performed by the GPU. + /// + /// NOTE: `map_read_async` should not be called on the returned buffer until the encoded commands have + /// been submitted to the device queue. + pub fn to_buffer( + &self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + ) -> (wgpu::Buffer, wgpu::BufferAddress) { + // Create the buffer and encode the copy. + fn texture_to_buffer( + texture: &wgpu::Texture, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + ) -> (wgpu::Buffer, wgpu::BufferAddress) { + // Create buffer that will be mapped for reading. + let size = texture.extent(); + let format = texture.format(); + let format_size_bytes = format_size_bytes(format) as u64; + let layer_len_pixels = size.width as u64 * size.height as u64 * size.depth as u64; + let layer_size_bytes = layer_len_pixels * format_size_bytes; + let data_size_bytes = layer_size_bytes * texture.array_layer_count() as u64; + let buffer_descriptor = wgpu::BufferDescriptor { + size: data_size_bytes, + usage: wgpu::BufferUsage::COPY_DST | wgpu::BufferUsage::MAP_READ, + }; + let buffer = device.create_buffer(&buffer_descriptor); + + // Copy the full contents of the texture to the buffer. + let texture_copy_view = texture.create_default_copy_view(); + let buffer_copy_view = texture.create_default_buffer_copy_view(&buffer); + encoder.copy_texture_to_buffer(texture_copy_view, buffer_copy_view, size); + + (buffer, data_size_bytes) + } + + // If this texture is multi-sampled, resolve it first. + if self.sample_count() > 1 { + let view = self.create_default_view(); + let descriptor = self.descriptor_cloned(); + let resolved_texture = wgpu::TextureBuilder::from(descriptor) + .sample_count(1) + .usage(wgpu::TextureUsage::OUTPUT_ATTACHMENT | wgpu::TextureUsage::COPY_SRC) + .build(device); + let resolved_view = resolved_texture.create_default_view(); + wgpu::resolve_texture(&view, &resolved_view, encoder); + texture_to_buffer(&resolved_texture, device, encoder) + } else { + texture_to_buffer(self, device, encoder) + } + } + + /// Encode the necessary commands to read the contents of the texture into memory. + /// + /// The entire contents of the texture will be made available as a single slice of bytes. + /// + /// This method uses `to_buffer` internally, exposing a simplified API for reading the produced + /// buffer as a slice of bytes. + /// + /// If the texture has a sample count greater than one, it will first be resolved to a + /// non-multisampled texture before being copied to the buffer. + /// + /// NOTE: `read` should not be called on the returned buffer until the encoded commands have + /// been submitted to the device queue. + pub fn to_buffer_bytes( + &self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + ) -> BufferBytes { + let (buffer, len_bytes) = self.to_buffer(device, encoder); + BufferBytes { buffer, len_bytes } + } + /// The view descriptor describing a full view of the texture. pub fn create_default_view_descriptor(&self) -> wgpu::TextureViewDescriptor { let dimension = match self.dimension() { @@ -127,12 +238,12 @@ impl Texture { buffer: &'a wgpu::Buffer, ) -> wgpu::BufferCopyView<'a> { let format_size_bytes = format_size_bytes(self.format()); - let size = self.size(); + let [width, height] = self.size(); wgpu::BufferCopyView { buffer, offset: 0, - row_pitch: size.width * format_size_bytes, - image_height: size.height, + row_pitch: width * format_size_bytes, + image_height: height, } } } @@ -258,7 +369,36 @@ impl Builder { } } -impl std::ops::Deref for Texture { +impl BufferBytes { + /// Asynchronously maps the buffer of bytes to host memory and, once mapped, calls the given + /// user callback with the data as a slice of bytes. + /// + /// Note: The given callback will not be called until the memory is mapped and the device is + /// polled. You should not rely on the callback being called immediately. + pub fn read(&self, callback: F) + where + F: 'static + FnOnce(wgpu::BufferMapAsyncResult<&[u8]>), + { + self.buffer.map_read_async(0, self.len_bytes, callback) + } + + /// The length of the `wgpu::Buffer` in bytes. + pub fn len_bytes(&self) -> wgpu::BufferAddress { + self.len_bytes + } + + /// A reference to the inner `wgpu::Buffer`. + pub fn inner(&self) -> &wgpu::Buffer { + &self.buffer + } + + /// Consumes `self` and returns the inner `wgpu::Buffer`. + pub fn into_inner(self) -> wgpu::Buffer { + self.buffer + } +} + +impl Deref for Texture { type Target = TextureHandle; fn deref(&self) -> &Self::Target { &self.texture @@ -287,7 +427,7 @@ impl Into for Builder { /// Return the size of the given texture format in bytes. pub fn format_size_bytes(format: wgpu::TextureFormat) -> u32 { - use wgpu::TextureFormat::*; + use crate::wgpu::TextureFormat::*; match format { R8Unorm | R8Snorm | R8Uint | R8Sint => 1, R16Unorm | R16Snorm | R16Uint | R16Sint | R16Float | Rg8Unorm | Rg8Snorm | Rg8Uint @@ -300,3 +440,19 @@ pub fn format_size_bytes(format: wgpu::TextureFormat) -> u32 { Depth32Float | Depth24Plus | Depth24PlusStencil8 => 4, } } + +/// Returns `true` if the given `wgpu::Extent3d`s are equal. +pub fn extent_3d_eq(a: &wgpu::Extent3d, b: &wgpu::Extent3d) -> bool { + a.width == b.width && a.height == b.height && a.depth == b.depth +} + +/// Returns `true` if the given texture descriptors are equal. +pub fn descriptor_eq(a: &wgpu::TextureDescriptor, b: &wgpu::TextureDescriptor) -> bool { + extent_3d_eq(&a.size, &b.size) + && a.array_layer_count == b.array_layer_count + && a.mip_level_count == b.mip_level_count + && a.sample_count == b.sample_count + && a.dimension == b.dimension + && a.format == b.format + && a.usage == b.usage +} diff --git a/src/wgpu/texture/format_converter/mod.rs b/src/wgpu/texture/reshaper/mod.rs similarity index 85% rename from src/wgpu/texture/format_converter/mod.rs rename to src/wgpu/texture/reshaper/mod.rs index 8e4e43ae9..638aa00de 100644 --- a/src/wgpu/texture/format_converter/mod.rs +++ b/src/wgpu/texture/reshaper/mod.rs @@ -1,4 +1,6 @@ -/// Writes a texture to another texture of the same dimensions but with a different format. +use crate::wgpu; + +/// Reshapes a texture from its original size and format to the destination size and format. /// /// The `src_texture` must have the `TextureUsage::SAMPLED` enabled. /// @@ -9,7 +11,7 @@ /// Both textures should **not** be multisampled. *Note: Please open an issue if you would like /// support for multisampled source textures as it should be quite trivial to add.* #[derive(Debug)] -pub struct FormatConverter { +pub struct Reshaper { _vs_mod: wgpu::ShaderModule, _fs_mod: wgpu::ShaderModule, bind_group_layout: wgpu::BindGroupLayout, @@ -19,11 +21,13 @@ pub struct FormatConverter { vertex_buffer: wgpu::Buffer, } -impl FormatConverter { - /// Construct a new `FormatConverter`. +impl Reshaper { + /// Construct a new `Reshaper`. pub fn new( device: &wgpu::Device, src_texture: &wgpu::TextureView, + src_multisampled: bool, + dst_sample_count: u32, dst_format: wgpu::TextureFormat, ) -> Self { // Load shader modules. @@ -37,14 +41,19 @@ impl FormatConverter { let fs_mod = device.create_shader_module(&fs_spirv); // Create the sampler for sampling from the source texture. - let sampler_desc = sampler_desc(); - let sampler = device.create_sampler(&sampler_desc); + let sampler = wgpu::SamplerBuilder::new().build(device); // Create the render pipeline. - let bind_group_layout = bind_group_layout(device); + let bind_group_layout = bind_group_layout(device, src_multisampled); let pipeline_layout = pipeline_layout(device, &bind_group_layout); - let render_pipeline = - render_pipeline(device, &pipeline_layout, &vs_mod, &fs_mod, dst_format); + let render_pipeline = render_pipeline( + device, + &pipeline_layout, + &vs_mod, + &fs_mod, + dst_sample_count, + dst_format, + ); // Create the bind group. let bind_group = bind_group(device, &bind_group_layout, src_texture, &sampler); @@ -54,7 +63,7 @@ impl FormatConverter { .create_buffer_mapped(VERTICES.len(), wgpu::BufferUsage::VERTEX) .fill_from_slice(&VERTICES[..]); - FormatConverter { + Reshaper { _vs_mod: vs_mod, _fs_mod: fs_mod, bind_group_layout, @@ -121,26 +130,12 @@ fn vertex_attrs() -> [wgpu::VertexAttributeDescriptor; 1] { }] } -fn sampler_desc() -> wgpu::SamplerDescriptor { - wgpu::SamplerDescriptor { - address_mode_u: wgpu::AddressMode::ClampToEdge, - address_mode_v: wgpu::AddressMode::ClampToEdge, - address_mode_w: wgpu::AddressMode::ClampToEdge, - mag_filter: wgpu::FilterMode::Linear, - min_filter: wgpu::FilterMode::Linear, - mipmap_filter: wgpu::FilterMode::Linear, - lod_min_clamp: -100.0, - lod_max_clamp: 100.0, - compare_function: wgpu::CompareFunction::Always, - } -} - -fn bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { +fn bind_group_layout(device: &wgpu::Device, src_multisampled: bool) -> wgpu::BindGroupLayout { let texture_binding = wgpu::BindGroupLayoutBinding { binding: 0, visibility: wgpu::ShaderStage::FRAGMENT, ty: wgpu::BindingType::SampledTexture { - multisampled: false, + multisampled: src_multisampled, dimension: wgpu::TextureViewDimension::D2, }, }; @@ -188,6 +183,7 @@ fn render_pipeline( layout: &wgpu::PipelineLayout, vs_mod: &wgpu::ShaderModule, fs_mod: &wgpu::ShaderModule, + dst_sample_count: u32, dst_format: wgpu::TextureFormat, ) -> wgpu::RenderPipeline { let vs_desc = wgpu::ProgrammableStageDescriptor { @@ -227,7 +223,7 @@ fn render_pipeline( depth_stencil_state: None, index_format: wgpu::IndexFormat::Uint16, vertex_buffers: &[vertex_buffer_desc], - sample_count: 1, + sample_count: dst_sample_count, sample_mask: !0, alpha_to_coverage_enabled: false, }; diff --git a/src/wgpu/texture/format_converter/shaders/frag.spv b/src/wgpu/texture/reshaper/shaders/frag.spv similarity index 100% rename from src/wgpu/texture/format_converter/shaders/frag.spv rename to src/wgpu/texture/reshaper/shaders/frag.spv diff --git a/src/wgpu/texture/format_converter/shaders/shader.frag b/src/wgpu/texture/reshaper/shaders/shader.frag similarity index 100% rename from src/wgpu/texture/format_converter/shaders/shader.frag rename to src/wgpu/texture/reshaper/shaders/shader.frag diff --git a/src/wgpu/texture/format_converter/shaders/shader.vert b/src/wgpu/texture/reshaper/shaders/shader.vert similarity index 100% rename from src/wgpu/texture/format_converter/shaders/shader.vert rename to src/wgpu/texture/reshaper/shaders/shader.vert diff --git a/src/wgpu/texture/format_converter/shaders/vert.spv b/src/wgpu/texture/reshaper/shaders/vert.spv similarity index 100% rename from src/wgpu/texture/format_converter/shaders/vert.spv rename to src/wgpu/texture/reshaper/shaders/vert.spv diff --git a/src/window.rs b/src/window.rs index 8c25e929d..bc4d0a29c 100644 --- a/src/window.rs +++ b/src/window.rs @@ -10,7 +10,7 @@ use crate::geom::{Point2, Vector2}; use crate::wgpu; use crate::App; use std::any::Any; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::{env, fmt}; use winit::dpi::LogicalSize; @@ -221,13 +221,21 @@ pub struct Window { pub(crate) device_queue_pair: Arc, msaa_samples: u32, pub(crate) swap_chain: WindowSwapChain, - // Data for rendering a `Frame`'s intermediary image to a swap chain image. - pub(crate) frame_render_data: Option, + pub(crate) frame_data: Option, pub(crate) frame_count: u64, pub(crate) user_functions: UserFunctions, pub(crate) tracked_state: TrackedState, } +// Data related to `Frame`s produced for this window's swapchain textures. +#[derive(Debug)] +pub(crate) struct FrameData { + // Data for rendering a `Frame`'s intermediary image to a swap chain image. + pub(crate) render: frame::RenderData, + // Data for capturing a `Frame`'s intermediary image before submission. + pub(crate) capture: frame::CaptureData, +} + // Track and store some information about the window in order to avoid making repeated internal // queries to the platform-specific API. This is beneficial in some cases where queries to the // platform-specific API can be very slow (e.g. macOS cocoa). @@ -745,18 +753,20 @@ impl<'app> Builder<'app> { // If we're using an intermediary image for rendering frames to swap_chain images, create // the necessary render data. - let (frame_render_data, msaa_samples) = match user_functions.view { + let (frame_data, msaa_samples) = match user_functions.view { Some(View::WithModel(_)) | Some(View::Sketch(_)) | None => { let msaa_samples = msaa_samples.unwrap_or(Frame::DEFAULT_MSAA_SAMPLES); // TODO: Verity that requested sample count is valid for surface? let swap_chain_dims = [swap_chain_desc.width, swap_chain_desc.height]; - let render_data = frame::RenderData::new( + let render = frame::RenderData::new( &device, swap_chain_dims, swap_chain_desc.format, msaa_samples, ); - (Some(render_data), msaa_samples) + let capture = frame::CaptureData::default(); + let frame_data = FrameData { render, capture }; + (Some(frame_data), msaa_samples) } Some(View::WithModelRaw(_)) => (None, 1), }; @@ -779,7 +789,7 @@ impl<'app> Builder<'app> { device_queue_pair, msaa_samples, swap_chain, - frame_render_data, + frame_data, frame_count, user_functions, tracked_state, @@ -1269,12 +1279,15 @@ impl Window { self.swap_chain_device() .create_swap_chain(&self.surface, &self.swap_chain.descriptor), ); - self.frame_render_data = Some(frame::RenderData::new( - self.swap_chain_device(), - size_px, - self.swap_chain.descriptor.format, - self.msaa_samples, - )); + if self.frame_data.is_some() { + let render_data = frame::RenderData::new( + self.swap_chain_device(), + size_px, + self.swap_chain.descriptor.format, + self.msaa_samples, + ); + self.frame_data.as_mut().unwrap().render = render_data; + } } /// Attempts to determine whether or not the window is currently fullscreen. @@ -1299,6 +1312,41 @@ impl Window { let (w, h) = self.inner_size_points(); geom::Rect::from_w_h(w, h) } + + /// Capture the next frame right before it is drawn to this window and write it to an image + /// file at the given path. If a frame already exists, it will be captured before its `submit` + /// method is called or before it is `drop`ped. + /// + /// The destination image file type will be inferred from the extension given in the path. + /// + /// The **App** will use a separate thread for writing the image file in order to avoid + /// interfering with the main application thread. Note that if you are capturing every frame + /// and your window size is very large or the file type you are writing to is expensive to + /// encode, this capturing thread may fall behind the main thread, resulting in a large delay + /// before the exiting the program. This is because the capturing thread needs more time to + /// write the captured images. + pub fn capture_frame

(&self, path: P) + where + P: AsRef, + { + let path = path.as_ref(); + + // If the parent directory does not exist, create it. + let dir = path.parent().expect("capture_frame path has no directory"); + if !dir.exists() { + std::fs::create_dir_all(&dir).expect("failed to create `capture_frame` directory"); + } + + let mut capture_next_frame_path = self + .frame_data + .as_ref() + .expect("window capture requires that `view` draws to a `Frame` (not a `RawFrame`)") + .capture + .next_frame_path + .lock() + .expect("failed to lock `capture_next_frame_path`"); + *capture_next_frame_path = Some(path.to_path_buf()); + } } // Debug implementations for function wrappers.