Add RenderPass trait and refactor PixelsBuilder

This commit is contained in:
Jay Oster 2019-10-02 00:53:41 -07:00
parent 2ec43fccd1
commit 70306c5cfd
2 changed files with 198 additions and 119 deletions

View file

@ -1,8 +1,8 @@
//! A tiny library providing a GPU-powered pixel frame buffer. //! A tiny library providing a GPU-powered pixel pixel buffer.
//! //!
//! `Pixels` represents a 2D frame buffer with an explicit image resolution, //! [`Pixels`] represents a 2D pixel buffer with an explicit image resolution,
//! making it ideal for prototyping simple pixel-based games, animations, and //! making it ideal for prototyping simple pixel-based games, animations, and
//! emulators. The frame buffer is rendered entirely on the GPU, allowing //! emulators. The pixel buffer is rendered entirely on the GPU, allowing
//! developers to easily incorporate special effects with shaders and a //! developers to easily incorporate special effects with shaders and a
//! customizable pipeline. //! customizable pipeline.
//! //!
@ -17,6 +17,9 @@ use std::fmt;
use vk_shader_macros::include_glsl; use vk_shader_macros::include_glsl;
pub use wgpu; pub use wgpu;
mod render_pass;
pub use render_pass::RenderPass;
/// A logical texture for a window surface. /// A logical texture for a window surface.
#[derive(Debug)] #[derive(Debug)]
pub struct SurfaceTexture<'a> { pub struct SurfaceTexture<'a> {
@ -25,29 +28,39 @@ pub struct SurfaceTexture<'a> {
height: u32, height: u32,
} }
/// Represents a 2D frame buffer with an explicit image resolution. /// Represents a 2D pixel buffer with an explicit image resolution.
///
/// See [`PixelsBuilder`] for building a customized pixel buffer.
#[derive(Debug)] #[derive(Debug)]
pub struct Pixels { pub struct Pixels {
bind_group: wgpu::BindGroup,
device: wgpu::Device, device: wgpu::Device,
queue: wgpu::Queue, queue: wgpu::Queue,
render_pipeline: wgpu::RenderPipeline, renderer: Renderer,
swap_chain: wgpu::SwapChain, swap_chain: wgpu::SwapChain,
} }
/// A builder to help create customized frame buffers. /// A builder to help create customized pixel buffers.
#[derive(Debug)] #[derive(Debug)]
pub struct PixelsOptions { pub struct PixelsBuilder<'a> {
request_adapter_options: wgpu::RequestAdapterOptions, request_adapter_options: wgpu::RequestAdapterOptions,
device_descriptor: wgpu::DeviceDescriptor, device_descriptor: wgpu::DeviceDescriptor,
width: u32, width: u32,
height: u32, height: u32,
pixel_aspect_ratio: f64,
surface_texture: SurfaceTexture<'a>,
} }
/// All the ways in which creating a frame buffer can fail. /// Renderer implements RenderPass.
#[derive(Debug)]
struct Renderer {
bind_group: wgpu::BindGroup,
render_pipeline: wgpu::RenderPipeline,
}
/// All the ways in which creating a pixel buffer can fail.
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
/// No suitable Adapter found /// No suitable [`wgpu::Adapter`] found
AdapterNotFound, AdapterNotFound,
} }
@ -93,44 +106,146 @@ impl<'a> SurfaceTexture<'a> {
} }
} }
/// # Examples
///
/// ```no_run
/// # use pixels::Pixels;
/// # let surface = wgpu::Surface::create(&pixels_mocks::RWH);
/// # let surface_texture = pixels::SurfaceTexture::new(1024, 768, &surface);
/// let fb = Pixels::new(320, 240, surface_texture)?;
/// # Ok::<(), pixels::Error>(())
/// ```
impl Pixels { impl Pixels {
/// Create a frame buffer instance with default options. /// Create a pixel buffer instance with default options.
///
/// # Examples
///
/// ```no_run
/// # use pixels::Pixels;
/// # let surface = wgpu::Surface::create(&pixels_mocks::RWH);
/// # let surface_texture = pixels::SurfaceTexture::new(1024, 768, &surface);
/// let fb = Pixels::new(320, 240, surface_texture)?;
/// # Ok::<(), pixels::Error>(())
/// ```
/// ///
/// # Errors /// # Errors
/// ///
/// Returns an error when a `wgpu::Adapter` cannot be found. /// Returns an error when a [`wgpu::Adapter`] cannot be found.
/// ///
/// # Panics /// # Panics
/// ///
/// Panics when `width` or `height` are 0. /// Panics when `width` or `height` are 0.
pub fn new(width: u32, height: u32, surface_texture: SurfaceTexture) -> Result<Pixels, Error> { pub fn new<'a>(
Pixels::new_with_options(surface_texture, PixelsOptions::new(width, height)) width: u32,
height: u32,
surface_texture: SurfaceTexture<'a>,
) -> Result<Pixels, Error> {
PixelsBuilder::new(width, height, surface_texture).build()
} }
/// Create a frame buffer instance with the given options. // TODO: Support resize
/// Draw this pixel buffer to the configured [`SurfaceTexture`].
pub fn render(&mut self) {
// TODO: Center frame buffer in surface
let frame = self.swap_chain.get_next_texture();
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor { todo: 0 });
// TODO: Run all render passes in a loop
self.renderer.render_pass(&mut encoder, &frame.view);
self.queue.submit(&[encoder.finish()]);
}
}
impl RenderPass for Renderer {
fn update_bindings(&mut self, _texture_view: &wgpu::TextureView) {}
fn render_pass(&self, encoder: &mut wgpu::CommandEncoder, render_target: &wgpu::TextureView) {
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor {
attachment: render_target,
resolve_target: None,
load_op: wgpu::LoadOp::Clear,
store_op: wgpu::StoreOp::Store,
clear_color: wgpu::Color::GREEN,
}],
depth_stencil_attachment: None,
});
rpass.set_pipeline(&self.render_pipeline);
rpass.set_bind_group(0, &self.bind_group, &[]);
rpass.draw(0..3, 0..1);
}
}
impl<'a> PixelsBuilder<'a> {
/// Create a builder that can be finalized into a [`Pixels`] pixel buffer.
///
/// # Examples
///
/// ```no_run
/// # use pixels::PixelsBuilder;
/// # let surface = wgpu::Surface::create(&pixels_mocks::RWH);
/// # let surface_texture = pixels::SurfaceTexture::new(1024, 768, &surface);
/// let fb = PixelsBuilder::new(256, 240, surface_texture)
/// .pixel_aspect_ratio(8.0 / 7.0)
/// # // TODO: demonstrate adding a render pass here
/// # //.render_pass(...)
/// .build()?;
/// # Ok::<(), pixels::Error>(())
/// ```
///
/// # Panics
///
/// Panics when `width` or `height` are 0.
pub fn new(width: u32, height: u32, surface_texture: SurfaceTexture<'a>) -> PixelsBuilder<'a> {
assert!(width > 0);
assert!(height > 0);
PixelsBuilder {
request_adapter_options: wgpu::RequestAdapterOptions::default(),
device_descriptor: wgpu::DeviceDescriptor::default(),
width,
height,
pixel_aspect_ratio: 1.0,
surface_texture,
}
}
/// Add options for requesting a [`wgpu::Adapter`].
pub fn request_adapter_options(
mut self,
request_adapter_options: wgpu::RequestAdapterOptions,
) -> PixelsBuilder<'a> {
self.request_adapter_options = request_adapter_options;
self
}
/// Add options for requesting a [`wgpu::Device`].
pub fn device_descriptor(
mut self,
device_descriptor: wgpu::DeviceDescriptor,
) -> PixelsBuilder<'a> {
self.device_descriptor = device_descriptor;
self
}
/// Set the pixel aspect ratio to simulate non-square pixels.
///
/// This setting enables a render pass that horizontally scales the pixel
/// buffer by the given factor.
///
/// E.g. set this to `8.0 / 7.0` for an 8:7 pixel aspect ratio.
pub fn pixel_aspect_ratio(mut self, pixel_aspect_ratio: f64) -> PixelsBuilder<'a> {
self.pixel_aspect_ratio = pixel_aspect_ratio;
self
}
/// Create a pixel buffer from the options builder.
/// ///
/// # Errors /// # Errors
/// ///
/// Returns an error when a `wgpu::Adapter` cannot be found or shaders /// Returns an error when a [`wgpu::Adapter`] cannot be found.
/// are invalid SPIR-V. pub fn build(self) -> Result<Pixels, Error> {
pub fn new_with_options(
surface_texture: SurfaceTexture,
options: PixelsOptions,
) -> Result<Pixels, Error> {
// TODO: Create a texture with the dimensions specified in `options` // TODO: Create a texture with the dimensions specified in `options`
// TODO: Use `options.pixel_aspect_ratio` to stretch the scaled texture
let adapter = wgpu::Adapter::request(&options.request_adapter_options) let adapter =
.ok_or(Error::AdapterNotFound)?; wgpu::Adapter::request(&self.request_adapter_options).ok_or(Error::AdapterNotFound)?;
let (device, queue) = adapter.request_device(&options.device_descriptor); let (device, queue) = adapter.request_device(&self.device_descriptor);
let vs_module = device.create_shader_module(include_glsl!("shaders/shader.vert")); let vs_module = device.create_shader_module(include_glsl!("shaders/shader.vert"));
let fs_module = device.create_shader_module(include_glsl!("shaders/shader.frag")); let fs_module = device.create_shader_module(include_glsl!("shaders/shader.frag"));
@ -179,105 +294,28 @@ impl Pixels {
}); });
let swap_chain = device.create_swap_chain( let swap_chain = device.create_swap_chain(
surface_texture.surface, self.surface_texture.surface,
&wgpu::SwapChainDescriptor { &wgpu::SwapChainDescriptor {
usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT, usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT,
format: wgpu::TextureFormat::Bgra8UnormSrgb, format: wgpu::TextureFormat::Bgra8UnormSrgb,
width: surface_texture.width, width: self.surface_texture.width,
height: surface_texture.height, height: self.surface_texture.height,
present_mode: wgpu::PresentMode::Vsync, present_mode: wgpu::PresentMode::Vsync,
}, },
); );
Ok(Pixels { let renderer = Renderer {
bind_group, bind_group,
render_pipeline,
};
Ok(Pixels {
device, device,
queue, queue,
render_pipeline, renderer,
swap_chain, swap_chain,
}) })
} }
// TODO: Support resize
/// Draw this frame buffer to the configured `wgpu::Surface`.
pub fn render(&mut self) {
// TODO: Center frame buffer in surface
let frame = self.swap_chain.get_next_texture();
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor { todo: 0 });
{
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor {
attachment: &frame.view,
resolve_target: None,
load_op: wgpu::LoadOp::Clear,
store_op: wgpu::StoreOp::Store,
clear_color: wgpu::Color::GREEN,
}],
depth_stencil_attachment: None,
});
rpass.set_pipeline(&self.render_pipeline);
rpass.set_bind_group(0, &self.bind_group, &[]);
rpass.draw(0..3, 0..1);
}
self.queue.submit(&[encoder.finish()]);
}
}
/// # Examples
///
/// ```no_run
/// # use pixels::PixelsOptions;
/// # let surface = wgpu::Surface::create(&pixels_mocks::RWH);
/// # let surface_texture = pixels::SurfaceTexture::new(1024, 768, &surface);
/// let fb = PixelsOptions::new(320, 240)
/// # // TODO: demonstrate adding a render pass here
/// # //.render_pass(...)
/// .build(surface_texture)?;
/// # Ok::<(), pixels::Error>(())
/// ```
impl PixelsOptions {
/// Create a builder that can be finalized into a frame buffer instance.
///
/// # Panics
///
/// Panics when `width` or `height` are 0.
pub fn new(width: u32, height: u32) -> PixelsOptions {
assert!(width > 0);
assert!(height > 0);
PixelsOptions {
request_adapter_options: wgpu::RequestAdapterOptions::default(),
device_descriptor: wgpu::DeviceDescriptor::default(),
width,
height,
}
}
/// Add options for requesting a `wgpu::Adapter`.
pub fn request_adapter_options(mut self, rao: wgpu::RequestAdapterOptions) -> PixelsOptions {
self.request_adapter_options = rao;
self
}
/// Add options for requesting a `wgpu::Device`.
pub fn device_descriptor(mut self, dd: wgpu::DeviceDescriptor) -> PixelsOptions {
self.device_descriptor = dd;
self
}
/// Create a frame buffer from the options builder.
///
/// # Errors
///
/// Returns an error when a `wgpu::Adapter` cannot be found or shaders
/// are invalid SPIR-V.
pub fn build(self, surface_texture: SurfaceTexture) -> Result<Pixels, Error> {
Pixels::new_with_options(surface_texture, self)
}
} }
impl fmt::Display for Error { impl fmt::Display for Error {

41
src/render_pass.rs Normal file
View file

@ -0,0 +1,41 @@
use wgpu::TextureView;
/// 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 {
/// The `update_bindings` method will be called when the `texture_view`
/// needs to be recreated.
///
/// It's always called at least once when the default [`Pixels`] render
/// passes are created. This can also happen in response to resizing the
/// [`SurfaceTexture`].
///
/// You will typically recreate your bindings here to reference the new
/// input `texture_view`.
///
/// [`Pixels`]: ./struct.Pixels.html
/// [`SurfaceTexture`]: ./struct.SurfaceTexture.html
fn update_bindings(&mut self, texture_view: &TextureView);
/// 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`.
fn render_pass(&self, encoder: &mut wgpu::CommandEncoder, render_target: &TextureView);
}