diff --git a/src/lib.rs b/src/lib.rs index 056f815..15bfb57 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,23 +28,16 @@ #![deny(clippy::all)] #![forbid(unsafe_code)] -use std::cell::RefCell; use std::env; -use std::rc::Rc; pub use crate::macros::*; -pub use crate::render_pass::{BoxedRenderPass, Device, Queue, RenderPass}; -use crate::renderers::Renderer; +pub use crate::renderers::ScalingRenderer; use thiserror::Error; pub use wgpu; -use wgpu::{Extent3d, TextureView}; mod macros; -mod render_pass; mod renderers; -type RenderPassFactory = Box BoxedRenderPass>; - /// A logical texture for a window surface. #[derive(Debug)] pub struct SurfaceTexture { @@ -59,14 +52,14 @@ pub struct SurfaceTexture { #[derive(Debug)] pub struct Pixels { // WGPU state - device: Rc, - queue: Rc>, + device: wgpu::Device, + queue: wgpu::Queue, swap_chain: wgpu::SwapChain, surface_texture: SurfaceTexture, present_mode: wgpu::PresentMode, - // List of render passes - renderers: Vec, + // A default renderer to scale the input texture to the screen size + scaling_renderer: ScalingRenderer, // Texture state for the texel upload texture: wgpu::Texture, @@ -90,7 +83,6 @@ pub struct PixelsBuilder<'req> { present_mode: wgpu::PresentMode, surface_texture: SurfaceTexture, texture_format: wgpu::TextureFormat, - renderer_factories: Vec, } /// All the ways in which creating a pixel buffer can fail. @@ -176,8 +168,6 @@ impl Pixels { /// Call this method in response to a resize event from your window manager. The size expected /// is in physical pixel units. pub fn resize(&mut self, width: u32, height: u32) { - // TODO: Call `update_bindings` on each render pass to create a texture chain - // Update SurfaceTexture dimensions self.surface_texture.width = width; self.surface_texture.height = height; @@ -209,21 +199,83 @@ impl Pixels { let mut encoder = self .device .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); - for renderer in self.renderers.iter_mut() { - renderer.resize(&mut encoder, width, height); - } - - self.queue.borrow_mut().submit(&[encoder.finish()]); + self.scaling_renderer + .resize(&mut self.device, &mut encoder, width, height); + self.queue.submit(&[encoder.finish()]); } /// Draw this pixel buffer to the configured [`SurfaceTexture`]. /// - /// This executes all render passes in sequence. See [`RenderPass`]. + /// # Errors + /// + /// Returns an error when [`wgpu::SwapChain::get_next_texture`] times out. + /// + /// # Example + /// + /// ```no_run + /// # use pixels::Pixels; + /// # let surface = wgpu::Surface::create(&pixels_mocks::RWH); + /// # let surface_texture = pixels::SurfaceTexture::new(1024, 768, surface); + /// let mut pixels = Pixels::new(320, 240, surface_texture)?; + /// + /// // Clear the pixel buffer + /// let frame = pixels.get_frame(); + /// for pixel in frame.chunks_exact_mut(4) { + /// pixel[0] = 0x00; // R + /// pixel[1] = 0x00; // G + /// pixel[2] = 0x00; // B + /// pixel[3] = 0xff; // A + /// } + /// + /// // Draw it to the `SurfaceTexture` + /// pixels.render(); + /// # Ok::<(), pixels::Error>(()) + /// ``` + pub fn render(&mut self) -> Result<(), Error> { + self.render_with(|encoder, render_target, scaling_renderer| { + scaling_renderer.render(encoder, render_target); + }) + } + + /// Draw this pixel buffer to the configured [`SurfaceTexture`], + /// using a custom user-provided render function. + /// + /// Provides access to a [`wgpu::CommandEncoder`], + /// a [`wgpu::TextureView`] from the swapchain which you can use to render to the screen, + /// and the default [`ScalingRenderer`]. /// /// # Errors /// /// Returns an error when [`wgpu::SwapChain::get_next_texture`] times out. - pub fn render(&mut self) -> Result<(), Error> { + /// + /// # Example + /// + /// ```no_run + /// # use pixels::Pixels; + /// # let surface = wgpu::Surface::create(&pixels_mocks::RWH); + /// # let surface_texture = pixels::SurfaceTexture::new(1024, 768, surface); + /// let mut pixels = Pixels::new(320, 240, surface_texture)?; + /// + /// // Clear the pixel buffer + /// let frame = pixels.get_frame(); + /// for pixel in frame.chunks_exact_mut(4) { + /// pixel[0] = 0x00; // R + /// pixel[1] = 0x00; // G + /// pixel[2] = 0x00; // B + /// pixel[3] = 0xff; // A + /// } + /// + /// // Draw it to the `SurfaceTexture` + /// pixels.render_with(|encoder, render_target, scaling_renderer| { + /// scaling_renderer.render(encoder, render_target); + /// // etc... + /// }); + /// # Ok::<(), pixels::Error>(()) + /// ``` + pub fn render_with(&mut self, render_function: F) -> Result<(), Error> + where + F: FnOnce(&mut wgpu::CommandEncoder, &wgpu::TextureView, &ScalingRenderer), + { // TODO: Center frame buffer in surface let frame = self .swap_chain @@ -258,40 +310,15 @@ impl Pixels { self.texture_extent, ); - // Execute all render passes - for renderer in self.renderers.iter() { - // TODO: Create a texture chain so that each pass receives the texture drawn by the previous - renderer.render(&mut encoder, &frame.view); - } + // Call the users render function. + (render_function)(&mut encoder, &frame.view, &self.scaling_renderer); - self.queue.borrow_mut().submit(&[encoder.finish()]); + self.queue.submit(&[encoder.finish()]); Ok(()) } /// Get a mutable byte slice for the pixel buffer. The buffer is _not_ cleared for you; it will /// retain the previous frame's contents until you clear it yourself. - /// - /// # Example - /// - /// ```no_run - /// # use pixels::Pixels; - /// # let surface = wgpu::Surface::create(&pixels_mocks::RWH); - /// # let surface_texture = pixels::SurfaceTexture::new(1024, 768, surface); - /// let mut pixels = Pixels::new(320, 240, surface_texture)?; - /// - /// // Clear the pixel buffer - /// let frame = pixels.get_frame(); - /// for pixel in frame.chunks_exact_mut(4) { - /// pixel[0] = 0x00; // R - /// pixel[1] = 0x00; // G - /// pixel[2] = 0x00; // B - /// pixel[3] = 0xff; // A - /// } - /// - /// // Draw it to the `SurfaceTexture` - /// pixels.render(); - /// # Ok::<(), pixels::Error>(()) - /// ``` pub fn get_frame(&mut self) -> &mut [u8] { &mut self.pixels } @@ -388,6 +415,21 @@ impl Pixels { pos.1.max(0).min(self.texture_extent.height as isize - 1) as usize, ) } + + /// Provides access the the [`wgpu::Device`] `pixels` uses. + pub fn device(&self) -> &wgpu::Device { + &self.device + } + + /// Provides access the the [`wgpu::Queue`] `pixels` uses. + pub fn queue(&self) -> &wgpu::Queue { + &self.queue + } + + /// Provides access the the [`wgpu::Texture`] `pixels` makes by uploading the frame you provide to the GPU. + pub fn texture(&self) -> &wgpu::Texture { + &self.texture + } } impl<'req> PixelsBuilder<'req> { @@ -399,24 +441,12 @@ impl<'req> PixelsBuilder<'req> { /// # use pixels::PixelsBuilder; /// # let surface = wgpu::Surface::create(&pixels_mocks::RWH); /// # let surface_texture = pixels::SurfaceTexture::new(1024, 768, surface); - /// struct MyRenderPass { - /// // ... - /// }; - /// - /// impl pixels::RenderPass for MyRenderPass { - /// // ... - /// # fn update_bindings(&mut self, _: &wgpu::TextureView, _: &wgpu::Extent3d) {} - /// # fn render(&self, _: &mut wgpu::CommandEncoder, _: &wgpu::TextureView) {} - /// } - /// /// let mut pixels = PixelsBuilder::new(256, 240, surface_texture) - /// .pixel_aspect_ratio(8.0 / 7.0) - /// .add_render_pass(|device, queue, texture, texture_size| { - /// // Create reources for MyRenderPass here - /// Box::new(MyRenderPass { - /// // ... - /// }) + /// .request_adapter_options(wgpu::RequestAdapterOptions { + /// power_preference: wgpu::PowerPreference::HighPerformance, + /// compatible_surface: None, /// }) + /// .pixel_aspect_ratio(8.0 / 7.0) /// .build()?; /// # Ok::<(), pixels::Error>(()) /// ``` @@ -438,7 +468,6 @@ impl<'req> PixelsBuilder<'req> { present_mode: wgpu::PresentMode::Fifo, surface_texture, texture_format: wgpu::TextureFormat::Rgba8UnormSrgb, - renderer_factories: Vec::new(), } } @@ -523,62 +552,6 @@ impl<'req> PixelsBuilder<'req> { self } - /// Add a render pass. - /// - /// Render passes are executed in the order they are added. - /// - /// # Factory Arguments - /// - /// * `device` - A reference-counted [`wgpu::Device`] which allows you to create GPU resources. - /// * `queue` - A reference-counted [`wgpu::Queue`] which can execute command buffers. - /// * `texture` - A [`wgpu::TextureView`] reference that is used as the texture input for the - /// render pass. - /// * `texture_size` - A [`wgpu::Extent3d`] providing the input texture size. - /// - /// # Examples - /// - /// ```no_run - /// use pixels::{BoxedRenderPass, Device, PixelsBuilder, Queue, RenderPass}; - /// use pixels::wgpu::{Extent3d, TextureView}; - /// - /// struct MyRenderPass { - /// device: Device, - /// queue: Queue, - /// } - /// - /// impl MyRenderPass { - /// fn factory( - /// device: Device, - /// queue: Queue, - /// texture: &TextureView, - /// texture_size: &Extent3d, - /// ) -> BoxedRenderPass { - /// // Create a bind group, pipeline, etc. and store all of the necessary state... - /// Box::new(MyRenderPass { device, queue }) - /// } - /// } - /// - /// impl RenderPass for MyRenderPass { - /// // ... - /// # fn update_bindings(&mut self, _: &wgpu::TextureView, _: &wgpu::Extent3d) {} - /// # fn render(&self, _: &mut wgpu::CommandEncoder, _: &wgpu::TextureView) {} - /// } - /// - /// # let surface = wgpu::Surface::create(&pixels_mocks::RWH); - /// # let surface_texture = pixels::SurfaceTexture::new(1024, 768, surface); - /// let builder = PixelsBuilder::new(320, 240, surface_texture) - /// .add_render_pass(MyRenderPass::factory) - /// .build()?; - /// # Ok::<(), pixels::Error>(()) - /// ``` - pub fn add_render_pass( - mut self, - factory: impl Fn(Device, Queue, &TextureView, &Extent3d) -> BoxedRenderPass + 'static, - ) -> PixelsBuilder<'req> { - self.renderer_factories.push(Box::new(factory)); - self - } - /// Create a pixel buffer from the options builder. /// /// # Errors @@ -602,9 +575,8 @@ impl<'req> PixelsBuilder<'req> { )) .ok_or(Error::AdapterNotFound)?; - let (device, queue) = pollster::block_on(adapter.request_device(&self.device_descriptor)); - let device = Rc::new(device); - let queue = Rc::new(RefCell::new(queue)); + let (mut device, queue) = + pollster::block_on(adapter.request_device(&self.device_descriptor)); // The rest of this is technically a fixed-function pipeline... For now! @@ -656,24 +628,7 @@ impl<'req> PixelsBuilder<'req> { .transform .inversed(); - // Create a renderer that impls `RenderPass` - let mut renderers = vec![Renderer::factory( - device.clone(), - queue.clone(), - &texture_view, - &texture_extent, - )]; - - // Create all render passes - renderers.extend(self.renderer_factories.iter().map(|f| { - // TODO: Create a texture chain so that each pass receives the texture drawn by the previous - f( - device.clone(), - queue.clone(), - &texture_view, - &texture_extent, - ) - })); + let scaling_renderer = ScalingRenderer::new(&mut device, &texture_view, &texture_extent); Ok(Pixels { device, @@ -681,7 +636,7 @@ impl<'req> PixelsBuilder<'req> { swap_chain, surface_texture, present_mode, - renderers, + scaling_renderer, texture, texture_extent, texture_format_size, diff --git a/src/render_pass.rs b/src/render_pass.rs deleted file mode 100644 index 0a642a8..0000000 --- a/src/render_pass.rs +++ /dev/null @@ -1,80 +0,0 @@ -use std::cell::RefCell; -use std::fmt; -use std::rc::Rc; -use wgpu::{Extent3d, TextureView}; - -/// A reference-counted [`wgpu::Device`] -pub type Device = Rc; - -/// A reference-counted [`wgpu::Queue`] (with interior mutability) -pub type Queue = Rc>; - -/// The boxed render pass type for dynamic dispatch -pub type BoxedRenderPass = Box; - -/// Objects that implement this trait can be added to [`Pixels`] as a render pass. -/// -/// [`Pixels`] always has at least one render pass; a scaling pass that uses a nearest-neighbor -/// sampler to preserve pixel edges. Optionally it may also have a second scaling pass that -/// transforms the texture to its final size (for non-square pixel aspect ratios). During this -/// second pass, the texture is stretched horizontally using a linear sampler. -/// -/// Any additional render passes are executed afterward. -/// -/// Each render pass is configured with one [`wgpu::TextureView`] as an input. You will probably -/// want to create a binding for this `texture_view` so your shaders can sample from it. -/// -/// The render pass will also receive a reference to another [`wgpu::TextureView`] when the pass is -/// executed. This texture view is the `render_target`. -/// -/// [`Pixels`]: ./struct.Pixels.html -pub trait RenderPass { - /// Called when it is time to execute this render pass. Use the `encoder` to encode all - /// commands related to this render pass. The result must be stored to the `render_target`. - /// - /// # Arguments - /// * `encoder` - Command encoder for the render pass - /// * `render_target` - A reference to the output texture - /// * `texels` - The byte slice passed to `Pixels::render` - fn render(&self, encoder: &mut wgpu::CommandEncoder, render_target: &TextureView); - - /// This method will be called when the input [`wgpu::TextureView`] needs to be rebinded. - /// - /// A [`wgpu::TextureView`] is provided to the `RenderPass` factory as an input texture with - /// the original [`SurfaceTexture`] size. This method is called in response to resizing the - /// [`SurfaceTexture`], where your `RenderPass` impl can update its input texture for the new - /// size. - /// - /// # Arguments - /// * `input_texture` - A reference to the `TextureView` for this render pass's input - /// * `input_texture_size` - The `input_texture` size - /// - /// [`Pixels`]: ./struct.Pixels.html - /// [`SurfaceTexture`]: ./struct.SurfaceTexture.html - fn update_bindings(&mut self, input_texture: &TextureView, input_texture_size: &Extent3d); - - /// When the window is resized, this method will be called, allowing the render pass to - /// customize itself to the display size. - /// - /// The default implementation is a no-op. - /// - /// # Arguments - /// * `encoder` - Command encoder for the render pass - /// * `width` - Render target width in physical pixel units - /// * `height` - Render target height in physical pixel units - #[allow(unused_variables)] - fn resize(&mut self, encoder: &mut wgpu::CommandEncoder, width: u32, height: u32) {} - - /// This function implements [`Debug`](fmt::Debug) for trait objects. - /// - /// You are encouraged to override the default impl to provide better debug messages. - fn debug(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "dyn RenderPass") - } -} - -impl fmt::Debug for dyn RenderPass + 'static { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.debug(f) - } -} diff --git a/src/renderers.rs b/src/renderers.rs index 1e5b67f..da2072a 100644 --- a/src/renderers.rs +++ b/src/renderers.rs @@ -1,15 +1,9 @@ -use std::fmt; -use std::rc::Rc; -use ultraviolet::Mat4; -use wgpu::{self, Extent3d, TextureView}; - use crate::include_spv; -use crate::render_pass::{BoxedRenderPass, Device, Queue, RenderPass}; +use ultraviolet::Mat4; -/// Renderer implements [`RenderPass`]. +/// The default renderer that scales your frame to the screen size. #[derive(Debug)] -pub(crate) struct Renderer { - device: Rc, +pub struct ScalingRenderer { uniform_buffer: wgpu::Buffer, bind_group: wgpu::BindGroup, render_pipeline: wgpu::RenderPipeline, @@ -17,14 +11,12 @@ pub(crate) struct Renderer { height: f32, } -impl Renderer { - /// Factory function for generating `RenderPass` trait objects. - pub(crate) fn factory( - device: Device, - _queue: Queue, - texture_view: &TextureView, - texture_size: &Extent3d, - ) -> BoxedRenderPass { +impl ScalingRenderer { + pub(crate) fn new( + device: &mut wgpu::Device, + texture_view: &wgpu::TextureView, + texture_size: &wgpu::Extent3d, + ) -> Self { let vs_module = device.create_shader_module(include_spv!("../shaders/vert.spv")); let fs_module = device.create_shader_module(include_spv!("../shaders/frag.spv")); @@ -139,19 +131,16 @@ impl Renderer { alpha_to_coverage_enabled: false, }); - Box::new(Renderer { - device, + Self { uniform_buffer, bind_group, render_pipeline, width: texture_size.width as f32, height: texture_size.height as f32, - }) + } } -} -impl RenderPass for Renderer { - fn render(&self, encoder: &mut wgpu::CommandEncoder, render_target: &TextureView) { + pub fn render(&self, encoder: &mut wgpu::CommandEncoder, render_target: &wgpu::TextureView) { // Draw the updated texture to the render target let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor { @@ -168,23 +157,20 @@ impl RenderPass for Renderer { rpass.draw(0..6, 0..1); } - fn resize(&mut self, encoder: &mut wgpu::CommandEncoder, width: u32, height: u32) { + pub(crate) fn resize( + &mut self, + device: &mut wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + width: u32, + height: u32, + ) { let matrix = ScalingMatrix::new((self.width, self.height), (width as f32, height as f32)); let transform_bytes = matrix.as_bytes(); - let temp_buf = self - .device - .create_buffer_with_data(&transform_bytes, wgpu::BufferUsage::COPY_SRC); + let temp_buf = + device.create_buffer_with_data(&transform_bytes, wgpu::BufferUsage::COPY_SRC); encoder.copy_buffer_to_buffer(&temp_buf, 0, &self.uniform_buffer, 0, 64); } - - // We don't actually have to rebind the TextureView here. - // It's guaranteed that the initial texture never changes. - fn update_bindings(&mut self, _input_texture: &TextureView, _input_texture_size: &Extent3d) {} - - fn debug(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self) - } } #[derive(Debug)]