From 8bebb29a06e71ea6fc0fdbdb13e477df1c97091f Mon Sep 17 00:00:00 2001 From: Jay Oster Date: Sun, 27 Oct 2019 16:35:22 -0700 Subject: [PATCH] Fix screen scaling when window is resized (#25) * Refactor window creation and size handling * Require pixel aspect ratio to be > 0 * Fix screen scaling when window is resized - Ensure the screen retains its correct pixel aspect ratio - Updated public API on `RenderPass` ... this will continue to be unstable until the initial release - Add build instructions for the internal shaders --- examples/invaders/main.rs | 79 +++++++++++++++++++++++++++----------- shaders/README.md | 13 +++++++ shaders/shader.vert | 6 ++- shaders/vert.spv | Bin 1200 -> 1484 bytes src/lib.rs | 60 +++++++++++++++++++++-------- src/render_pass.rs | 42 +++++++++++++------- src/renderers.rs | 69 +++++++++++++++++++++++++++++++-- 7 files changed, 212 insertions(+), 57 deletions(-) create mode 100644 shaders/README.md diff --git a/examples/invaders/main.rs b/examples/invaders/main.rs index 0340de4..d1cb912 100644 --- a/examples/invaders/main.rs +++ b/examples/invaders/main.rs @@ -3,6 +3,7 @@ use std::time::Instant; use pixels::{Error, Pixels, SurfaceTexture}; use simple_invaders::{Controls, Direction, World, SCREEN_HEIGHT, SCREEN_WIDTH}; +use winit::dpi::{LogicalPosition, LogicalSize, PhysicalSize}; use winit::event::{Event, VirtualKeyCode, WindowEvent}; use winit::event_loop::{ControlFlow, EventLoop}; use winit_input_helper::WinitInputHelper; @@ -18,29 +19,8 @@ fn main() -> Result<(), Error> { .parse() .unwrap_or(false); - let (window, surface, width, height, mut hidpi_factor) = { - let scale = 3.0; - let width = SCREEN_WIDTH as f64 * scale; - let height = SCREEN_HEIGHT as f64 * scale; - - let window = winit::window::WindowBuilder::new() - .with_inner_size(winit::dpi::LogicalSize::new(width, height)) - .with_title("pixel invaders") - .build(&event_loop) - .unwrap(); - let surface = pixels::wgpu::Surface::create(&window); - let hidpi_factor = window.hidpi_factor(); - let size = window.inner_size().to_physical(hidpi_factor); - - ( - window, - surface, - size.width.round() as u32, - size.height.round() as u32, - hidpi_factor, - ) - }; - + let (window, surface, width, height, mut hidpi_factor) = + create_window("pixel invaders", &event_loop); let surface_texture = SurfaceTexture::new(width, height, surface); let mut pixels = Pixels::new(SCREEN_WIDTH as u32, SCREEN_HEIGHT as u32, surface_texture)?; let mut invaders = World::new(debug); @@ -103,3 +83,56 @@ fn main() -> Result<(), Error> { } }); } + +/// Create a window for the game. +/// +/// Automatically scales the window to cover about 2/3 of the monitor height. +/// +/// # Returns +/// +/// Tuple of `(window, surface, width, height, hidpi_factor)` +/// `width` and `height` are in `LogicalSize` units. +fn create_window( + title: &str, + event_loop: &EventLoop<()>, +) -> (winit::window::Window, wgpu::Surface, u32, u32, f64) { + // Create a hidden window so we can estimate a good default window size + let window = winit::window::WindowBuilder::new() + .with_visible(false) + .with_title(title) + .build(&event_loop) + .unwrap(); + let hidpi_factor = window.hidpi_factor(); + + // Get dimensions + let width = SCREEN_WIDTH as f64; + let height = SCREEN_HEIGHT as f64; + let (monitor_width, monitor_height) = { + let size = window.current_monitor().size(); + (size.width / hidpi_factor, size.height / hidpi_factor) + }; + let scale = (monitor_height / height * 2.0 / 3.0).round(); + + // Resize, center, and display the window + let min_size = PhysicalSize::new(width, height).to_logical(hidpi_factor); + let default_size = LogicalSize::new(width * scale, height * scale); + let center = LogicalPosition::new( + (monitor_width - width * scale) / 2.0, + (monitor_height - height * scale) / 2.0, + ); + window.set_inner_size(default_size); + window.set_min_inner_size(Some(min_size)); + window.set_outer_position(center); + window.set_visible(true); + + let surface = pixels::wgpu::Surface::create(&window); + let size = default_size.to_physical(hidpi_factor); + + ( + window, + surface, + size.width.round() as u32, + size.height.round() as u32, + hidpi_factor, + ) +} diff --git a/shaders/README.md b/shaders/README.md new file mode 100644 index 0000000..153db0b --- /dev/null +++ b/shaders/README.md @@ -0,0 +1,13 @@ +# Shaders + +The GLSL shader source is not compiled as part of the normal cargo build process. This was a conscious decision sparked by the current state of the ecosystem; compiling GLSL-to-SPIR-V requires a C++ toolchain including CMake, which is an unacceptable constraint for a simple crate providing a pixel buffer. + +If you need to modify the GLSL sources, you must also recompile the SPIR-V as well. This can be done with `glslang`, `glslc`, etc. + +Compile shaders with `glslangValidator`: + +```bash +glslangValidator -V shader.frag && glslangValidator -V shader.vert +``` + +For mor information, see https://github.com/parasyte/pixels/issues/9 diff --git a/shaders/shader.vert b/shaders/shader.vert index 8804d9d..3e1d026 100644 --- a/shaders/shader.vert +++ b/shaders/shader.vert @@ -9,6 +9,10 @@ out gl_PerVertex { layout(location = 0) out vec2 v_TexCoord; +layout(set = 0, binding = 2) uniform Locals { + mat4 u_Transform; +}; + const vec2 positions[6] = vec2[6]( // Upper left triangle vec2(-1.0, -1.0), @@ -35,5 +39,5 @@ const vec2 uv[6] = vec2[6]( void main() { v_TexCoord = uv[gl_VertexIndex]; - gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0); + gl_Position = u_Transform * vec4(positions[gl_VertexIndex], 0.0, 1.0); } diff --git a/shaders/vert.spv b/shaders/vert.spv index 2a663f8152b976649a172579cd93a5e451d6de27..9c8f317fa61a865096195c68b4f30590d9f5f2fb 100644 GIT binary patch delta 614 zcmY+A+e!ja6o%J0Gn$kX5fMS5*;#8kst^?gp%+0#-9|{%RV681+bf7KQgqQ}FVL&> z2twbld!T0cX07$F!(Myd(%-`wKao$Fd6u#vt8_9y<-qc0cG|eDKeWt}yo&X_+*~#5 zPp$h#^U*@{ZDAnehZc2uXV*0wG)oVm#y4(`?O%<9Z49amQ$Er6gZ|(-d_7fW>Y&`m ziTCNXrwEmrfdFP*tF$c2JXT{gZqLPPm!|xp^HpCUPGVQU?Deb2x>{)!q={H7;E$Re z{}YR@hy;ac8^NxD2B<-KA%7gSK&kueA9_*u-M?%faiA7jg8m(``_yQ;Wv45iz+Z<+ zIN-t&R_CaH8oL3~RHJ%bViTlaP@_w3ISmtQced?B4|`6V{^|~>Sh)=7avg>!w+mWK VH9EY6EkhP^pyd>+u4kTA;1^K#DZu~$ delta 346 zcmZ9I%?ZL#5QOKM#27RPA|3=G8h@h3zk}dK!3tC?z*1rh66_;j0XAS6UM)dz2BHTK zAM BoxedRenderPass>; +type RenderPassFactory = Box BoxedRenderPass>; /// A logical texture for a window surface. #[derive(Debug)] @@ -148,7 +148,7 @@ 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: Scaling with a uniform transformation matrix + // TODO: Call `update_bindings` on each render pass to create a texture chain // Update SurfaceTexture dimensions self.surface_texture.width = width; @@ -165,6 +165,16 @@ impl Pixels { present_mode: wgpu::PresentMode::Vsync, }, ); + + // Update state for all render passes + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { todo: 0 }); + for renderer in self.renderers.iter_mut() { + renderer.resize(&mut encoder, width, height); + } + + self.queue.borrow_mut().submit(&[encoder.finish()]); } /// Draw this pixel buffer to the configured [`SurfaceTexture`]. @@ -205,7 +215,7 @@ impl Pixels { // 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_pass(&mut encoder, &frame.view); + renderer.render(&mut encoder, &frame.view); } self.queue.borrow_mut().submit(&[encoder.finish()]); @@ -247,13 +257,13 @@ impl PixelsBuilder { /// /// impl pixels::RenderPass for MyRenderPass { /// // ... - /// # fn update_bindings(&mut self, _: &wgpu::TextureView) {} - /// # fn render_pass(&self, _: &mut wgpu::CommandEncoder, _: &wgpu::TextureView) {} + /// # 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| { + /// .add_render_pass(|device, queue, texture, texture_size| { /// // Create reources for MyRenderPass here /// Box::new(MyRenderPass { /// // ... @@ -306,7 +316,13 @@ impl PixelsBuilder { /// factor. /// /// E.g. set this to `8.0 / 7.0` for an 8:7 pixel aspect ratio. - pub const fn pixel_aspect_ratio(mut self, pixel_aspect_ratio: f64) -> PixelsBuilder { + /// + /// # Panics + /// + /// The aspect ratio must be > 0. + pub fn pixel_aspect_ratio(mut self, pixel_aspect_ratio: f64) -> PixelsBuilder { + assert!(pixel_aspect_ratio > 0.0); + self.pixel_aspect_ratio = pixel_aspect_ratio; self } @@ -330,13 +346,14 @@ impl PixelsBuilder { /// * `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. + /// render pass. + /// * `texture_size` - A [`wgpu::Extent3d`] providing the input texture size. /// /// # Examples /// /// ```no_run /// use pixels::{BoxedRenderPass, Device, PixelsBuilder, Queue, RenderPass}; - /// use wgpu::TextureView; + /// use wgpu::{Extent3d, TextureView}; /// /// struct MyRenderPass { /// device: Device, @@ -344,7 +361,12 @@ impl PixelsBuilder { /// } /// /// impl MyRenderPass { - /// fn factory(device: Device, queue: Queue, texture: &TextureView) -> BoxedRenderPass { + /// 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 }) /// } @@ -352,8 +374,8 @@ impl PixelsBuilder { /// /// impl RenderPass for MyRenderPass { /// // ... - /// # fn update_bindings(&mut self, _: &wgpu::TextureView) {} - /// # fn render_pass(&self, _: &mut wgpu::CommandEncoder, _: &wgpu::TextureView) {} + /// # 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); @@ -365,7 +387,7 @@ impl PixelsBuilder { /// ``` pub fn add_render_pass( mut self, - factory: impl Fn(Device, Queue, &TextureView) -> BoxedRenderPass + 'static, + factory: impl Fn(Device, Queue, &TextureView, &Extent3d) -> BoxedRenderPass + 'static, ) -> PixelsBuilder { self.renderer_factories.push(Box::new(factory)); self @@ -430,20 +452,26 @@ impl PixelsBuilder { 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 recieves the texture drawn by the previous - f(device.clone(), queue.clone(), &texture_view) + f( + device.clone(), + queue.clone(), + &texture_view, + &texture_extent, + ) })); Ok(Pixels { device, queue, swap_chain, - renderers, surface_texture, + renderers, texture, texture_extent, texture_format_size, diff --git a/src/render_pass.rs b/src/render_pass.rs index b4568d6..0a642a8 100644 --- a/src/render_pass.rs +++ b/src/render_pass.rs @@ -1,7 +1,7 @@ use std::cell::RefCell; use std::fmt; use std::rc::Rc; -use wgpu::TextureView; +use wgpu::{Extent3d, TextureView}; /// A reference-counted [`wgpu::Device`] pub type Device = Rc; @@ -29,17 +29,6 @@ pub type BoxedRenderPass = Box; /// /// [`Pixels`]: ./struct.Pixels.html pub trait RenderPass { - /// 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. - /// - /// [`Pixels`]: ./struct.Pixels.html - /// [`SurfaceTexture`]: ./struct.SurfaceTexture.html - fn update_bindings(&mut self, input_texture: &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`. /// @@ -47,7 +36,34 @@ pub trait RenderPass { /// * `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_pass(&self, encoder: &mut wgpu::CommandEncoder, render_target: &TextureView); + 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. /// diff --git a/src/renderers.rs b/src/renderers.rs index 4311803..dcd07d5 100644 --- a/src/renderers.rs +++ b/src/renderers.rs @@ -1,7 +1,7 @@ use byteorder::{ByteOrder, LittleEndian}; use std::fmt; use std::rc::Rc; -use wgpu::{self, TextureView}; +use wgpu::{self, Extent3d, TextureView}; use crate::render_pass::{BoxedRenderPass, Device, Queue, RenderPass}; @@ -9,8 +9,11 @@ use crate::render_pass::{BoxedRenderPass, Device, Queue, RenderPass}; #[derive(Debug)] pub(crate) struct Renderer { device: Rc, + uniform_buffer: wgpu::Buffer, bind_group: wgpu::BindGroup, render_pipeline: wgpu::RenderPipeline, + width: f32, + height: f32, } impl Renderer { @@ -19,6 +22,7 @@ impl Renderer { device: Device, _queue: Queue, texture_view: &TextureView, + texture_size: &Extent3d, ) -> BoxedRenderPass { let vert_spv = include_bytes!("../shaders/vert.spv"); let mut vert = Vec::new(); @@ -52,6 +56,18 @@ impl Renderer { compare_function: wgpu::CompareFunction::Always, }); + // Create uniform buffer + #[rustfmt::skip] + let transform: [f32; 16] = [ + 1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0, + ]; + let uniform_buffer = device + .create_buffer_mapped(16, wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST) + .fill_from_slice(&transform); + // Create bind group let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { bindings: &[ @@ -68,6 +84,11 @@ impl Renderer { visibility: wgpu::ShaderStage::FRAGMENT, ty: wgpu::BindingType::Sampler, }, + wgpu::BindGroupLayoutBinding { + binding: 2, + visibility: wgpu::ShaderStage::VERTEX, + ty: wgpu::BindingType::UniformBuffer { dynamic: false }, + }, ], }); let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { @@ -81,6 +102,13 @@ impl Renderer { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler), }, + wgpu::Binding { + binding: 2, + resource: wgpu::BindingResource::Buffer { + buffer: &uniform_buffer, + range: 0..64, + }, + }, ], }); @@ -122,16 +150,17 @@ impl Renderer { Box::new(Renderer { device, + uniform_buffer, bind_group, render_pipeline, + width: texture_size.width as f32, + height: texture_size.height as f32, }) } } impl RenderPass for Renderer { - fn update_bindings(&mut self, _input_texture: &TextureView) {} - - fn render_pass(&self, encoder: &mut wgpu::CommandEncoder, render_target: &TextureView) { + fn render(&self, encoder: &mut wgpu::CommandEncoder, render_target: &TextureView) { // Draw the updated texture to the render target let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor { @@ -148,6 +177,38 @@ impl RenderPass for Renderer { rpass.draw(0..6, 0..1); } + fn resize(&mut self, encoder: &mut wgpu::CommandEncoder, width: u32, height: u32) { + let width = width as f32; + let height = height as f32; + + // Get smallest scale size + let scale = (width / self.width) + .min(height / self.height) + .max(1.0) + .floor(); + + // Update transformation matrix + let sw = self.width * scale / width; + let sh = self.height * scale / height; + #[rustfmt::skip] + let transform: [f32; 16] = [ + sw, 0.0, 0.0, 0.0, + 0.0, sh, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0, + ]; + + let temp_buf = self + .device + .create_buffer_mapped(16, wgpu::BufferUsage::COPY_SRC) + .fill_from_slice(&transform); + 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) }