diff --git a/Cargo.lock b/Cargo.lock index 9b3a2b08..86519fbb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2315,6 +2315,16 @@ dependencies = [ "winit 0.29.9", ] +[[package]] +name = "triangle-util" +version = "0.0.0" +dependencies = [ + "vulkano", + "vulkano-shaders", + "vulkano-util", + "winit 0.29.9", +] + [[package]] name = "triangle-v1_3" version = "0.0.0" diff --git a/examples/interactive-fractal/app.rs b/examples/interactive-fractal/app.rs index 9c546807..a01a624b 100644 --- a/examples/interactive-fractal/app.rs +++ b/examples/interactive-fractal/app.rs @@ -54,7 +54,11 @@ pub struct FractalApp { } impl FractalApp { - pub fn new(gfx_queue: Arc, image_format: vulkano::format::Format) -> FractalApp { + pub fn new( + gfx_queue: Arc, + image_format: vulkano::format::Format, + swapchain_image_views: &[Arc], + ) -> FractalApp { let memory_allocator = Arc::new(StandardMemoryAllocator::new_default( gfx_queue.device().clone(), )); @@ -82,6 +86,7 @@ impl FractalApp { command_buffer_allocator, descriptor_set_allocator, image_format, + swapchain_image_views, ), is_julia: false, is_c_paused: false, diff --git a/examples/interactive-fractal/main.rs b/examples/interactive-fractal/main.rs index 38a936d0..ed6fa4c6 100644 --- a/examples/interactive-fractal/main.rs +++ b/examples/interactive-fractal/main.rs @@ -63,6 +63,7 @@ fn main() -> Result<(), impl Error> { let mut app = FractalApp::new( gfx_queue.clone(), primary_window_renderer.swapchain_format(), + primary_window_renderer.swapchain_image_views(), ); app.print_guide(); @@ -144,7 +145,10 @@ fn compute_then_render( target_image_id: usize, ) { // Start the frame. - let before_pipeline_future = match renderer.acquire() { + let before_pipeline_future = match renderer.acquire(|swapchain_image_views| { + app.place_over_frame + .recreate_framebuffers(swapchain_image_views) + }) { Err(e) => { println!("{e}"); return; @@ -159,9 +163,12 @@ fn compute_then_render( let after_compute = app.compute(image.clone()).join(before_pipeline_future); // Render the image over the swapchain image, inputting the previous future. - let after_renderpass_future = - app.place_over_frame - .render(after_compute, image, renderer.swapchain_image_view()); + let after_renderpass_future = app.place_over_frame.render( + after_compute, + image, + renderer.swapchain_image_view(), + renderer.image_index(), + ); // Finish the frame (which presents the view), inputting the last future. Wait for the future // so resources are not in use when we render. diff --git a/examples/interactive-fractal/place_over_frame.rs b/examples/interactive-fractal/place_over_frame.rs index 798a88a8..78e3524c 100644 --- a/examples/interactive-fractal/place_over_frame.rs +++ b/examples/interactive-fractal/place_over_frame.rs @@ -20,6 +20,7 @@ pub struct RenderPassPlaceOverFrame { render_pass: Arc, pixels_draw_pipeline: PixelsDrawPipeline, command_buffer_allocator: Arc, + framebuffers: Vec>, } impl RenderPassPlaceOverFrame { @@ -28,6 +29,7 @@ impl RenderPassPlaceOverFrame { command_buffer_allocator: Arc, descriptor_set_allocator: Arc, output_format: Format, + swapchain_image_views: &[Arc], ) -> RenderPassPlaceOverFrame { let render_pass = vulkano::single_pass_renderpass!( gfx_queue.device().clone(), @@ -55,9 +57,10 @@ impl RenderPassPlaceOverFrame { RenderPassPlaceOverFrame { gfx_queue, - render_pass, + render_pass: render_pass.clone(), pixels_draw_pipeline, command_buffer_allocator, + framebuffers: create_framebuffers(swapchain_image_views, render_pass), } } @@ -68,6 +71,7 @@ impl RenderPassPlaceOverFrame { before_future: F, view: Arc, target: Arc, + image_index: u32, ) -> Box where F: GpuFuture + 'static, @@ -75,16 +79,6 @@ impl RenderPassPlaceOverFrame { // Get dimensions. let img_dims: [u32; 2] = target.image().extent()[0..2].try_into().unwrap(); - // Create framebuffer (must be in same order as render pass description in `new`. - let framebuffer = Framebuffer::new( - self.render_pass.clone(), - FramebufferCreateInfo { - attachments: vec![target], - ..Default::default() - }, - ) - .unwrap(); - // Create primary command buffer builder. let mut command_buffer_builder = RecordingCommandBuffer::new( self.command_buffer_allocator.clone(), @@ -102,7 +96,9 @@ impl RenderPassPlaceOverFrame { .begin_render_pass( RenderPassBeginInfo { clear_values: vec![Some([0.0; 4].into())], - ..RenderPassBeginInfo::framebuffer(framebuffer) + ..RenderPassBeginInfo::framebuffer( + self.framebuffers[image_index as usize].clone(), + ) }, SubpassBeginInfo { contents: SubpassContents::SecondaryCommandBuffers, @@ -132,4 +128,27 @@ impl RenderPassPlaceOverFrame { after_future.boxed() } + + pub fn recreate_framebuffers(&mut self, swapchain_image_views: &[Arc]) { + self.framebuffers = create_framebuffers(swapchain_image_views, self.render_pass.clone()); + } +} + +fn create_framebuffers( + swapchain_image_views: &[Arc], + render_pass: Arc, +) -> Vec> { + swapchain_image_views + .iter() + .map(|swapchain_image_view| { + Framebuffer::new( + render_pass.clone(), + FramebufferCreateInfo { + attachments: vec![swapchain_image_view.clone()], + ..Default::default() + }, + ) + .unwrap() + }) + .collect::>() } diff --git a/examples/multi-window-game-of-life/app.rs b/examples/multi-window-game-of-life/app.rs index 44e4a9d0..dea21213 100644 --- a/examples/multi-window-game-of-life/app.rs +++ b/examples/multi-window-game-of-life/app.rs @@ -9,10 +9,10 @@ use vulkano::{ }, descriptor_set::allocator::StandardDescriptorSetAllocator, device::Queue, - format::Format, }; use vulkano_util::{ context::{VulkanoConfig, VulkanoContext}, + renderer::VulkanoWindowRenderer, window::{VulkanoWindows, WindowDescriptor}, }; use winit::{event_loop::EventLoop, window::WindowId}; @@ -28,11 +28,11 @@ impl RenderPipeline { compute_queue: Arc, gfx_queue: Arc, size: [u32; 2], - swapchain_format: Format, + window_renderer: &VulkanoWindowRenderer, ) -> RenderPipeline { RenderPipeline { compute: GameOfLifeComputePipeline::new(app, compute_queue, size), - place_over_frame: RenderPassPlaceOverFrame::new(app, gfx_queue, swapchain_format), + place_over_frame: RenderPassPlaceOverFrame::new(app, gfx_queue, window_renderer), } } } @@ -81,10 +81,7 @@ impl App { (WINDOW_WIDTH / SCALING) as u32, (WINDOW_HEIGHT / SCALING) as u32, ], - self.windows - .get_primary_renderer() - .unwrap() - .swapchain_format(), + self.windows.get_primary_renderer().unwrap(), ), ); self.pipelines.insert( @@ -97,7 +94,7 @@ impl App { (WINDOW2_WIDTH / SCALING) as u32, (WINDOW2_HEIGHT / SCALING) as u32, ], - self.windows.get_renderer(id2).unwrap().swapchain_format(), + self.windows.get_renderer(id2).unwrap(), ), ); } diff --git a/examples/multi-window-game-of-life/main.rs b/examples/multi-window-game-of-life/main.rs index b1881eed..bd4c1426 100644 --- a/examples/multi-window-game-of-life/main.rs +++ b/examples/multi-window-game-of-life/main.rs @@ -194,7 +194,11 @@ fn compute_then_render( } // Start the frame. - let before_pipeline_future = match window_renderer.acquire() { + let before_pipeline_future = match window_renderer.acquire(|swapchain_image_views| { + pipeline + .place_over_frame + .recreate_framebuffers(swapchain_image_views) + }) { Err(e) => { println!("{e}"); return; @@ -211,9 +215,12 @@ fn compute_then_render( let color_image = pipeline.compute.color_image(); let target_image = window_renderer.swapchain_image_view(); - let after_render = pipeline - .place_over_frame - .render(after_compute, color_image, target_image); + let after_render = pipeline.place_over_frame.render( + after_compute, + color_image, + target_image, + window_renderer.image_index(), + ); // Finish the frame. Wait for the future so resources are not in use when we render. window_renderer.present(after_render, true); diff --git a/examples/multi-window-game-of-life/render_pass.rs b/examples/multi-window-game-of-life/render_pass.rs index 62c59040..af2dd9e0 100644 --- a/examples/multi-window-game-of-life/render_pass.rs +++ b/examples/multi-window-game-of-life/render_pass.rs @@ -7,11 +7,11 @@ use vulkano::{ SubpassContents, }, device::Queue, - format::Format, image::view::ImageView, render_pass::{Framebuffer, FramebufferCreateInfo, RenderPass, Subpass}, sync::GpuFuture, }; +use vulkano_util::renderer::VulkanoWindowRenderer; /// A render pass which places an incoming image over the frame, filling it. pub struct RenderPassPlaceOverFrame { @@ -19,19 +19,20 @@ pub struct RenderPassPlaceOverFrame { render_pass: Arc, pixels_draw_pipeline: PixelsDrawPipeline, command_buffer_allocator: Arc, + framebuffers: Vec>, } impl RenderPassPlaceOverFrame { pub fn new( app: &App, gfx_queue: Arc, - output_format: Format, + window_renderer: &VulkanoWindowRenderer, ) -> RenderPassPlaceOverFrame { let render_pass = vulkano::single_pass_renderpass!( gfx_queue.device().clone(), attachments: { color: { - format: output_format, + format: window_renderer.swapchain_format(), samples: 1, load_op: Clear, store_op: Store, @@ -48,9 +49,10 @@ impl RenderPassPlaceOverFrame { RenderPassPlaceOverFrame { gfx_queue, - render_pass, + render_pass: render_pass.clone(), pixels_draw_pipeline, command_buffer_allocator: app.command_buffer_allocator.clone(), + framebuffers: create_framebuffers(window_renderer.swapchain_image_views(), render_pass), } } @@ -61,6 +63,7 @@ impl RenderPassPlaceOverFrame { before_future: F, image_view: Arc, target: Arc, + image_index: u32, ) -> Box where F: GpuFuture + 'static, @@ -68,16 +71,6 @@ impl RenderPassPlaceOverFrame { // Get the dimensions. let img_dims: [u32; 2] = target.image().extent()[0..2].try_into().unwrap(); - // Create the framebuffer. - let framebuffer = Framebuffer::new( - self.render_pass.clone(), - FramebufferCreateInfo { - attachments: vec![target], - ..Default::default() - }, - ) - .unwrap(); - // Create a primary command buffer builder. let mut command_buffer_builder = RecordingCommandBuffer::new( self.command_buffer_allocator.clone(), @@ -95,7 +88,9 @@ impl RenderPassPlaceOverFrame { .begin_render_pass( RenderPassBeginInfo { clear_values: vec![Some([0.0; 4].into())], - ..RenderPassBeginInfo::framebuffer(framebuffer) + ..RenderPassBeginInfo::framebuffer( + self.framebuffers[image_index as usize].clone(), + ) }, SubpassBeginInfo { contents: SubpassContents::SecondaryCommandBuffers, @@ -125,4 +120,27 @@ impl RenderPassPlaceOverFrame { after_future.boxed() } + + pub fn recreate_framebuffers(&mut self, swapchain_image_views: &[Arc]) { + self.framebuffers = create_framebuffers(swapchain_image_views, self.render_pass.clone()); + } +} + +fn create_framebuffers( + swapchain_image_views: &[Arc], + render_pass: Arc, +) -> Vec> { + swapchain_image_views + .iter() + .map(|swapchain_image_view| { + Framebuffer::new( + render_pass.clone(), + FramebufferCreateInfo { + attachments: vec![swapchain_image_view.clone()], + ..Default::default() + }, + ) + .unwrap() + }) + .collect::>() } diff --git a/examples/triangle-util/Cargo.toml b/examples/triangle-util/Cargo.toml new file mode 100644 index 00000000..a59818c4 --- /dev/null +++ b/examples/triangle-util/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "triangle-util" +version = "0.0.0" +edition = "2021" +publish = false + +[[bin]] +name = "triangle-util" +path = "main.rs" +test = false +bench = false +doc = false + +[dependencies] +# The `vulkano` crate is the main crate that you must use to use Vulkan. +vulkano = { workspace = true, features = ["macros"] } +# Provides the `shader!` macro that is used to generate code for using shaders. +vulkano-shaders = { workspace = true } +# Contains the utility functions that make life easier. +vulkano-util = { workspace = true } +# The Vulkan library doesn't provide any functionality to create and handle windows, as +# this would be out of scope. In order to open a window, we are going to use the `winit` crate. +winit = { workspace = true } diff --git a/examples/triangle-util/main.rs b/examples/triangle-util/main.rs new file mode 100644 index 00000000..00b36bd6 --- /dev/null +++ b/examples/triangle-util/main.rs @@ -0,0 +1,463 @@ +// Welcome to the triangle-util example! +// +// This is almost exactly the same as the triange example, except that it uses utility functions +// to make life easier. +// +// This example assumes that you are already more or less familiar with graphics programming and +// that you want to learn Vulkan. This means that for example it won't go into details about what a +// vertex or a shader is. + +use std::{error::Error, sync::Arc}; +use vulkano::{ + buffer::{Buffer, BufferContents, BufferCreateInfo, BufferUsage}, + command_buffer::{ + allocator::StandardCommandBufferAllocator, CommandBufferBeginInfo, CommandBufferLevel, + CommandBufferUsage, RecordingCommandBuffer, RenderPassBeginInfo, SubpassBeginInfo, + SubpassContents, + }, + image::view::ImageView, + memory::allocator::{AllocationCreateInfo, MemoryTypeFilter}, + pipeline::{ + graphics::{ + color_blend::{ColorBlendAttachmentState, ColorBlendState}, + input_assembly::InputAssemblyState, + multisample::MultisampleState, + rasterization::RasterizationState, + vertex_input::{Vertex, VertexDefinition}, + viewport::{Viewport, ViewportState}, + GraphicsPipelineCreateInfo, + }, + layout::PipelineDescriptorSetLayoutCreateInfo, + DynamicState, GraphicsPipeline, PipelineLayout, PipelineShaderStageCreateInfo, + }, + render_pass::{Framebuffer, FramebufferCreateInfo, RenderPass, Subpass}, + sync::GpuFuture, +}; +use vulkano_util::{ + context::{VulkanoConfig, VulkanoContext}, + window::VulkanoWindows, +}; +use winit::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, +}; + +fn main() -> Result<(), impl Error> { + let context = VulkanoContext::new(VulkanoConfig::default()); + let event_loop = EventLoop::new().unwrap(); + // Manages any windows and their rendering. + let mut windows_manager = VulkanoWindows::default(); + windows_manager.create_window(&event_loop, &context, &Default::default(), |_| {}); + let window_renderer = windows_manager.get_primary_renderer_mut().unwrap(); + + // Some little debug infos. + println!( + "Using device: {} (type: {:?})", + context.device().physical_device().properties().device_name, + context.device().physical_device().properties().device_type, + ); + + // We now create a buffer that will store the shape of our triangle. We use `#[repr(C)]` here + // to force rustc to use a defined layout for our data, as the default representation has *no + // guarantees*. + #[derive(BufferContents, Vertex)] + #[repr(C)] + struct Vertex { + #[format(R32G32_SFLOAT)] + position: [f32; 2], + } + + let vertices = [ + Vertex { + position: [-0.5, -0.25], + }, + Vertex { + position: [0.0, 0.5], + }, + Vertex { + position: [0.25, -0.1], + }, + ]; + let vertex_buffer = Buffer::from_iter( + context.memory_allocator().clone(), + BufferCreateInfo { + usage: BufferUsage::VERTEX_BUFFER, + ..Default::default() + }, + AllocationCreateInfo { + memory_type_filter: MemoryTypeFilter::PREFER_DEVICE + | MemoryTypeFilter::HOST_SEQUENTIAL_WRITE, + ..Default::default() + }, + vertices, + ) + .unwrap(); + + // The next step is to create the shaders. + // + // The raw shader creation API provided by the vulkano library is unsafe for various reasons, + // so The `shader!` macro provides a way to generate a Rust module from GLSL source - in the + // example below, the source is provided as a string input directly to the shader, but a path + // to a source file can be provided as well. Note that the user must specify the type of shader + // (e.g. "vertex", "fragment", etc.) using the `ty` option of the macro. + // + // The items generated by the `shader!` macro include a `load` function which loads the shader + // using an input logical device. The module also includes type definitions for layout + // structures defined in the shader source, for example uniforms and push constants. + // + // A more detailed overview of what the `shader!` macro generates can be found in the + // vulkano-shaders crate docs. You can view them at https://docs.rs/vulkano-shaders/ + mod vs { + vulkano_shaders::shader! { + ty: "vertex", + src: r" + #version 450 + + layout(location = 0) in vec2 position; + + void main() { + gl_Position = vec4(position, 0.0, 1.0); + } + ", + } + } + + mod fs { + vulkano_shaders::shader! { + ty: "fragment", + src: r" + #version 450 + + layout(location = 0) out vec4 f_color; + + void main() { + f_color = vec4(1.0, 0.0, 0.0, 1.0); + } + ", + } + } + + // At this point, OpenGL initialization would be finished. However in Vulkan it is not. OpenGL + // implicitly does a lot of computation whenever you draw. In Vulkan, you have to do all this + // manually. + + // The next step is to create a *render pass*, which is an object that describes where the + // output of the graphics pipeline will go. It describes the layout of the images where the + // colors, depth and/or stencil information will be written. + let render_pass = vulkano::single_pass_renderpass!( + context.device().clone(), + attachments: { + // `color` is a custom name we give to the first and only attachment. + color: { + // `format: ` indicates the type of the format of the image. This has to be one + // of the types of the `vulkano::format` module (or alternatively one of your + // structs that implements the `FormatDesc` trait). Here we use the same format as + // the swapchain. + format: window_renderer.swapchain_format(), + // `samples: 1` means that we ask the GPU to use one sample to determine the value + // of each pixel in the color attachment. We could use a larger value + // (multisampling) for antialiasing. An example of this can be found in + // msaa-renderpass.rs. + samples: 1, + // `load_op: Clear` means that we ask the GPU to clear the content of this + // attachment at the start of the drawing. + load_op: Clear, + // `store_op: Store` means that we ask the GPU to store the output of the draw in + // the actual image. We could also ask it to discard the result. + store_op: Store, + }, + }, + pass: { + // We use the attachment named `color` as the one and only color attachment. + color: [color], + // No depth-stencil attachment is indicated with empty brackets. + depth_stencil: {}, + }, + ) + .unwrap(); + + // Before we draw, we have to create what is called a **pipeline**. A pipeline describes how + // a GPU operation is to be performed. It is similar to an OpenGL program, but it also contains + // many settings for customization, all baked into a single object. For drawing, we create + // a **graphics** pipeline, but there are also other types of pipeline. + let pipeline = { + // First, we load the shaders that the pipeline will use: + // the vertex shader and the fragment shader. + // + // A Vulkan shader can in theory contain multiple entry points, so we have to specify which + // one. + let vs = vs::load(context.device().clone()) + .unwrap() + .entry_point("main") + .unwrap(); + let fs = fs::load(context.device().clone()) + .unwrap() + .entry_point("main") + .unwrap(); + + // Automatically generate a vertex input state from the vertex shader's input interface, + // that takes a single vertex buffer containing `Vertex` structs. + let vertex_input_state = Vertex::per_vertex() + .definition(&vs.info().input_interface) + .unwrap(); + + // Make a list of the shader stages that the pipeline will have. + let stages = [ + PipelineShaderStageCreateInfo::new(vs), + PipelineShaderStageCreateInfo::new(fs), + ]; + + // We must now create a **pipeline layout** object, which describes the locations and types + // of descriptor sets and push constants used by the shaders in the pipeline. + // + // Multiple pipelines can share a common layout object, which is more efficient. + // The shaders in a pipeline must use a subset of the resources described in its pipeline + // layout, but the pipeline layout is allowed to contain resources that are not present in + // the shaders; they can be used by shaders in other pipelines that share the same + // layout. Thus, it is a good idea to design shaders so that many pipelines have + // common resource locations, which allows them to share pipeline layouts. + let layout = PipelineLayout::new( + context.device().clone(), + // Since we only have one pipeline in this example, and thus one pipeline layout, + // we automatically generate the creation info for it from the resources used in the + // shaders. In a real application, you would specify this information manually so that + // you can re-use one layout in multiple pipelines. + PipelineDescriptorSetLayoutCreateInfo::from_stages(&stages) + .into_pipeline_layout_create_info(context.device().clone()) + .unwrap(), + ) + .unwrap(); + + // We have to indicate which subpass of which render pass this pipeline is going to be used + // in. The pipeline will only be usable from this particular subpass. + let subpass = Subpass::from(render_pass.clone(), 0).unwrap(); + + // Finally, create the pipeline. + GraphicsPipeline::new( + context.device().clone(), + None, + GraphicsPipelineCreateInfo { + stages: stages.into_iter().collect(), + // How vertex data is read from the vertex buffers into the vertex shader. + vertex_input_state: Some(vertex_input_state), + // How vertices are arranged into primitive shapes. + // The default primitive shape is a triangle. + input_assembly_state: Some(InputAssemblyState::default()), + // How primitives are transformed and clipped to fit the framebuffer. + // We use a resizable viewport, set to draw over the entire window. + viewport_state: Some(ViewportState::default()), + // How polygons are culled and converted into a raster of pixels. + // The default value does not perform any culling. + rasterization_state: Some(RasterizationState::default()), + // How multiple fragment shader samples are converted to a single pixel value. + // The default value does not perform any multisampling. + multisample_state: Some(MultisampleState::default()), + // How pixel values are combined with the values already present in the framebuffer. + // The default value overwrites the old value with the new one, without any + // blending. + color_blend_state: Some(ColorBlendState::with_attachment_states( + subpass.num_color_attachments(), + ColorBlendAttachmentState::default(), + )), + // Dynamic states allows us to specify parts of the pipeline settings when + // recording the command buffer, before we perform drawing. + // Here, we specify that the viewport should be dynamic. + dynamic_state: [DynamicState::Viewport].into_iter().collect(), + subpass: Some(subpass.into()), + ..GraphicsPipelineCreateInfo::layout(layout) + }, + ) + .unwrap() + }; + + // Dynamic viewports allow us to recreate just the viewport when the window is resized. + // Otherwise we would have to recreate the whole pipeline. + let mut viewport = Viewport { + offset: [0.0, 0.0], + extent: [0.0, 0.0], + depth_range: 0.0..=1.0, + }; + + // The render pass we created above only describes the layout of our framebuffers. Before we + // can draw we also need to create the actual framebuffers. + // + // Since we need to draw to multiple images, we are going to create a different framebuffer for + // each image. + let mut framebuffers = window_size_dependent_setup( + window_renderer.swapchain_image_views(), + render_pass.clone(), + &mut viewport, + ); + + // Before we can start creating and recording command buffers, we need a way of allocating + // them. Vulkano provides a command buffer allocator, which manages raw Vulkan command pools + // underneath and provides a safe interface for them. + let command_buffer_allocator = Arc::new(StandardCommandBufferAllocator::new( + context.device().clone(), + Default::default(), + )); + + // Initialization is finally finished! + + // In the loop below we are going to submit commands to the GPU. Submitting a command produces + // an object that implements the `GpuFuture` trait, which holds the resources for as long as + // they are in use by the GPU. + + event_loop.run(move |event, elwt| { + elwt.set_control_flow(ControlFlow::Poll); + + match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => { + elwt.exit(); + } + Event::WindowEvent { + event: WindowEvent::Resized(_), + .. + } => { + window_renderer.resize(); + } + Event::WindowEvent { + event: WindowEvent::RedrawRequested, + .. + } => { + // Do not draw the frame when the screen size is zero. On Windows, this can + // occur when minimizing the application. + let image_extent: [u32; 2] = window_renderer.window().inner_size().into(); + + if image_extent.contains(&0) { + return; + } + + // Begin rendering by acquiring the gpu future from the window renderer. + let previous_frame_end = window_renderer + .acquire(|swapchain_images| { + // Whenever the window resizes we need to recreate everything dependent on + // the window size. In this example that includes + // the swapchain, the framebuffers and the dynamic + // state viewport. + framebuffers = window_size_dependent_setup( + swapchain_images, + render_pass.clone(), + &mut viewport, + ); + }) + .unwrap(); + + // In order to draw, we have to record a *command buffer*. The command buffer object + // holds the list of commands that are going to be executed. + // + // Recording a command buffer is an expensive operation (usually a few hundred + // microseconds), but it is known to be a hot path in the driver and is expected to + // be optimized. + // + // Note that we have to pass a queue family when we create the command buffer. The + // command buffer will only be executable on that given queue family. + let mut builder = RecordingCommandBuffer::new( + command_buffer_allocator.clone(), + context.graphics_queue().queue_family_index(), + CommandBufferLevel::Primary, + CommandBufferBeginInfo { + usage: CommandBufferUsage::OneTimeSubmit, + ..Default::default() + }, + ) + .unwrap(); + + builder + // Before we can draw, we have to *enter a render pass*. + .begin_render_pass( + RenderPassBeginInfo { + // A list of values to clear the attachments with. This list contains + // one item for each attachment in the render pass. In this case, there + // is only one attachment, and we clear it with a blue color. + // + // Only attachments that have `AttachmentLoadOp::Clear` are provided + // with clear values, any others should use `None` as the clear value. + clear_values: vec![Some([0.0, 0.0, 1.0, 1.0].into())], + + ..RenderPassBeginInfo::framebuffer( + framebuffers[window_renderer.image_index() as usize].clone(), + ) + }, + SubpassBeginInfo { + // The contents of the first (and only) subpass. + // This can be either `Inline` or `SecondaryCommandBuffers`. + // The latter is a bit more advanced and is not covered here. + contents: SubpassContents::Inline, + ..Default::default() + }, + ) + .unwrap() + // We are now inside the first subpass of the render pass. + // + // TODO: Document state setting and how it affects subsequent draw commands. + .set_viewport(0, [viewport.clone()].into_iter().collect()) + .unwrap() + .bind_pipeline_graphics(pipeline.clone()) + .unwrap() + .bind_vertex_buffers(0, vertex_buffer.clone()) + .unwrap(); + + unsafe { + builder + // We add a draw command. + .draw(vertex_buffer.len() as u32, 1, 0, 0) + .unwrap(); + } + + builder + // We leave the render pass. Note that if we had multiple subpasses we could + // have called `next_subpass` to jump to the next subpass. + .end_render_pass(Default::default()) + .unwrap(); + + // Finish recording the command buffer by calling `end`. + let command_buffer = builder.end().unwrap(); + + let future = previous_frame_end + .then_execute(context.graphics_queue().clone(), command_buffer) + .unwrap() + .boxed(); + + // The color output is now expected to contain our triangle. But in order to + // show it on the screen, we have to *present* the image by calling + // `present` on the window renderer. + // + // This function does not actually present the image immediately. Instead it + // submits a present command at the end of the queue. This means that it will + // only be presented once the GPU has finished executing the command buffer + // that draws the triangle. + window_renderer.present(future, false); + } + Event::AboutToWait => window_renderer.window().request_redraw(), + _ => (), + } + }) +} + +/// This function is called once during initialization, then again whenever the window is resized. +fn window_size_dependent_setup( + swapchain_images: &[Arc], + render_pass: Arc, + viewport: &mut Viewport, +) -> Vec> { + let extent = swapchain_images[0].image().extent(); + viewport.extent = [extent[0] as f32, extent[1] as f32]; + + swapchain_images + .iter() + .map(|swapchain_image| { + Framebuffer::new( + render_pass.clone(), + FramebufferCreateInfo { + attachments: vec![swapchain_image.clone()], + ..Default::default() + }, + ) + .unwrap() + }) + .collect::>() +} diff --git a/vulkano-util/src/renderer.rs b/vulkano-util/src/renderer.rs index db13f9dd..3f1c4d54 100644 --- a/vulkano-util/src/renderer.rs +++ b/vulkano-util/src/renderer.rs @@ -203,6 +203,16 @@ impl VulkanoWindowRenderer { dims[0] / dims[1] } + /// Returns a reference to the swapchain image views. + #[inline] + #[must_use] + // swapchain_image_views or swapchain_images_views, neither sounds good. + pub fn swapchain_image_views(&self) -> &Vec> { + // Why do we use "final views" as the field name, + // yet always externally refer to them as "swapchain image views"? + &self.final_views + } + /// Resize swapchain and camera view images at the beginning of next frame based on window /// size. #[inline] @@ -245,16 +255,21 @@ impl VulkanoWindowRenderer { } /// Begin your rendering by calling `acquire`. - /// Returns a [`GpuFuture`] representing the time after which the - /// swapchain image has been acquired and previous frame ended. - /// Execute your command buffers after calling this function and finish rendering by calling - /// [`VulkanoWindowRenderer::present`]. + /// 'on_recreate_swapchain' is called when the swapchain gets recreated, due to being resized, + /// suboptimal, or changing the present mode. Returns a [`GpuFuture`] representing the time + /// after which the swapchain image has been acquired and previous frame ended. + /// Execute your command buffers after calling this function and + /// finish rendering by calling [`VulkanoWindowRenderer::present`]. #[inline] - pub fn acquire(&mut self) -> Result, VulkanError> { + pub fn acquire( + &mut self, + on_recreate_swapchain: impl FnOnce(&Vec>), + ) -> Result, VulkanError> { // Recreate swap chain if needed (when resizing of window occurs or swapchain is outdated) // Also resize render views if needed if self.recreate_swapchain { self.recreate_swapchain_and_views(); + on_recreate_swapchain(&self.final_views); } // Acquire next image in the swapchain