From 7dd1dc2f9dc1f08b3aae51367b0f1e85051e94c7 Mon Sep 17 00:00:00 2001 From: firestar99 Date: Tue, 30 Sep 2025 10:58:28 +0200 Subject: [PATCH 01/12] ash runner: make debug msgs easier to read --- examples/runners/ash/src/main.rs | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/examples/runners/ash/src/main.rs b/examples/runners/ash/src/main.rs index 1551c22901..6be3efb63d 100644 --- a/examples/runners/ash/src/main.rs +++ b/examples/runners/ash/src/main.rs @@ -1320,33 +1320,21 @@ unsafe fn any_as_u8_slice(p: &T) -> &[u8] { unsafe extern "system" fn vulkan_debug_callback( message_severity: vk::DebugUtilsMessageSeverityFlagsEXT, - message_type: vk::DebugUtilsMessageTypeFlagsEXT, + _message_type: vk::DebugUtilsMessageTypeFlagsEXT, p_callback_data: *const vk::DebugUtilsMessengerCallbackDataEXT<'_>, _user_data: *mut std::os::raw::c_void, ) -> vk::Bool32 { let callback_data = unsafe { *p_callback_data }; - let message_id_number: i32 = callback_data.message_id_number; - let message_id_name = if callback_data.p_message_id_name.is_null() { Cow::from("") } else { unsafe { CStr::from_ptr(callback_data.p_message_id_name).to_string_lossy() } }; - let message = if callback_data.p_message.is_null() { Cow::from("") } else { unsafe { CStr::from_ptr(callback_data.p_message).to_string_lossy() } }; - - println!( - "{:?}:\n{:?} [{} ({})] : {}\n", - message_severity, - message_type, - message_id_name, - &message_id_number.to_string(), - message, - ); - + println!("{message_severity:?}: [{message_id_name}] : {message}"); vk::FALSE } From 85ce83707ae6731783d23c71a44f69f4aa4fb17d Mon Sep 17 00:00:00 2001 From: firestar99 Date: Tue, 30 Sep 2025 10:24:15 +0200 Subject: [PATCH 02/12] ash runner: constant window updates, don't wait for new events --- examples/runners/ash/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/runners/ash/src/main.rs b/examples/runners/ash/src/main.rs index 6be3efb63d..b4744e2d35 100644 --- a/examples/runners/ash/src/main.rs +++ b/examples/runners/ash/src/main.rs @@ -236,7 +236,7 @@ pub fn main() { WindowEvent::CloseRequested => event_loop_window_target.exit(), _ => {} }, - _ => event_loop_window_target.set_control_flow(ControlFlow::Wait), + _ => event_loop_window_target.set_control_flow(ControlFlow::Poll), }) .unwrap(); } From 29bd9398b228c67a941f21a21eaa78703c4c5f28 Mon Sep 17 00:00:00 2001 From: firestar99 Date: Tue, 30 Sep 2025 10:22:37 +0200 Subject: [PATCH 03/12] ash runner: remove multimodule, simplify pipeline creation --- Cargo.lock | 1 + examples/runners/ash/Cargo.toml | 1 + examples/runners/ash/src/main.rs | 411 ++++++++++--------------------- 3 files changed, 138 insertions(+), 275 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fdc5a17b1c..5e1f988318 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -934,6 +934,7 @@ dependencies = [ name = "example-runner-ash" version = "0.0.0" dependencies = [ + "anyhow", "ash", "ash-molten", "ash-window", diff --git a/examples/runners/ash/Cargo.toml b/examples/runners/ash/Cargo.toml index ea00020feb..3818c3b6b3 100644 --- a/examples/runners/ash/Cargo.toml +++ b/examples/runners/ash/Cargo.toml @@ -22,6 +22,7 @@ clap = { version = "4", features = ["derive"] } cfg-if = "1.0.0" shared = { path = "../../shaders/shared" } spirv-builder = { workspace = true, default-features = false } +anyhow = "1.0.98" [target.'cfg(target_os = "macos")'.dependencies] ash-molten = { version = "0.20", features = ["pre-built"] } diff --git a/examples/runners/ash/src/main.rs b/examples/runners/ash/src/main.rs index b4744e2d35..9aa3d3cc88 100644 --- a/examples/runners/ash/src/main.rs +++ b/examples/runners/ash/src/main.rs @@ -71,13 +71,10 @@ // #![allow()] use ash::{ext, khr, util::read_spv, vk}; - +use clap::{Parser, ValueEnum}; use raw_window_handle::{HasDisplayHandle as _, HasWindowHandle as _}; -use winit::{ - event::{Event, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, -}; - +use shared::ShaderConstants; +use spirv_builder::{MetadataPrintout, SpirvBuilder}; use std::{ borrow::Cow, ffi::{CStr, CString}, @@ -87,12 +84,10 @@ use std::{ sync::mpsc::{TryRecvError, TrySendError, sync_channel}, thread, }; - -use clap::{Parser, ValueEnum}; - -use spirv_builder::{MetadataPrintout, SpirvBuilder}; - -use shared::ShaderConstants; +use winit::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, +}; // This runner currently doesn't run the `compute` shader example. #[derive(Debug, PartialEq, Eq, Copy, Clone, ValueEnum)] @@ -136,7 +131,7 @@ pub fn main() { } let options = Options::parse(); - let (vert_data, frag_data) = compile_shaders(&options.shader); + let shader_data = compile_shaders(&options.shader).unwrap(); // runtime setup let event_loop = EventLoop::new().unwrap(); @@ -153,15 +148,9 @@ pub fn main() { )), ) .unwrap(); - let mut ctx = RenderBase::new(window, &options).into_ctx(); - - // Insert shader modules. - ctx.update_shader_modules(&vert_data, &frag_data); - - // Create pipeline. - ctx.rebuild_pipeline(vk::PipelineCache::null()); - - let (compiler_sender, compiler_receiver) = sync_channel::<(Vec, Vec)>(1); + let mut ctx = RenderCtx::new(RenderBase::new(window, &options), shader_data); + ctx.rebuild_pipeline(); + let (compiler_sender, compiler_receiver) = sync_channel::>(1); // FIXME(eddyb) incomplete `winit` upgrade, follow the guides in: // https://github.com/rust-windowing/winit/releases/tag/v0.30.0 @@ -181,10 +170,10 @@ pub fn main() { ctx.render(); } } - Ok((new_vert_data, new_frag_data)) => { - ctx.update_shader_modules(&new_vert_data, &new_frag_data); + Ok(shader_data) => { ctx.recompiling_shaders = false; - ctx.rebuild_pipeline(vk::PipelineCache::null()); + ctx.shader_code = shader_data; + ctx.rebuild_pipeline(); } Err(TryRecvError::Disconnected) => { panic!("compiler receiver disconnected unexpectedly"); @@ -207,8 +196,8 @@ pub fn main() { ctx.recompiling_shaders = true; let compiler_sender = compiler_sender.clone(); thread::spawn(move || { - if let Err(TrySendError::Disconnected(_)) = - compiler_sender.try_send(compile_shaders(&options.shader)) + if let Err(TrySendError::Disconnected(_)) = compiler_sender + .try_send(compile_shaders(&options.shader).unwrap()) { panic!("compiler sender disconnected unexpectedly"); }; @@ -226,7 +215,7 @@ pub fn main() { // HACK(eddyb) to see any changes, re-specializing the // shader module is needed (e.g. during pipeline rebuild). - ctx.rebuild_pipeline(vk::PipelineCache::null()); + ctx.rebuild_pipeline(); } _ => {} }, @@ -241,48 +230,29 @@ pub fn main() { .unwrap(); } -pub fn compile_shaders(shader: &RustGPUShader) -> (Vec, Vec) { +pub fn compile_shaders(shader: &RustGPUShader) -> anyhow::Result> { let manifest_dir = env!("CARGO_MANIFEST_DIR"); let crate_path = [manifest_dir, "..", "..", "shaders", shader.crate_name()] .iter() .copied() .collect::(); - let mut shaders = SpirvBuilder::new(crate_path, "spirv-unknown-vulkan1.1") + let compile_result = SpirvBuilder::new(crate_path, "spirv-unknown-vulkan1.1") .print_metadata(MetadataPrintout::None) .shader_panic_strategy(spirv_builder::ShaderPanicStrategy::DebugPrintfThenExit { print_inputs: true, print_backtrace: true, }) - // TODO: `multimodule` is no longer needed since - // https://github.com/KhronosGroup/SPIRV-Tools/issues/4892 was fixed, but removing it is - // non-trivial and hasn't been done yet. - .multimodule(true) - .build() - .unwrap() - .module - .unwrap_multi() - .iter() - .map(|(name, path)| { - ( - name.clone(), - read_spv(&mut File::open(path).unwrap()).unwrap(), - ) - }) - .collect::>(); - - // We always have two shaders. And the fragment shader is always before the - // vertex shader in `shaders`. This is because `unwrap_multi` returns a - // `BTreeMap` sorted by shader name, and `main_fs` comes before `main_vs`, - // alphabetically. We still check the names to make sure they are in the - // order we expect. That way if the order ever changes we'll get an - // assertion failure here as opposed to a harder-to-debug failure later on. + .build()?; + let spv_path = compile_result.module.unwrap_single(); + + // Assert that we always have these two shaders + let shaders = &compile_result.entry_points; assert_eq!(shaders.len(), 2); - assert_eq!(shaders[0].0, "main_fs"); - assert_eq!(shaders[1].0, "main_vs"); - let vert = shaders.pop().unwrap().1; - let frag = shaders.pop().unwrap().1; - (vert, frag) + assert!(shaders.contains(&"main_vs".to_string())); + assert!(shaders.contains(&"main_fs".to_string())); + + Ok(read_spv(&mut File::open(spv_path)?)?) } pub struct RenderBase { @@ -662,10 +632,6 @@ impl RenderBase { pub fn create_render_sync(&self) -> RenderSync { RenderSync::new(self) } - - pub fn into_ctx(self) -> RenderCtx { - RenderCtx::from_base(self) - } } impl Drop for RenderBase { @@ -695,9 +661,8 @@ pub struct RenderCtx { pub commands: RenderCommandPool, pub viewports: Box<[vk::Viewport]>, pub scissors: Box<[vk::Rect2D]>, + pub shader_code: Vec, pub pipeline: Option, - pub vert_module: Option, - pub frag_module: Option, pub rendering_paused: bool, pub recompiling_shaders: bool, @@ -709,7 +674,7 @@ pub struct RenderCtx { } impl RenderCtx { - pub fn from_base(base: RenderBase) -> Self { + pub fn new(base: RenderBase, shader_code: Vec) -> Self { let sync = RenderSync::new(&base); let (swapchain, extent) = base.create_swapchain(); @@ -745,9 +710,8 @@ impl RenderCtx { framebuffers, viewports, scissors, + shader_code, pipeline: None, - vert_module: None, - frag_module: None, rendering_paused: false, recompiling_shaders: false, start: std::time::Instant::now(), @@ -756,92 +720,119 @@ impl RenderCtx { } } - pub fn create_pipeline_layout(&self) -> vk::PipelineLayout { - let push_constant_range = vk::PushConstantRange::default() - .offset(0) - .size(std::mem::size_of::() as u32) - .stage_flags(vk::ShaderStageFlags::ALL); + /// Update shaders and rebuild the pipeline + pub fn rebuild_pipeline(&mut self) { unsafe { - self.base + self.cleanup_pipeline(); + + let shader_module = self + .base .device - .create_pipeline_layout( - &vk::PipelineLayoutCreateInfo::default() - .push_constant_ranges(&[push_constant_range]), + .create_shader_module( + &vk::ShaderModuleCreateInfo::default().code(&self.shader_code), None, ) - .unwrap() - } - } + .unwrap(); - pub fn rebuild_pipeline(&mut self, pipeline_cache: vk::PipelineCache) { - // NOTE(eddyb) this acts like an integration test for specialization constants. - let spec_const_entries = [vk::SpecializationMapEntry::default() - .constant_id(0x5007) - .offset(0) - .size(4)]; - let spec_const_data = - u32::to_le_bytes(self.sky_fs_spec_id_0x5007_sun_intensity_extra_spec_const_factor); - let specialization_info = vk::SpecializationInfo::default() - .map_entries(&spec_const_entries) - .data(&spec_const_data); - - self.cleanup_pipeline(); - let pipeline_layout = self.create_pipeline_layout(); - let viewport = vk::PipelineViewportStateCreateInfo::default() - .scissor_count(1) - .viewport_count(1); - - let vs_entry_point = "main_vs"; - let fs_entry_point = "main_fs"; - let vert_module = self.vert_module.as_ref().unwrap(); - let frag_module = self.frag_module.as_ref().unwrap(); - let vert_name = CString::new(vs_entry_point).unwrap(); - let frag_name = CString::new(fs_entry_point).unwrap(); - let desc = PipelineDescriptor::new(Box::new([ - vk::PipelineShaderStageCreateInfo { - module: *vert_module, - p_name: (*vert_name).as_ptr(), - stage: vk::ShaderStageFlags::VERTEX, - ..Default::default() - }, - vk::PipelineShaderStageCreateInfo { - s_type: vk::StructureType::PIPELINE_SHADER_STAGE_CREATE_INFO, - module: *frag_module, - p_name: (*frag_name).as_ptr(), - stage: vk::ShaderStageFlags::FRAGMENT, - p_specialization_info: &specialization_info, - ..Default::default() - }, - ])); - let desc_indirect_parts = desc.indirect_parts(); - let pipeline_info = vk::GraphicsPipelineCreateInfo::default() - .stages(&desc.shader_stages) - .vertex_input_state(&desc.vertex_input) - .input_assembly_state(&desc.input_assembly) - .rasterization_state(&desc.rasterization) - .multisample_state(&desc.multisample) - .depth_stencil_state(&desc.depth_stencil) - .color_blend_state(&desc_indirect_parts.color_blend) - .dynamic_state(&desc_indirect_parts.dynamic_state_info) - .viewport_state(&viewport) - .layout(pipeline_layout) - .render_pass(self.render_pass); - - let mut pipelines = unsafe { - self.base + let pipeline_layout = self + .base .device - .create_graphics_pipelines(pipeline_cache, &[pipeline_info], None) - .expect("Unable to create graphics pipeline") - }; - // A single `pipeline_info` results in a single pipeline. - assert_eq!(pipelines.len(), 1); - self.pipeline = pipelines.pop().map(|pipeline| Pipeline { - pipeline, - pipeline_layout, - }); + .create_pipeline_layout( + &vk::PipelineLayoutCreateInfo::default().push_constant_ranges(&[ + vk::PushConstantRange::default() + .offset(0) + .size(size_of::() as u32) + .stage_flags(vk::ShaderStageFlags::ALL), + ]), + None, + ) + .unwrap(); + + let mut pipelines = + self.base + .device + .create_graphics_pipelines(vk::PipelineCache::null(), &[vk::GraphicsPipelineCreateInfo::default() + .stages( + &[ + vk::PipelineShaderStageCreateInfo { + module: shader_module, + p_name: c"main_vs".as_ptr(), + stage: vk::ShaderStageFlags::VERTEX, + ..Default::default() + }, + vk::PipelineShaderStageCreateInfo { + module: shader_module, + p_name: c"main_fs".as_ptr(), + stage: vk::ShaderStageFlags::FRAGMENT, + // NOTE(eddyb) this acts like an integration test for specialization constants. + p_specialization_info: &vk::SpecializationInfo::default() + .map_entries(&[vk::SpecializationMapEntry::default() + .constant_id(0x5007) + .offset(0) + .size(4)]) + .data(&u32::to_le_bytes( + self.sky_fs_spec_id_0x5007_sun_intensity_extra_spec_const_factor, + )), + ..Default::default() + }, + ], + ) + .vertex_input_state(&vk::PipelineVertexInputStateCreateInfo::default()) + .input_assembly_state(&vk::PipelineInputAssemblyStateCreateInfo { + topology: vk::PrimitiveTopology::TRIANGLE_LIST, + ..Default::default() + }) + .rasterization_state(&vk::PipelineRasterizationStateCreateInfo { + front_face: vk::FrontFace::COUNTER_CLOCKWISE, + line_width: 1.0, + ..Default::default() + }) + .multisample_state(&vk::PipelineMultisampleStateCreateInfo { + rasterization_samples: vk::SampleCountFlags::TYPE_1, + ..Default::default() + }) + .depth_stencil_state(&vk::PipelineDepthStencilStateCreateInfo::default()) + .color_blend_state( + &vk::PipelineColorBlendStateCreateInfo::default() + .attachments( + &[vk::PipelineColorBlendAttachmentState { + blend_enable: 0, + src_color_blend_factor: vk::BlendFactor::SRC_COLOR, + dst_color_blend_factor: vk::BlendFactor::ONE_MINUS_DST_COLOR, + color_blend_op: vk::BlendOp::ADD, + src_alpha_blend_factor: vk::BlendFactor::ZERO, + dst_alpha_blend_factor: vk::BlendFactor::ZERO, + alpha_blend_op: vk::BlendOp::ADD, + color_write_mask: vk::ColorComponentFlags::RGBA, + }], + ), + ) + .dynamic_state( + &vk::PipelineDynamicStateCreateInfo::default() + .dynamic_states(&[vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR]), + ) + .viewport_state( + &vk::PipelineViewportStateCreateInfo::default() + .scissor_count(1) + .viewport_count(1), + ) + .layout(pipeline_layout) + .render_pass(self.render_pass)], None) + .expect("Unable to create graphics pipeline"); + + // A single `pipeline_info` results in a single pipeline. + assert_eq!(pipelines.len(), 1); + self.pipeline = pipelines.pop().map(|pipeline| Pipeline { + pipeline, + pipeline_layout, + }); + + // shader modules are allowed to be deleted after the pipeline has been created + self.base.device.destroy_shader_module(shader_module, None); + } } - pub fn cleanup_pipeline(&mut self) { + pub unsafe fn cleanup_pipeline(&mut self) { unsafe { self.base.device.device_wait_idle().unwrap(); if let Some(pipeline) = self.pipeline.take() { @@ -853,37 +844,6 @@ impl RenderCtx { } } - /// Update the vertex and fragment shader modules. Does not rebuild - /// pipelines that may be using the shader module, nor does it invalidate - /// them. - pub fn update_shader_modules(&mut self, vert_data: &[u32], frag_data: &[u32]) { - let shader_info = vk::ShaderModuleCreateInfo::default().code(vert_data); - let shader_module = unsafe { - self.base - .device - .create_shader_module(&shader_info, None) - .expect("Vertex shader module error") - }; - if let Some(old_module) = self.vert_module.replace(shader_module) { - unsafe { - self.base.device.destroy_shader_module(old_module, None); - } - } - - let shader_info = vk::ShaderModuleCreateInfo::default().code(frag_data); - let shader_module = unsafe { - self.base - .device - .create_shader_module(&shader_info, None) - .expect("Fragment shader module error") - }; - if let Some(old_module) = self.frag_module.replace(shader_module) { - unsafe { - self.base.device.destroy_shader_module(old_module, None); - } - } - } - /// Destroys the swapchain, as well as the renderpass and frame and command buffers pub fn cleanup_swapchain(&mut self) { unsafe { @@ -1130,12 +1090,6 @@ impl Drop for RenderCtx { self.base .device .destroy_command_pool(self.commands.pool, None); - self.base - .device - .destroy_shader_module(self.vert_module.unwrap(), None); - self.base - .device - .destroy_shader_module(self.frag_module.unwrap(), None); } } } @@ -1219,99 +1173,6 @@ pub struct Pipeline { pub pipeline_layout: vk::PipelineLayout, } -pub struct PipelineDescriptor<'a> { - pub color_blend_attachments: Box<[vk::PipelineColorBlendAttachmentState]>, - pub dynamic_state: Box<[vk::DynamicState]>, - pub shader_stages: Box<[vk::PipelineShaderStageCreateInfo<'a>]>, - pub vertex_input: vk::PipelineVertexInputStateCreateInfo<'static>, - pub input_assembly: vk::PipelineInputAssemblyStateCreateInfo<'static>, - pub rasterization: vk::PipelineRasterizationStateCreateInfo<'static>, - pub multisample: vk::PipelineMultisampleStateCreateInfo<'static>, - pub depth_stencil: vk::PipelineDepthStencilStateCreateInfo<'static>, -} - -// HACK(eddyb) these fields need to borrow from `PipelineDescriptor` itself. -pub struct PipelineDescriptorIndirectParts<'a> { - pub color_blend: vk::PipelineColorBlendStateCreateInfo<'a>, - pub dynamic_state_info: vk::PipelineDynamicStateCreateInfo<'a>, -} - -impl<'a> PipelineDescriptor<'a> { - fn new(shader_stages: Box<[vk::PipelineShaderStageCreateInfo<'a>]>) -> Self { - let vertex_input = vk::PipelineVertexInputStateCreateInfo { - vertex_attribute_description_count: 0, - vertex_binding_description_count: 0, - ..Default::default() - }; - let input_assembly = vk::PipelineInputAssemblyStateCreateInfo { - topology: vk::PrimitiveTopology::TRIANGLE_LIST, - ..Default::default() - }; - - let rasterization = vk::PipelineRasterizationStateCreateInfo { - front_face: vk::FrontFace::COUNTER_CLOCKWISE, - line_width: 1.0, - polygon_mode: vk::PolygonMode::FILL, - ..Default::default() - }; - let multisample = vk::PipelineMultisampleStateCreateInfo { - rasterization_samples: vk::SampleCountFlags::TYPE_1, - ..Default::default() - }; - let noop_stencil_state = vk::StencilOpState { - fail_op: vk::StencilOp::KEEP, - pass_op: vk::StencilOp::KEEP, - depth_fail_op: vk::StencilOp::KEEP, - compare_op: vk::CompareOp::ALWAYS, - ..Default::default() - }; - let depth_stencil = vk::PipelineDepthStencilStateCreateInfo { - depth_test_enable: 0, - depth_write_enable: 0, - depth_compare_op: vk::CompareOp::ALWAYS, - front: noop_stencil_state, - back: noop_stencil_state, - max_depth_bounds: 1.0, - ..Default::default() - }; - let color_blend_attachments = Box::new([vk::PipelineColorBlendAttachmentState { - blend_enable: 0, - src_color_blend_factor: vk::BlendFactor::SRC_COLOR, - dst_color_blend_factor: vk::BlendFactor::ONE_MINUS_DST_COLOR, - color_blend_op: vk::BlendOp::ADD, - src_alpha_blend_factor: vk::BlendFactor::ZERO, - dst_alpha_blend_factor: vk::BlendFactor::ZERO, - alpha_blend_op: vk::BlendOp::ADD, - color_write_mask: vk::ColorComponentFlags::R - | vk::ColorComponentFlags::G - | vk::ColorComponentFlags::B - | vk::ColorComponentFlags::A, - }]); - let dynamic_state = Box::new([vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR]); - - Self { - color_blend_attachments, - dynamic_state, - shader_stages, - vertex_input, - input_assembly, - rasterization, - multisample, - depth_stencil, - } - } - - fn indirect_parts(&self) -> PipelineDescriptorIndirectParts<'_> { - PipelineDescriptorIndirectParts { - color_blend: vk::PipelineColorBlendStateCreateInfo::default() - .logic_op(vk::LogicOp::CLEAR) - .attachments(&self.color_blend_attachments), - dynamic_state_info: vk::PipelineDynamicStateCreateInfo::default() - .dynamic_states(&self.dynamic_state), - } - } -} - unsafe fn any_as_u8_slice(p: &T) -> &[u8] { unsafe { ::std::slice::from_raw_parts((p as *const T).cast::(), ::std::mem::size_of::()) From ed00a3b3738d1f31d971fd5910fb6a784ccac09c Mon Sep 17 00:00:00 2001 From: firestar99 Date: Tue, 30 Sep 2025 10:53:52 +0200 Subject: [PATCH 04/12] ash runner: use bytemuck --- Cargo.lock | 1 + examples/runners/ash/Cargo.toml | 1 + examples/runners/ash/src/main.rs | 8 +------- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5e1f988318..fd855795b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -938,6 +938,7 @@ dependencies = [ "ash", "ash-molten", "ash-window", + "bytemuck", "cfg-if", "clap", "raw-window-handle 0.6.2", diff --git a/examples/runners/ash/Cargo.toml b/examples/runners/ash/Cargo.toml index 3818c3b6b3..0fa5fb6104 100644 --- a/examples/runners/ash/Cargo.toml +++ b/examples/runners/ash/Cargo.toml @@ -23,6 +23,7 @@ cfg-if = "1.0.0" shared = { path = "../../shaders/shared" } spirv-builder = { workspace = true, default-features = false } anyhow = "1.0.98" +bytemuck.workspace = true [target.'cfg(target_os = "macos")'.dependencies] ash-molten = { version = "0.20", features = ["pre-built"] } diff --git a/examples/runners/ash/src/main.rs b/examples/runners/ash/src/main.rs index 9aa3d3cc88..89c96cee6b 100644 --- a/examples/runners/ash/src/main.rs +++ b/examples/runners/ash/src/main.rs @@ -993,7 +993,7 @@ impl RenderCtx { pipeline.pipeline_layout, ash::vk::ShaderStageFlags::ALL, 0, - any_as_u8_slice(&push_constants), + bytemuck::bytes_of(&push_constants), ); device.cmd_draw(draw_command_buffer, 3, 1, 0, 0); @@ -1173,12 +1173,6 @@ pub struct Pipeline { pub pipeline_layout: vk::PipelineLayout, } -unsafe fn any_as_u8_slice(p: &T) -> &[u8] { - unsafe { - ::std::slice::from_raw_parts((p as *const T).cast::(), ::std::mem::size_of::()) - } -} - unsafe extern "system" fn vulkan_debug_callback( message_severity: vk::DebugUtilsMessageSeverityFlagsEXT, _message_type: vk::DebugUtilsMessageTypeFlagsEXT, From 12b87f747fb4ceba43af78bb1da9b466286b391f Mon Sep 17 00:00:00 2001 From: firestar99 Date: Tue, 30 Sep 2025 10:52:24 +0200 Subject: [PATCH 05/12] ash runner: general code cleanup --- examples/runners/ash/src/main.rs | 618 +++++++++++++++---------------- 1 file changed, 300 insertions(+), 318 deletions(-) diff --git a/examples/runners/ash/src/main.rs b/examples/runners/ash/src/main.rs index 89c96cee6b..4137caa47d 100644 --- a/examples/runners/ash/src/main.rs +++ b/examples/runners/ash/src/main.rs @@ -70,6 +70,7 @@ // crate-specific exceptions: // #![allow()] +use anyhow::{Context, anyhow}; use ash::{ext, khr, util::read_spv, vk}; use clap::{Parser, ValueEnum}; use raw_window_handle::{HasDisplayHandle as _, HasWindowHandle as _}; @@ -77,13 +78,13 @@ use shared::ShaderConstants; use spirv_builder::{MetadataPrintout, SpirvBuilder}; use std::{ borrow::Cow, - ffi::{CStr, CString}, + ffi::CStr, fs::File, - os::raw::c_char, path::PathBuf, sync::mpsc::{TryRecvError, TrySendError, sync_channel}, thread, }; +use winit::event_loop::ActiveEventLoop; use winit::{ event::{Event, WindowEvent}, event_loop::{ControlFlow, EventLoop}, @@ -118,7 +119,7 @@ pub struct Options { debug_layer: bool, } -pub fn main() { +pub fn main() -> anyhow::Result<()> { // Hack: spirv_builder builds into a custom directory if running under cargo, to not // deadlock, and the default target directory if not. However, packages like `proc-macro2` // have different configurations when being built here vs. when building @@ -148,26 +149,23 @@ pub fn main() { )), ) .unwrap(); - let mut ctx = RenderCtx::new(RenderBase::new(window, &options), shader_data); + let mut ctx = RenderCtx::new(RenderBase::new(window, &options)?, shader_data)?; ctx.rebuild_pipeline(); let (compiler_sender, compiler_receiver) = sync_channel::>(1); - // FIXME(eddyb) incomplete `winit` upgrade, follow the guides in: - // https://github.com/rust-windowing/winit/releases/tag/v0.30.0 - #[allow(deprecated)] - event_loop - .run(move |event, event_loop_window_target| match event { + let mut event_handler = + move |event: Event<_>, event_loop_window_target: &ActiveEventLoop| match event { Event::AboutToWait => { match compiler_receiver.try_recv() { Err(TryRecvError::Empty) => { if ctx.rendering_paused { let vk::Extent2D { width, height } = ctx.base.surface_resolution(); if height > 0 && width > 0 { - ctx.recreate_swapchain(); - ctx.render(); + ctx.recreate_swapchain()?; + ctx.render()?; } } else { - ctx.render(); + ctx.render()?; } } Ok(shader_data) => { @@ -176,58 +174,74 @@ pub fn main() { ctx.rebuild_pipeline(); } Err(TryRecvError::Disconnected) => { - panic!("compiler receiver disconnected unexpectedly"); + return Err(anyhow!("compiler receiver disconnected unexpectedly")); } }; + Ok(()) } - Event::WindowEvent { event, .. } => match event { - WindowEvent::KeyboardInput { - event: - winit::event::KeyEvent { - logical_key: winit::keyboard::Key::Named(key), - state: winit::event::ElementState::Pressed, - .. - }, - .. - } => match key { - winit::keyboard::NamedKey::Escape => event_loop_window_target.exit(), - winit::keyboard::NamedKey::F5 => { - if !ctx.recompiling_shaders { - ctx.recompiling_shaders = true; - let compiler_sender = compiler_sender.clone(); - thread::spawn(move || { - if let Err(TrySendError::Disconnected(_)) = compiler_sender - .try_send(compile_shaders(&options.shader).unwrap()) - { - panic!("compiler sender disconnected unexpectedly"); - }; - }); + Event::WindowEvent { event, .. } => { + match event { + WindowEvent::KeyboardInput { + event: + winit::event::KeyEvent { + logical_key: winit::keyboard::Key::Named(key), + state: winit::event::ElementState::Pressed, + .. + }, + .. + } => match key { + winit::keyboard::NamedKey::Escape => event_loop_window_target.exit(), + winit::keyboard::NamedKey::F5 => { + if !ctx.recompiling_shaders { + ctx.recompiling_shaders = true; + let compiler_sender = compiler_sender.clone(); + thread::spawn(move || { + if let Err(TrySendError::Disconnected(_)) = compiler_sender + .try_send(compile_shaders(&options.shader).unwrap()) + { + panic!("compiler sender disconnected unexpectedly"); + }; + }); + } } - } - winit::keyboard::NamedKey::ArrowUp | winit::keyboard::NamedKey::ArrowDown => { - let factor = - &mut ctx.sky_fs_spec_id_0x5007_sun_intensity_extra_spec_const_factor; - *factor = if key == winit::keyboard::NamedKey::ArrowUp { - factor.saturating_add(1) - } else { - factor.saturating_sub(1) - }; + winit::keyboard::NamedKey::ArrowUp + | winit::keyboard::NamedKey::ArrowDown => { + let factor = &mut ctx + .sky_fs_spec_id_0x5007_sun_intensity_extra_spec_const_factor; + *factor = if key == winit::keyboard::NamedKey::ArrowUp { + factor.saturating_add(1) + } else { + factor.saturating_sub(1) + }; - // HACK(eddyb) to see any changes, re-specializing the - // shader module is needed (e.g. during pipeline rebuild). - ctx.rebuild_pipeline(); + // HACK(eddyb) to see any changes, re-specializing the + // shader module is needed (e.g. during pipeline rebuild). + ctx.rebuild_pipeline(); + } + _ => {} + }, + WindowEvent::Resized(_) => { + ctx.recreate_swapchain()?; } + WindowEvent::CloseRequested => event_loop_window_target.exit(), _ => {} - }, - WindowEvent::Resized(_) => { - ctx.recreate_swapchain(); } - WindowEvent::CloseRequested => event_loop_window_target.exit(), - _ => {} - }, - _ => event_loop_window_target.set_control_flow(ControlFlow::Poll), - }) - .unwrap(); + + Ok(()) + } + _ => { + event_loop_window_target.set_control_flow(ControlFlow::Poll); + Ok(()) + } + }; + + // FIXME(eddyb) incomplete `winit` upgrade, follow the guides in: + // https://github.com/rust-windowing/winit/releases/tag/v0.30.0 + #[allow(deprecated)] + event_loop.run(move |event, event_loop_window_target| { + event_handler(event, event_loop_window_target).unwrap(); + })?; + Ok(()) } pub fn compile_shaders(shader: &RustGPUShader) -> anyhow::Result> { @@ -269,7 +283,7 @@ pub struct RenderBase { pub pdevice: vk::PhysicalDevice, pub queue_family_index: u32, - pub present_queue: vk::Queue, + pub main_queue: vk::Queue, pub surface: vk::SurfaceKHR, pub surface_loader: khr::surface::Instance, @@ -277,100 +291,88 @@ pub struct RenderBase { } impl RenderBase { - pub fn new(window: winit::window::Window, options: &Options) -> Self { - cfg_if::cfg_if! { - if #[cfg(target_os = "macos")] { - let entry = ash_molten::load(); - } else { - let entry = unsafe{ash::Entry::load()}.unwrap(); - } - } - - let instance: ash::Instance = { - let app_name = CString::new("VulkanTriangle").unwrap(); - - let layer_names = if options.debug_layer { - vec![CString::new("VK_LAYER_KHRONOS_validation").unwrap()] - } else { - vec![] - }; - let layers_names_raw: Vec<*const c_char> = layer_names - .iter() - .map(|raw_name| raw_name.as_ptr()) - .collect(); - - let mut extension_names_raw = - ash_window::enumerate_required_extensions(window.display_handle().unwrap().into()) - .unwrap() - .to_vec(); - if options.debug_layer { - extension_names_raw.push(ext::debug_utils::NAME.as_ptr()); + pub fn new(window: winit::window::Window, options: &Options) -> anyhow::Result { + unsafe { + cfg_if::cfg_if! { + if #[cfg(target_os = "macos")] { + let entry = ash_molten::load(); + } else { + let entry = ash::Entry::load()?; + } } - let appinfo = vk::ApplicationInfo::default() - .application_name(&app_name) - .application_version(0) - .engine_name(&app_name) - .engine_version(0) - .api_version(vk::make_api_version(0, 1, 2, 0)); + let instance: ash::Instance = { + let layer_names: &'static [_] = if options.debug_layer { + const { &[c"VK_LAYER_KHRONOS_validation".as_ptr()] } + } else { + &[] + }; - let instance_create_info = vk::InstanceCreateInfo::default() - .application_info(&appinfo) - .enabled_layer_names(&layers_names_raw) - .enabled_extension_names(&extension_names_raw); + let mut extension_names_raw = + ash_window::enumerate_required_extensions(window.display_handle()?.into())? + .to_vec(); + if options.debug_layer { + extension_names_raw.push(ext::debug_utils::NAME.as_ptr()); + } - unsafe { + let app_name = c"VulkanTriangle"; entry - .create_instance(&instance_create_info, None) - .expect("Instance creation error") - } - }; + .create_instance( + &vk::InstanceCreateInfo::default() + .application_info( + &vk::ApplicationInfo::default() + .application_name(app_name) + .application_version(0) + .engine_name(app_name) + .engine_version(0) + .api_version(vk::make_api_version(0, 1, 2, 0)), + ) + .enabled_layer_names(layer_names) + .enabled_extension_names(&extension_names_raw), + None, + ) + .context("create_instance")? + }; - let surface = unsafe { - ash_window::create_surface( + let surface = ash_window::create_surface( &entry, &instance, window.display_handle().unwrap().into(), window.window_handle().unwrap().into(), None, ) - .unwrap() - }; - - let (debug_utils_loader, debug_call_back) = if options.debug_layer { - let debug_utils_loader = ext::debug_utils::Instance::new(&entry, &instance); - let debug_call_back = { - let debug_info = vk::DebugUtilsMessengerCreateInfoEXT::default() - .message_severity( - vk::DebugUtilsMessageSeverityFlagsEXT::ERROR - | vk::DebugUtilsMessageSeverityFlagsEXT::WARNING - | vk::DebugUtilsMessageSeverityFlagsEXT::INFO, - ) - .message_type( - vk::DebugUtilsMessageTypeFlagsEXT::GENERAL - | vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION - | vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE, - ) - .pfn_user_callback(Some(vulkan_debug_callback)); + .context("create_surface")?; + + let (debug_utils_loader, debug_call_back) = if options.debug_layer { + let debug_utils_loader = ext::debug_utils::Instance::new(&entry, &instance); + let debug_call_back = { + debug_utils_loader.create_debug_utils_messenger( + &vk::DebugUtilsMessengerCreateInfoEXT::default() + .message_severity( + vk::DebugUtilsMessageSeverityFlagsEXT::ERROR + | vk::DebugUtilsMessageSeverityFlagsEXT::WARNING + | vk::DebugUtilsMessageSeverityFlagsEXT::INFO, + ) + .message_type( + vk::DebugUtilsMessageTypeFlagsEXT::GENERAL + | vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION + | vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE, + ) + .pfn_user_callback(Some(vulkan_debug_callback)), + None, + )? + }; - unsafe { - debug_utils_loader - .create_debug_utils_messenger(&debug_info, None) - .unwrap() - } + (Some(debug_utils_loader), Some(debug_call_back)) + } else { + (None, None) }; - (Some(debug_utils_loader), Some(debug_call_back)) - } else { - (None, None) - }; - - let surface_loader = khr::surface::Instance::new(&entry, &instance); + let surface_loader = khr::surface::Instance::new(&entry, &instance); - let (pdevice, queue_family_index) = unsafe { - instance + let (pdevice, queue_family_index) = instance .enumerate_physical_devices() - .expect("Physical device error") + .context("enumerate_physical_devices")? .iter() .find_map(|pdevice| { instance @@ -393,76 +395,63 @@ impl RenderBase { } }) }) - .expect("Couldn't find suitable device.") - }; - - let device: ash::Device = { - let device_extension_names_raw = [ - khr::swapchain::NAME.as_ptr(), - khr::shader_non_semantic_info::NAME.as_ptr(), - ]; - let features = vk::PhysicalDeviceFeatures { - shader_clip_distance: 1, - ..Default::default() - }; - let priorities = [1.0]; - let queue_info = [vk::DeviceQueueCreateInfo::default() - .queue_family_index(queue_family_index) - .queue_priorities(&priorities)]; - - let mut vulkan_memory_model_features = - vk::PhysicalDeviceVulkanMemoryModelFeatures::default().vulkan_memory_model(true); - - let device_create_info = vk::DeviceCreateInfo::default() - .push_next(&mut vulkan_memory_model_features) - .queue_create_infos(&queue_info) - .enabled_extension_names(&device_extension_names_raw) - .enabled_features(&features); - unsafe { - instance - .create_device(pdevice, &device_create_info, None) - .unwrap() - } - }; + .context("Couldn't find suitable device.")?; + + let device = instance + .create_device( + pdevice, + &vk::DeviceCreateInfo::default() + .push_next( + &mut vk::PhysicalDeviceVulkanMemoryModelFeatures::default() + .vulkan_memory_model(true), + ) + .queue_create_infos(&[vk::DeviceQueueCreateInfo::default() + .queue_family_index(queue_family_index) + .queue_priorities(&[1.0])]) + .enabled_extension_names(&[ + khr::swapchain::NAME.as_ptr(), + khr::shader_non_semantic_info::NAME.as_ptr(), + ]), + None, + ) + .context("create_device")?; - let swapchain_loader = khr::swapchain::Device::new(&instance, &device); + let swapchain_loader = khr::swapchain::Device::new(&instance, &device); - let present_queue = unsafe { device.get_device_queue(queue_family_index, 0) }; + let main_queue = device.get_device_queue(queue_family_index, 0); - let surface_format = { - let acceptable_formats = { - [ - vk::Format::R8G8B8_SRGB, - vk::Format::B8G8R8_SRGB, - vk::Format::R8G8B8A8_SRGB, - vk::Format::B8G8R8A8_SRGB, - vk::Format::A8B8G8R8_SRGB_PACK32, - ] - }; - unsafe { + let surface_format = { + let acceptable_formats = { + [ + vk::Format::R8G8B8_SRGB, + vk::Format::B8G8R8_SRGB, + vk::Format::R8G8B8A8_SRGB, + vk::Format::B8G8R8A8_SRGB, + vk::Format::A8B8G8R8_SRGB_PACK32, + ] + }; *surface_loader - .get_physical_device_surface_formats(pdevice, surface) - .unwrap() + .get_physical_device_surface_formats(pdevice, surface)? .iter() .find(|sfmt| acceptable_formats.contains(&sfmt.format)) - .expect("Unable to find suitable surface format.") - } - }; + .context("Unable to find suitable surface format.")? + }; - Self { - window, - entry, - instance, - device, - swapchain_loader, - debug_utils_loader, - debug_call_back, - pdevice, - queue_family_index, - present_queue, - surface, - surface_loader, - surface_format, + Ok(Self { + window, + entry, + instance, + device, + swapchain_loader, + debug_utils_loader, + debug_call_back, + pdevice, + queue_family_index, + main_queue, + surface, + surface_loader, + surface_format, + }) } } @@ -488,82 +477,86 @@ impl RenderBase { } } - pub fn create_swapchain(&self) -> (vk::SwapchainKHR, vk::Extent2D) { - let surface_capabilities = self.surface_capabilities(); - let mut desired_image_count = surface_capabilities.min_image_count + 1; - if surface_capabilities.max_image_count > 0 - && desired_image_count > surface_capabilities.max_image_count - { - desired_image_count = surface_capabilities.max_image_count; - } - let pre_transform = if surface_capabilities - .supported_transforms - .contains(vk::SurfaceTransformFlagsKHR::IDENTITY) - { - vk::SurfaceTransformFlagsKHR::IDENTITY - } else { - surface_capabilities.current_transform - }; - let present_mode = unsafe { - self.surface_loader - .get_physical_device_surface_present_modes(self.pdevice, self.surface) - .unwrap() + pub fn create_swapchain(&self) -> anyhow::Result<(vk::SwapchainKHR, vk::Extent2D)> { + unsafe { + let surface_capabilities = self.surface_capabilities(); + let mut desired_image_count = surface_capabilities.min_image_count + 1; + if surface_capabilities.max_image_count > 0 + && desired_image_count > surface_capabilities.max_image_count + { + desired_image_count = surface_capabilities.max_image_count; + } + let pre_transform = if surface_capabilities + .supported_transforms + .contains(vk::SurfaceTransformFlagsKHR::IDENTITY) + { + vk::SurfaceTransformFlagsKHR::IDENTITY + } else { + surface_capabilities.current_transform + }; + let present_mode = self + .surface_loader + .get_physical_device_surface_present_modes(self.pdevice, self.surface)? .iter() .cloned() .find(|&mode| mode == vk::PresentModeKHR::MAILBOX) - .unwrap_or(vk::PresentModeKHR::FIFO) - }; - let extent = self.surface_resolution(); - let swapchain_create_info = vk::SwapchainCreateInfoKHR::default() - .surface(self.surface) - .min_image_count(desired_image_count) - .image_color_space(self.surface_format.color_space) - .image_format(self.surface_format.format) - .image_extent(extent) - .image_usage(vk::ImageUsageFlags::COLOR_ATTACHMENT) - .image_sharing_mode(vk::SharingMode::EXCLUSIVE) - .pre_transform(pre_transform) - .composite_alpha(vk::CompositeAlphaFlagsKHR::OPAQUE) - .present_mode(present_mode) - .clipped(true) - .image_array_layers(1); - let swapchain = unsafe { - self.swapchain_loader - .create_swapchain(&swapchain_create_info, None) - .unwrap() - }; - (swapchain, extent) + .unwrap_or(vk::PresentModeKHR::FIFO); + let extent = self.surface_resolution(); + let swapchain = self + .swapchain_loader + .create_swapchain( + &vk::SwapchainCreateInfoKHR::default() + .surface(self.surface) + .min_image_count(desired_image_count) + .image_color_space(self.surface_format.color_space) + .image_format(self.surface_format.format) + .image_extent(extent) + .image_usage(vk::ImageUsageFlags::COLOR_ATTACHMENT) + .image_sharing_mode(vk::SharingMode::EXCLUSIVE) + .pre_transform(pre_transform) + .composite_alpha(vk::CompositeAlphaFlagsKHR::OPAQUE) + .present_mode(present_mode) + .clipped(true) + .image_array_layers(1), + None, + ) + .context("create_swapchain")?; + Ok((swapchain, extent)) + } } - pub fn create_image_views(&self, swapchain: vk::SwapchainKHR) -> Vec { + pub fn create_image_views( + &self, + swapchain: vk::SwapchainKHR, + ) -> anyhow::Result> { unsafe { - self.swapchain_loader - .get_swapchain_images(swapchain) - .unwrap() + Ok(self + .swapchain_loader + .get_swapchain_images(swapchain)? .iter() .map(|&image| { - let create_view_info = vk::ImageViewCreateInfo::default() - .view_type(vk::ImageViewType::TYPE_2D) - .format(self.surface_format.format) - .components(vk::ComponentMapping { - r: vk::ComponentSwizzle::R, - g: vk::ComponentSwizzle::G, - b: vk::ComponentSwizzle::B, - a: vk::ComponentSwizzle::A, - }) - .subresource_range(vk::ImageSubresourceRange { - aspect_mask: vk::ImageAspectFlags::COLOR, - base_mip_level: 0, - level_count: 1, - base_array_layer: 0, - layer_count: 1, - }) - .image(image); - self.device - .create_image_view(&create_view_info, None) - .unwrap() + self.device.create_image_view( + &vk::ImageViewCreateInfo::default() + .view_type(vk::ImageViewType::TYPE_2D) + .format(self.surface_format.format) + .components(vk::ComponentMapping { + r: vk::ComponentSwizzle::R, + g: vk::ComponentSwizzle::G, + b: vk::ComponentSwizzle::B, + a: vk::ComponentSwizzle::A, + }) + .subresource_range(vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + base_mip_level: 0, + level_count: 1, + base_array_layer: 0, + layer_count: 1, + }) + .image(image), + None, + ) }) - .collect() + .collect::, _>>()?) } } @@ -572,26 +565,24 @@ impl RenderBase { image_views: &[vk::ImageView], render_pass: vk::RenderPass, extent: vk::Extent2D, - ) -> Vec { - image_views + ) -> anyhow::Result> { + Ok(image_views .iter() .map(|&present_image_view| { let framebuffer_attachments = [present_image_view]; unsafe { - self.device - .create_framebuffer( - &vk::FramebufferCreateInfo::default() - .render_pass(render_pass) - .attachments(&framebuffer_attachments) - .width(extent.width) - .height(extent.height) - .layers(1), - None, - ) - .unwrap() + self.device.create_framebuffer( + &vk::FramebufferCreateInfo::default() + .render_pass(render_pass) + .attachments(&framebuffer_attachments) + .width(extent.width) + .height(extent.height) + .layers(1), + None, + ) } }) - .collect() + .collect::>()?) } pub fn create_render_pass(&self) -> vk::RenderPass { @@ -628,10 +619,6 @@ impl RenderBase { .unwrap() } } - - pub fn create_render_sync(&self) -> RenderSync { - RenderSync::new(self) - } } impl Drop for RenderBase { @@ -674,13 +661,13 @@ pub struct RenderCtx { } impl RenderCtx { - pub fn new(base: RenderBase, shader_code: Vec) -> Self { - let sync = RenderSync::new(&base); + pub fn new(base: RenderBase, shader_code: Vec) -> anyhow::Result { + let sync = RenderSync::new(&base)?; - let (swapchain, extent) = base.create_swapchain(); - let image_views = base.create_image_views(swapchain); + let (swapchain, extent) = base.create_swapchain()?; + let image_views = base.create_image_views(swapchain)?; let render_pass = base.create_render_pass(); - let framebuffers = base.create_framebuffers(&image_views, render_pass, extent); + let framebuffers = base.create_framebuffers(&image_views, render_pass, extent)?; let commands = RenderCommandPool::new(&base); let (viewports, scissors) = { ( @@ -699,7 +686,7 @@ impl RenderCtx { ) }; - Self { + Ok(Self { sync, base, swapchain, @@ -717,7 +704,7 @@ impl RenderCtx { start: std::time::Instant::now(), sky_fs_spec_id_0x5007_sun_intensity_extra_spec_const_factor: 100, - } + }) } /// Update shaders and rebuild the pipeline @@ -832,7 +819,7 @@ impl RenderCtx { } } - pub unsafe fn cleanup_pipeline(&mut self) { + pub fn cleanup_pipeline(&mut self) { unsafe { self.base.device.device_wait_idle().unwrap(); if let Some(pipeline) = self.pipeline.take() { @@ -864,25 +851,25 @@ impl RenderCtx { } /// Recreates the swapchain, but does not recreate the pipelines because they use dynamic state. - pub fn recreate_swapchain(&mut self) { + pub fn recreate_swapchain(&mut self) -> anyhow::Result<()> { let surface_resolution = self.base.surface_resolution(); if surface_resolution.width == 0 || surface_resolution.height == 0 { self.rendering_paused = true; - return; + return Ok(()); } else if self.rendering_paused { self.rendering_paused = false; }; self.cleanup_swapchain(); - let (swapchain, extent) = self.base.create_swapchain(); + let (swapchain, extent) = self.base.create_swapchain()?; self.swapchain = swapchain; self.extent = extent; - self.image_views = self.base.create_image_views(self.swapchain); + self.image_views = self.base.create_image_views(self.swapchain)?; self.framebuffers = self.base - .create_framebuffers(&self.image_views, self.render_pass, extent); + .create_framebuffers(&self.image_views, self.render_pass, extent)?; self.viewports = Box::new([vk::Viewport { x: 0.0, y: extent.height as f32, @@ -895,9 +882,11 @@ impl RenderCtx { offset: vk::Offset2D { x: 0, y: 0 }, extent, }]); + + Ok(()) } - pub fn render(&mut self) { + pub fn render(&mut self) -> anyhow::Result<()> { let present_index = unsafe { match self.base.swapchain_loader.acquire_next_image( self.swapchain, @@ -907,8 +896,8 @@ impl RenderCtx { ) { Ok((idx, _)) => idx, Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => { - self.recreate_swapchain(); - return; + self.recreate_swapchain()?; + return Ok(()); } Err(err) => panic!("failed to acquire next image: {err:?}"), } @@ -934,12 +923,12 @@ impl RenderCtx { match self .base .swapchain_loader - .queue_present(self.base.present_queue, &present_info) + .queue_present(self.base.main_queue, &present_info) { Err(vk::Result::ERROR_OUT_OF_DATE_KHR) | Ok(true) => self.recreate_swapchain(), - Ok(false) => {} - Err(err) => panic!("failed to present queue: {err:?}"), - }; + Ok(false) => Ok(()), + Err(err) => Err(anyhow!("failed to present queue: {err:?}")), + } } } @@ -1059,7 +1048,7 @@ impl RenderCtx { self.base .device .queue_submit( - self.base.present_queue, + self.base.main_queue, &[submit_info], self.sync.draw_commands_reuse_fence, ) @@ -1101,31 +1090,24 @@ pub struct RenderSync { } impl RenderSync { - pub fn new(base: &RenderBase) -> Self { + pub fn new(base: &RenderBase) -> anyhow::Result { let fence_create_info = vk::FenceCreateInfo::default().flags(vk::FenceCreateFlags::SIGNALED); let semaphore_create_info = vk::SemaphoreCreateInfo::default(); unsafe { - let draw_commands_reuse_fence = base - .device - .create_fence(&fence_create_info, None) - .expect("Create fence failed."); - let present_complete_semaphore = base - .device - .create_semaphore(&semaphore_create_info, None) - .unwrap(); - let rendering_complete_semaphore = base - .device - .create_semaphore(&semaphore_create_info, None) - .unwrap(); + let draw_commands_reuse_fence = base.device.create_fence(&fence_create_info, None)?; + let present_complete_semaphore = + base.device.create_semaphore(&semaphore_create_info, None)?; + let rendering_complete_semaphore = + base.device.create_semaphore(&semaphore_create_info, None)?; - Self { + Ok(Self { present_complete_semaphore, rendering_complete_semaphore, draw_commands_reuse_fence, - } + }) } } } From c0aebde435824d1d47e38c741ebe2befce52a7f3 Mon Sep 17 00:00:00 2001 From: firestar99 Date: Tue, 30 Sep 2025 16:06:42 +0200 Subject: [PATCH 06/12] ash runner: rewrite * use Vulkan 1.3 * use DynamicRendering feature replacing Framebuffers * extract `MyDevice` struct containing vk initialization * decouple rendering from swapchain management * redo swapchain sync, fixes validation errors, intentionally kept basic * make rendering lazily recreate its pipeline --- examples/runners/ash/src/main.rs | 1383 +++++++++++++++--------------- 1 file changed, 699 insertions(+), 684 deletions(-) diff --git a/examples/runners/ash/src/main.rs b/examples/runners/ash/src/main.rs index 4137caa47d..7f943ba8c5 100644 --- a/examples/runners/ash/src/main.rs +++ b/examples/runners/ash/src/main.rs @@ -76,12 +76,15 @@ use clap::{Parser, ValueEnum}; use raw_window_handle::{HasDisplayHandle as _, HasWindowHandle as _}; use shared::ShaderConstants; use spirv_builder::{MetadataPrintout, SpirvBuilder}; +use std::ffi::c_char; +use std::ops::Deref; +use std::sync::Arc; use std::{ borrow::Cow, ffi::CStr, fs::File, path::PathBuf, - sync::mpsc::{TryRecvError, TrySendError, sync_channel}, + sync::mpsc::{TryRecvError, sync_channel}, thread, }; use winit::event_loop::ActiveEventLoop; @@ -120,64 +123,70 @@ pub struct Options { } pub fn main() -> anyhow::Result<()> { - // Hack: spirv_builder builds into a custom directory if running under cargo, to not - // deadlock, and the default target directory if not. However, packages like `proc-macro2` - // have different configurations when being built here vs. when building - // rustc_codegen_spirv normally, so we *want* to build into a separate target directory, to - // not have to rebuild half the crate graph every time we run. So, pretend we're running - // under cargo by setting these environment variables. - unsafe { - std::env::set_var("OUT_DIR", env!("OUT_DIR")); - std::env::set_var("PROFILE", env!("PROFILE")); - } - let options = Options::parse(); - let shader_data = compile_shaders(&options.shader).unwrap(); + let shader_code = compile_shaders(&options.shader)?; // runtime setup - let event_loop = EventLoop::new().unwrap(); + let event_loop = EventLoop::new()?; // FIXME(eddyb) incomplete `winit` upgrade, follow the guides in: // https://github.com/rust-windowing/winit/releases/tag/v0.30.0 #[allow(deprecated)] - let window = event_loop - .create_window( - winit::window::Window::default_attributes() - .with_title("Rust GPU - ash") - .with_inner_size(winit::dpi::LogicalSize::new( - f64::from(1280), - f64::from(720), - )), - ) - .unwrap(); - let mut ctx = RenderCtx::new(RenderBase::new(window, &options)?, shader_data)?; - ctx.rebuild_pipeline(); + let window = event_loop.create_window( + winit::window::Window::default_attributes() + .with_title("Rust GPU - ash") + .with_inner_size(winit::dpi::LogicalSize::new( + f64::from(1280), + f64::from(720), + )), + )?; + + let extensions = ash_window::enumerate_required_extensions(window.display_handle()?.as_raw())?; + let device = MyDevice::new(extensions, &options)?; + let mut swapchain = MySwapchainManager::new(device.clone(), window)?; + let mut renderer = MyRenderer::new(MyRenderPipelineManager::new( + device.clone(), + swapchain.surface_format.format, + shader_code, + )?)?; + + let mut recompiling_shaders = false; + let start = std::time::Instant::now(); let (compiler_sender, compiler_receiver) = sync_channel::>(1); - let mut event_handler = move |event: Event<_>, event_loop_window_target: &ActiveEventLoop| match event { Event::AboutToWait => { match compiler_receiver.try_recv() { - Err(TryRecvError::Empty) => { - if ctx.rendering_paused { - let vk::Extent2D { width, height } = ctx.base.surface_resolution(); - if height > 0 && width > 0 { - ctx.recreate_swapchain()?; - ctx.render()?; - } - } else { - ctx.render()?; - } - } - Ok(shader_data) => { - ctx.recompiling_shaders = false; - ctx.shader_code = shader_data; - ctx.rebuild_pipeline(); + Err(TryRecvError::Empty) => (), + Ok(shader_code) => { + recompiling_shaders = false; + renderer.pipeline.set_shader_code(shader_code); } Err(TryRecvError::Disconnected) => { return Err(anyhow!("compiler receiver disconnected unexpectedly")); } }; - Ok(()) + + swapchain.render(|frame| { + let extent = frame.extent; + let push_constants = ShaderConstants { + width: extent.width, + height: extent.height, + time: start.elapsed().as_secs_f32(), + + // FIXME(eddyb) implement mouse support for the ash runner. + cursor_x: 0.0, + cursor_y: 0.0, + drag_start_x: 0.0, + drag_start_y: 0.0, + drag_end_x: 0.0, + drag_end_y: 0.0, + mouse_button_pressed: 0, + mouse_button_press_time: [f32::NEG_INFINITY; 3], + }; + + renderer.render_frame(frame, push_constants)?; + Ok(()) + }) } Event::WindowEvent { event, .. } => { match event { @@ -192,36 +201,31 @@ pub fn main() -> anyhow::Result<()> { } => match key { winit::keyboard::NamedKey::Escape => event_loop_window_target.exit(), winit::keyboard::NamedKey::F5 => { - if !ctx.recompiling_shaders { - ctx.recompiling_shaders = true; + if !recompiling_shaders { + recompiling_shaders = true; let compiler_sender = compiler_sender.clone(); thread::spawn(move || { - if let Err(TrySendError::Disconnected(_)) = compiler_sender - .try_send(compile_shaders(&options.shader).unwrap()) - { - panic!("compiler sender disconnected unexpectedly"); - }; + let shader_code = compile_shaders(&options.shader) + .context("Compiling shaders failed") + .unwrap(); + compiler_sender.try_send(shader_code).unwrap(); }); } } winit::keyboard::NamedKey::ArrowUp | winit::keyboard::NamedKey::ArrowDown => { - let factor = &mut ctx - .sky_fs_spec_id_0x5007_sun_intensity_extra_spec_const_factor; - *factor = if key == winit::keyboard::NamedKey::ArrowUp { + let mut factor = renderer.pipeline.get_sky_fs_sun_intensity_factor(); + factor = if key == winit::keyboard::NamedKey::ArrowUp { factor.saturating_add(1) } else { factor.saturating_sub(1) }; - - // HACK(eddyb) to see any changes, re-specializing the - // shader module is needed (e.g. during pipeline rebuild). - ctx.rebuild_pipeline(); + renderer.pipeline.set_sky_fs_sun_intensity_factor(factor); } _ => {} }, WindowEvent::Resized(_) => { - ctx.recreate_swapchain()?; + swapchain.should_recreate(); } WindowEvent::CloseRequested => event_loop_window_target.exit(), _ => {} @@ -251,7 +255,7 @@ pub fn compile_shaders(shader: &RustGPUShader) -> anyhow::Result> { .copied() .collect::(); - let compile_result = SpirvBuilder::new(crate_path, "spirv-unknown-vulkan1.1") + let compile_result = SpirvBuilder::new(crate_path, "spirv-unknown-vulkan1.3") .print_metadata(MetadataPrintout::None) .shader_panic_strategy(spirv_builder::ShaderPanicStrategy::DebugPrintfThenExit { print_inputs: true, @@ -269,29 +273,31 @@ pub fn compile_shaders(shader: &RustGPUShader) -> anyhow::Result> { Ok(read_spv(&mut File::open(spv_path)?)?) } -pub struct RenderBase { - pub window: winit::window::Window, - +/// Central struct containing the Vulkan instance and device, among others +pub struct MyDevice { pub entry: ash::Entry, - pub instance: ash::Instance, + pub physical_device: vk::PhysicalDevice, pub device: ash::Device, - pub swapchain_loader: khr::swapchain::Device, - - pub debug_utils_loader: Option, - pub debug_call_back: Option, - - pub pdevice: vk::PhysicalDevice, - pub queue_family_index: u32, + pub main_queue_family: u32, pub main_queue: vk::Queue, + pub debug_ext_instance: ext::debug_utils::Instance, + pub debug_ext_device: ext::debug_utils::Device, + pub surface_ext: khr::surface::Instance, + pub swapchain_ext: khr::swapchain::Device, + debug_callback: vk::DebugUtilsMessengerEXT, +} - pub surface: vk::SurfaceKHR, - pub surface_loader: khr::surface::Instance, - pub surface_format: vk::SurfaceFormatKHR, +impl Deref for MyDevice { + type Target = ash::Device; + + fn deref(&self) -> &Self::Target { + &self.device + } } -impl RenderBase { - pub fn new(window: winit::window::Window, options: &Options) -> anyhow::Result { +impl MyDevice { + pub fn new(extension_names: &[*const c_char], options: &Options) -> anyhow::Result> { unsafe { cfg_if::cfg_if! { if #[cfg(target_os = "macos")] { @@ -301,19 +307,15 @@ impl RenderBase { } } - let instance: ash::Instance = { + let instance = { let layer_names: &'static [_] = if options.debug_layer { const { &[c"VK_LAYER_KHRONOS_validation".as_ptr()] } } else { &[] }; - let mut extension_names_raw = - ash_window::enumerate_required_extensions(window.display_handle()?.into())? - .to_vec(); - if options.debug_layer { - extension_names_raw.push(ext::debug_utils::NAME.as_ptr()); - } + let mut extension_names_raw = extension_names.to_vec(); + extension_names_raw.push(ext::debug_utils::NAME.as_ptr()); let app_name = c"VulkanTriangle"; entry @@ -325,7 +327,7 @@ impl RenderBase { .application_version(0) .engine_name(app_name) .engine_version(0) - .api_version(vk::make_api_version(0, 1, 2, 0)), + .api_version(vk::make_api_version(0, 1, 3, 0)), ) .enabled_layer_names(layer_names) .enabled_extension_names(&extension_names_raw), @@ -334,79 +336,68 @@ impl RenderBase { .context("create_instance")? }; - let surface = ash_window::create_surface( - &entry, - &instance, - window.display_handle().unwrap().into(), - window.window_handle().unwrap().into(), - None, - ) - .context("create_surface")?; - - let (debug_utils_loader, debug_call_back) = if options.debug_layer { - let debug_utils_loader = ext::debug_utils::Instance::new(&entry, &instance); - let debug_call_back = { - debug_utils_loader.create_debug_utils_messenger( - &vk::DebugUtilsMessengerCreateInfoEXT::default() - .message_severity( - vk::DebugUtilsMessageSeverityFlagsEXT::ERROR - | vk::DebugUtilsMessageSeverityFlagsEXT::WARNING - | vk::DebugUtilsMessageSeverityFlagsEXT::INFO, - ) - .message_type( - vk::DebugUtilsMessageTypeFlagsEXT::GENERAL - | vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION - | vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE, - ) - .pfn_user_callback(Some(vulkan_debug_callback)), - None, - )? - }; - - (Some(debug_utils_loader), Some(debug_call_back)) - } else { - (None, None) + let debug_instance = ext::debug_utils::Instance::new(&entry, &instance); + let debug_callback = { + debug_instance.create_debug_utils_messenger( + &vk::DebugUtilsMessengerCreateInfoEXT::default() + .message_severity( + vk::DebugUtilsMessageSeverityFlagsEXT::ERROR + | vk::DebugUtilsMessageSeverityFlagsEXT::WARNING + | vk::DebugUtilsMessageSeverityFlagsEXT::INFO, + ) + .message_type( + vk::DebugUtilsMessageTypeFlagsEXT::GENERAL + | vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION + | vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE, + ) + .pfn_user_callback(Some(vulkan_debug_callback)), + None, + )? }; - let surface_loader = khr::surface::Instance::new(&entry, &instance); + let physical_device = { + instance + .enumerate_physical_devices()? + .into_iter() + .min_by_key(|phy| { + match instance.get_physical_device_properties(*phy).device_type { + vk::PhysicalDeviceType::DISCRETE_GPU => 1, + vk::PhysicalDeviceType::VIRTUAL_GPU => 2, + vk::PhysicalDeviceType::INTEGRATED_GPU => 3, + vk::PhysicalDeviceType::CPU => 4, + _ => 5, + } + }) + .ok_or(anyhow!("No physical devices available"))? + }; - let (pdevice, queue_family_index) = instance - .enumerate_physical_devices() - .context("enumerate_physical_devices")? - .iter() - .find_map(|pdevice| { - instance - .get_physical_device_queue_family_properties(*pdevice) - .iter() - .enumerate() - .find_map(|(index, info)| { - if info.queue_flags.contains(vk::QueueFlags::GRAPHICS) - && surface_loader - .get_physical_device_surface_support( - *pdevice, - index as u32, - surface, - ) - .unwrap() - { - Some((*pdevice, index as u32)) - } else { - None - } - }) - }) - .context("Couldn't find suitable device.")?; + let main_queue_family = { + instance + .get_physical_device_queue_family_properties(physical_device) + .into_iter() + .enumerate() + .find(|(_, prop)| prop.queue_flags.contains(vk::QueueFlags::GRAPHICS)) + .ok_or(anyhow!( + "No graphics + compute queues on physical device available" + ))? + .0 as u32 + }; let device = instance .create_device( - pdevice, + physical_device, &vk::DeviceCreateInfo::default() .push_next( &mut vk::PhysicalDeviceVulkanMemoryModelFeatures::default() .vulkan_memory_model(true), ) + .push_next( + &mut vk::PhysicalDeviceVulkan13Features::default() + .synchronization2(true) + .dynamic_rendering(true), + ) .queue_create_infos(&[vk::DeviceQueueCreateInfo::default() - .queue_family_index(queue_family_index) + .queue_family_index(main_queue_family) .queue_priorities(&[1.0])]) .enabled_extension_names(&[ khr::swapchain::NAME.as_ptr(), @@ -415,10 +406,105 @@ impl RenderBase { None, ) .context("create_device")?; + let main_queue = device.get_device_queue(main_queue_family, 0); + + Ok(Arc::new(Self { + debug_ext_device: ext::debug_utils::Device::new(&instance, &device), + surface_ext: khr::surface::Instance::new(&entry, &instance), + swapchain_ext: khr::swapchain::Device::new(&instance, &device), + entry, + instance, + physical_device, + device, + main_queue_family, + main_queue, + debug_ext_instance: debug_instance, + debug_callback, + })) + } + } +} + +impl Drop for MyDevice { + fn drop(&mut self) { + unsafe { + self.debug_ext_instance + .destroy_debug_utils_messenger(self.debug_callback, None); + self.device.destroy_device(None); + self.instance.destroy_instance(None); + } + } +} + +/// A binary semaphore for swapchain operations +pub struct SwapchainSync { + acquire_semaphore: vk::Semaphore, + render_semaphore: vk::Semaphore, + render_fence: vk::Fence, +} + +impl SwapchainSync { + pub unsafe fn new(device: &MyDevice) -> anyhow::Result { + unsafe { + let signaled_fence = + vk::FenceCreateInfo::default().flags(vk::FenceCreateFlags::SIGNALED); + Ok(Self { + acquire_semaphore: device + .create_semaphore(&vk::SemaphoreCreateInfo::default(), None)?, + render_semaphore: device + .create_semaphore(&vk::SemaphoreCreateInfo::default(), None)?, + render_fence: device.create_fence(&signaled_fence, None)?, + }) + } + } + + pub unsafe fn destroy(&self, device: &MyDevice) { + unsafe { + device.destroy_semaphore(self.acquire_semaphore, None); + device.destroy_semaphore(self.render_semaphore, None); + device.destroy_fence(self.render_fence, None); + } + } +} + +/// Takes care of all things swapchain related +/// +/// Intentionally kept simple and does not offer support for multiple frames in flight +pub struct MySwapchainManager { + pub device: Arc, + pub window: winit::window::Window, + pub surface: vk::SurfaceKHR, + pub surface_format: vk::SurfaceFormatKHR, + pub surface_capabilities: vk::SurfaceCapabilitiesKHR, + pub present_mode: vk::PresentModeKHR, + pub image_count: u32, + pub pre_transform: vk::SurfaceTransformFlagsKHR, + + // state below + active: Option, + should_recreate: bool, + sync: SwapchainSync, +} - let swapchain_loader = khr::swapchain::Device::new(&instance, &device); +struct ActiveSwapchain { + extent: vk::Extent2D, + swapchain: vk::SwapchainKHR, + images: Vec<(vk::Image, vk::ImageView)>, +} - let main_queue = device.get_device_queue(queue_family_index, 0); +impl MySwapchainManager { + pub fn new(device: Arc, window: winit::window::Window) -> anyhow::Result { + unsafe { + let surface_ext = &device.surface_ext; + + let surface = ash_window::create_surface( + &device.entry, + &device.instance, + window.display_handle().unwrap().into(), + window.window_handle().unwrap().into(), + None, + ) + .context("create_surface")?; let surface_format = { let acceptable_formats = { @@ -430,313 +516,355 @@ impl RenderBase { vk::Format::A8B8G8R8_SRGB_PACK32, ] }; - *surface_loader - .get_physical_device_surface_formats(pdevice, surface)? + *surface_ext + .get_physical_device_surface_formats(device.physical_device, surface)? .iter() .find(|sfmt| acceptable_formats.contains(&sfmt.format)) .context("Unable to find suitable surface format.")? }; + let surface_capabilities = surface_ext + .get_physical_device_surface_capabilities(device.physical_device, surface)?; + let pre_transform = surface_capabilities.current_transform; + + let present_mode = surface_ext + .get_physical_device_surface_present_modes(device.physical_device, surface)? + .iter() + .cloned() + // Mailbox is preferred + .find(|&mode| mode == vk::PresentModeKHR::MAILBOX) + // FIFO is guaranteed to be available + .unwrap_or(vk::PresentModeKHR::FIFO); + + let image_count = { + let mut image_count = match present_mode { + // tripple buffering in mailbox mode:: one presenting, one ready and one drawing + vk::PresentModeKHR::MAILBOX => 3, + // double buffering in fifo mode: one presenting, one drawing + vk::PresentModeKHR::FIFO => 2, + _ => unreachable!(), + }; + if surface_capabilities.max_image_count != 0 { + image_count = image_count.min(surface_capabilities.max_image_count); + } + image_count.max(surface_capabilities.min_image_count) + }; + + let sync = SwapchainSync::new(&device)?; Ok(Self { - window, - entry, - instance, device, - swapchain_loader, - debug_utils_loader, - debug_call_back, - pdevice, - queue_family_index, - main_queue, + window, surface, - surface_loader, surface_format, + surface_capabilities, + present_mode, + image_count, + pre_transform, + + active: None, + should_recreate: true, + sync, }) } } - pub fn surface_resolution(&self) -> vk::Extent2D { - let surface_capabilities = self.surface_capabilities(); - match surface_capabilities.current_extent.width { - std::u32::MAX => { - let window_inner = self.window.inner_size(); - vk::Extent2D { - width: window_inner.width, - height: window_inner.height, - } - } - _ => surface_capabilities.current_extent, - } + #[inline] + fn should_recreate(&mut self) { + self.should_recreate = true; } - pub fn surface_capabilities(&self) -> vk::SurfaceCapabilitiesKHR { + /// After this function is called, `Self.active` is initialized + unsafe fn recreate_swapchain(&mut self) -> anyhow::Result<()> { unsafe { - self.surface_loader - .get_physical_device_surface_capabilities(self.pdevice, self.surface) - .unwrap() - } - } + let device = &self.device; + let swapchain_ext = &device.swapchain_ext; + let surface_ext = &self.device.surface_ext; + let format = self.surface_format.format; + + let extent = { + let window_size = self.window.inner_size(); + let capabilities = surface_ext.get_physical_device_surface_capabilities( + self.device.physical_device, + self.surface, + )?; + let min = capabilities.min_image_extent; + let max = capabilities.max_image_extent; + vk::Extent2D { + width: u32::clamp(window_size.width, min.width, max.width), + height: u32::clamp(window_size.height, min.height, max.height), + } + }; - pub fn create_swapchain(&self) -> anyhow::Result<(vk::SwapchainKHR, vk::Extent2D)> { - unsafe { - let surface_capabilities = self.surface_capabilities(); - let mut desired_image_count = surface_capabilities.min_image_count + 1; - if surface_capabilities.max_image_count > 0 - && desired_image_count > surface_capabilities.max_image_count - { - desired_image_count = surface_capabilities.max_image_count; + let old = self.active.take(); + if let Some(old) = old.as_ref() { + old.destroy_image_views(device); } - let pre_transform = if surface_capabilities - .supported_transforms - .contains(vk::SurfaceTransformFlagsKHR::IDENTITY) - { - vk::SurfaceTransformFlagsKHR::IDENTITY - } else { - surface_capabilities.current_transform - }; - let present_mode = self - .surface_loader - .get_physical_device_surface_present_modes(self.pdevice, self.surface)? - .iter() - .cloned() - .find(|&mode| mode == vk::PresentModeKHR::MAILBOX) - .unwrap_or(vk::PresentModeKHR::FIFO); - let extent = self.surface_resolution(); - let swapchain = self - .swapchain_loader + + let swapchain = swapchain_ext .create_swapchain( &vk::SwapchainCreateInfoKHR::default() .surface(self.surface) - .min_image_count(desired_image_count) + .min_image_count(self.image_count) .image_color_space(self.surface_format.color_space) - .image_format(self.surface_format.format) + .image_format(format) .image_extent(extent) .image_usage(vk::ImageUsageFlags::COLOR_ATTACHMENT) .image_sharing_mode(vk::SharingMode::EXCLUSIVE) - .pre_transform(pre_transform) + .pre_transform(self.pre_transform) .composite_alpha(vk::CompositeAlphaFlagsKHR::OPAQUE) - .present_mode(present_mode) + .present_mode(self.present_mode) .clipped(true) - .image_array_layers(1), + .image_array_layers(1) + .old_swapchain( + old.as_ref() + .map_or(vk::SwapchainKHR::null(), |old| old.swapchain), + ), None, ) .context("create_swapchain")?; - Ok((swapchain, extent)) - } - } - pub fn create_image_views( - &self, - swapchain: vk::SwapchainKHR, - ) -> anyhow::Result> { - unsafe { - Ok(self - .swapchain_loader - .get_swapchain_images(swapchain)? - .iter() - .map(|&image| { - self.device.create_image_view( + if let Some(old) = old.as_ref() { + old.destroy_swapchain(device); + } + + let images = device.swapchain_ext.get_swapchain_images(swapchain)?; + let images = images + .into_iter() + .map(|image| { + let image_view = device.create_image_view( &vk::ImageViewCreateInfo::default() + .image(image) .view_type(vk::ImageViewType::TYPE_2D) - .format(self.surface_format.format) - .components(vk::ComponentMapping { - r: vk::ComponentSwizzle::R, - g: vk::ComponentSwizzle::G, - b: vk::ComponentSwizzle::B, - a: vk::ComponentSwizzle::A, - }) + .format(format) + .components(vk::ComponentMapping::default()) // identity .subresource_range(vk::ImageSubresourceRange { aspect_mask: vk::ImageAspectFlags::COLOR, base_mip_level: 0, level_count: 1, base_array_layer: 0, layer_count: 1, - }) - .image(image), + }), None, - ) + )?; + Ok::<_, anyhow::Error>((image, image_view)) }) - .collect::, _>>()?) - } - } + .collect::, _>>()?; - pub fn create_framebuffers( - &self, - image_views: &[vk::ImageView], - render_pass: vk::RenderPass, - extent: vk::Extent2D, - ) -> anyhow::Result> { - Ok(image_views - .iter() - .map(|&present_image_view| { - let framebuffer_attachments = [present_image_view]; - unsafe { - self.device.create_framebuffer( - &vk::FramebufferCreateInfo::default() - .render_pass(render_pass) - .attachments(&framebuffer_attachments) - .width(extent.width) - .height(extent.height) - .layers(1), - None, - ) - } - }) - .collect::>()?) + self.active = Some(ActiveSwapchain { + swapchain, + images, + extent, + }); + Ok(()) + } } +} - pub fn create_render_pass(&self) -> vk::RenderPass { - let renderpass_attachments = [vk::AttachmentDescription { - format: self.surface_format.format, - samples: vk::SampleCountFlags::TYPE_1, - load_op: vk::AttachmentLoadOp::CLEAR, - store_op: vk::AttachmentStoreOp::STORE, - final_layout: vk::ImageLayout::PRESENT_SRC_KHR, - ..Default::default() - }]; - let color_attachment_refs = [vk::AttachmentReference { - attachment: 0, - layout: vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL, - }]; - let dependencies = [vk::SubpassDependency { - src_subpass: vk::SUBPASS_EXTERNAL, - src_stage_mask: vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT, - dst_access_mask: vk::AccessFlags::COLOR_ATTACHMENT_READ - | vk::AccessFlags::COLOR_ATTACHMENT_WRITE, - dst_stage_mask: vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT, - ..Default::default() - }]; - let subpasses = [vk::SubpassDescription::default() - .color_attachments(&color_attachment_refs) - .pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS)]; - let renderpass_create_info = vk::RenderPassCreateInfo::default() - .attachments(&renderpass_attachments) - .subpasses(&subpasses) - .dependencies(&dependencies); +impl ActiveSwapchain { + /// We must destroy the image views we own, but not the images, those are owned by the swapchain. + unsafe fn destroy_image_views(&self, device: &MyDevice) { unsafe { - self.device - .create_render_pass(&renderpass_create_info, None) - .unwrap() + for (_, image_view) in &self.images { + device.destroy_image_view(*image_view, None); + } } } + + /// Destroying the swapchain destroys* the images, so image views must be destroyed beforehand. + unsafe fn destroy_swapchain(&self, device: &MyDevice) { + unsafe { device.swapchain_ext.destroy_swapchain(self.swapchain, None) } + } } -impl Drop for RenderBase { +impl Drop for MySwapchainManager { fn drop(&mut self) { unsafe { - self.device.destroy_device(None); - self.surface_loader.destroy_surface(self.surface, None); - if let Some((debug_utils, call_back)) = - Option::zip(self.debug_utils_loader.take(), self.debug_call_back.take()) - { - debug_utils.destroy_debug_utils_messenger(call_back, None); + self.sync.destroy(&self.device); + if let Some(active) = self.active.as_ref() { + active.destroy_image_views(&self.device); + active.destroy_swapchain(&self.device); } - self.instance.destroy_instance(None); + self.device.surface_ext.destroy_surface(self.surface, None); } } } -pub struct RenderCtx { - pub base: RenderBase, - pub sync: RenderSync, - - pub swapchain: vk::SwapchainKHR, +/// Metadata on drawing a single frame on some image +pub struct DrawFrame { + /// the size of the image pub extent: vk::Extent2D, - pub image_views: Vec, - pub render_pass: vk::RenderPass, - pub framebuffers: Vec, - pub commands: RenderCommandPool, - pub viewports: Box<[vk::Viewport]>, - pub scissors: Box<[vk::Rect2D]>, - pub shader_code: Vec, - pub pipeline: Option, - - pub rendering_paused: bool, - pub recompiling_shaders: bool, - pub start: std::time::Instant, + /// the [`vk::Image`] to draw to + pub image: vk::Image, + /// the [`vk::ImageImage`] to draw to, created from `image` + pub image_view: vk::ImageView, + /// the `acquire_image` semaphore that must be waited for before draw commands are executed + pub acquire_semaphore: vk::Semaphore, + /// the `draw_finished` semaphore that must be signaled when drawing to the image has finished + pub draw_finished_semaphore: vk::Semaphore, + /// the `draw_finished` fence that must be signaled when drawing to the image has finished + pub draw_finished_fence: vk::Fence, +} + +impl MySwapchainManager { + pub fn render( + &mut self, + f: impl FnOnce(DrawFrame) -> anyhow::Result<()>, + ) -> anyhow::Result<()> { + unsafe { + self.device + .wait_for_fences(&[self.sync.render_fence], true, !0)?; + self.device.reset_fences(&[self.sync.render_fence])?; + + const RECREATE_ATTEMPTS: u32 = 10; + for _ in 0..RECREATE_ATTEMPTS { + if self.should_recreate { + self.should_recreate = false; + + // *In theory*, recreating the swapchain allows you to present any acquired images from the old + // swapchain. Which (iirc) requires you to wait for all images to be presented before destroying + // the old swapchain. But we just use the [`ash::Device::device_wait_idle`] "hack" to wait for all + // previous images to finish before immediately destroying the swapchain. + self.device.device_wait_idle()?; + self.recreate_swapchain()?; + } + + let active = self.active.as_ref().unwrap(); + let swapchain_ext = &self.device.swapchain_ext; + match swapchain_ext.acquire_next_image( + active.swapchain, + !0, + self.sync.acquire_semaphore, + vk::Fence::null(), + ) { + Ok((id, suboptimal)) => { + if suboptimal { + self.should_recreate = true; + } + let (image, image_view) = active.images[id as usize]; + f(DrawFrame { + extent: active.extent, + image, + image_view, + acquire_semaphore: self.sync.acquire_semaphore, + draw_finished_semaphore: self.sync.render_semaphore, + draw_finished_fence: self.sync.render_fence, + })?; + + let suboptimal = swapchain_ext.queue_present( + self.device.main_queue, + &vk::PresentInfoKHR::default() + .swapchains(&[active.swapchain]) + .image_indices(&[id]) + .wait_semaphores(&[self.sync.render_semaphore]), + )?; + if suboptimal { + self.should_recreate = true; + } + return Ok(()); + } + Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => { + // retry + self.should_recreate = true; + } + Err(e) => { + return Err(e.into()); + } + } + } + panic!( + "looped {} times trying to acquire swapchain image and failed repeatedly!", + RECREATE_ATTEMPTS + ); + } + } +} + +/// Manages the creation and recreation of [`MyRenderPipeline`], whenever new shader code ([`Self::set_shader_code`]) +/// is submitted or the spec constant is changed ([`Self::set_sky_fs_sun_intensity_factor`]) +pub struct MyRenderPipelineManager { + pub device: Arc, + color_out_format: vk::Format, + shader_code: Vec, + pipeline: Option, // Only used for sky-shader. // NOTE(eddyb) this acts like an integration test for specialization constants. - pub sky_fs_spec_id_0x5007_sun_intensity_extra_spec_const_factor: u32, + sky_fs_spec_id_0x5007_sun_intensity_extra_spec_const_factor: u32, + should_recreate: bool, } -impl RenderCtx { - pub fn new(base: RenderBase, shader_code: Vec) -> anyhow::Result { - let sync = RenderSync::new(&base)?; - - let (swapchain, extent) = base.create_swapchain()?; - let image_views = base.create_image_views(swapchain)?; - let render_pass = base.create_render_pass(); - let framebuffers = base.create_framebuffers(&image_views, render_pass, extent)?; - let commands = RenderCommandPool::new(&base); - let (viewports, scissors) = { - ( - Box::new([vk::Viewport { - x: 0.0, - y: extent.height as f32, - width: extent.width as f32, - height: -(extent.height as f32), - min_depth: 0.0, - max_depth: 1.0, - }]), - Box::new([vk::Rect2D { - offset: vk::Offset2D { x: 0, y: 0 }, - extent, - }]), - ) - }; +pub struct MyRenderPipeline { + pub pipeline: vk::Pipeline, + pub pipeline_layout: vk::PipelineLayout, +} +impl MyRenderPipelineManager { + pub fn new( + device: Arc, + color_out_format: vk::Format, + shader_code: Vec, + ) -> anyhow::Result { Ok(Self { - sync, - base, - swapchain, - extent, - image_views, - commands, - render_pass, - framebuffers, - viewports, - scissors, + device, + color_out_format, shader_code, pipeline: None, - rendering_paused: false, - recompiling_shaders: false, - start: std::time::Instant::now(), - sky_fs_spec_id_0x5007_sun_intensity_extra_spec_const_factor: 100, + should_recreate: true, }) } + #[inline] + pub fn set_sky_fs_sun_intensity_factor(&mut self, factor: u32) { + self.sky_fs_spec_id_0x5007_sun_intensity_extra_spec_const_factor = factor; + self.should_recreate(); + } + + #[inline] + pub fn get_sky_fs_sun_intensity_factor(&self) -> u32 { + self.sky_fs_spec_id_0x5007_sun_intensity_extra_spec_const_factor + } + + #[inline] + pub fn set_shader_code(&mut self, shader_code: Vec) { + self.shader_code = shader_code; + self.should_recreate(); + } + + #[inline] + pub fn should_recreate(&mut self) { + self.should_recreate = true; + } + + pub fn get_pipeline(&mut self) -> anyhow::Result<&MyRenderPipeline> { + if self.should_recreate { + self.rebuild_pipeline()?; + } + Ok(self.pipeline.as_ref().unwrap()) + } + /// Update shaders and rebuild the pipeline - pub fn rebuild_pipeline(&mut self) { + fn rebuild_pipeline(&mut self) -> anyhow::Result<()> { unsafe { - self.cleanup_pipeline(); + self.destroy_pipeline()?; - let shader_module = self - .base - .device - .create_shader_module( - &vk::ShaderModuleCreateInfo::default().code(&self.shader_code), - None, - ) - .unwrap(); - - let pipeline_layout = self - .base - .device - .create_pipeline_layout( - &vk::PipelineLayoutCreateInfo::default().push_constant_ranges(&[ - vk::PushConstantRange::default() - .offset(0) - .size(size_of::() as u32) - .stage_flags(vk::ShaderStageFlags::ALL), - ]), - None, - ) - .unwrap(); + let shader_module = self.device.create_shader_module( + &vk::ShaderModuleCreateInfo::default().code(&self.shader_code), + None, + )?; + + let pipeline_layout = self.device.create_pipeline_layout( + &vk::PipelineLayoutCreateInfo::default().push_constant_ranges(&[ + vk::PushConstantRange::default() + .offset(0) + .size(size_of::() as u32) + .stage_flags(vk::ShaderStageFlags::ALL), + ]), + None, + )?; let mut pipelines = - self.base + self .device .create_graphics_pipelines(vk::PipelineCache::null(), &[vk::GraphicsPipelineCreateInfo::default() .stages( @@ -775,9 +903,9 @@ impl RenderCtx { ..Default::default() }) .multisample_state(&vk::PipelineMultisampleStateCreateInfo { - rasterization_samples: vk::SampleCountFlags::TYPE_1, - ..Default::default() - }) + rasterization_samples: vk::SampleCountFlags::TYPE_1, + ..Default::default() + }) .depth_stencil_state(&vk::PipelineDepthStencilStateCreateInfo::default()) .color_blend_state( &vk::PipelineColorBlendStateCreateInfo::default() @@ -804,355 +932,242 @@ impl RenderCtx { .viewport_count(1), ) .layout(pipeline_layout) - .render_pass(self.render_pass)], None) - .expect("Unable to create graphics pipeline"); + .push_next(&mut vk::PipelineRenderingCreateInfo::default().color_attachment_formats(&[self.color_out_format])) + ], None).map_err(|(_, e)| e) + .context("Unable to create graphics pipeline")?; // A single `pipeline_info` results in a single pipeline. assert_eq!(pipelines.len(), 1); - self.pipeline = pipelines.pop().map(|pipeline| Pipeline { + self.pipeline = pipelines.pop().map(|pipeline| MyRenderPipeline { pipeline, pipeline_layout, }); // shader modules are allowed to be deleted after the pipeline has been created - self.base.device.destroy_shader_module(shader_module, None); + self.device.destroy_shader_module(shader_module, None); + Ok(()) } } - pub fn cleanup_pipeline(&mut self) { + unsafe fn destroy_pipeline(&mut self) -> anyhow::Result<()> { unsafe { - self.base.device.device_wait_idle().unwrap(); if let Some(pipeline) = self.pipeline.take() { - self.base.device.destroy_pipeline(pipeline.pipeline, None); - self.base - .device - .destroy_pipeline_layout(pipeline.pipeline_layout, None); - } - } - } - - /// Destroys the swapchain, as well as the renderpass and frame and command buffers - pub fn cleanup_swapchain(&mut self) { - unsafe { - self.base.device.device_wait_idle().unwrap(); - // framebuffers - for framebuffer in self.framebuffers.drain(..) { - self.base.device.destroy_framebuffer(framebuffer, None); - } - // image views - for image_view in self.image_views.drain(..) { - self.base.device.destroy_image_view(image_view, None); - } - // swapchain - self.base - .swapchain_loader - .destroy_swapchain(self.swapchain, None); - } - } - - /// Recreates the swapchain, but does not recreate the pipelines because they use dynamic state. - pub fn recreate_swapchain(&mut self) -> anyhow::Result<()> { - let surface_resolution = self.base.surface_resolution(); - - if surface_resolution.width == 0 || surface_resolution.height == 0 { - self.rendering_paused = true; - return Ok(()); - } else if self.rendering_paused { - self.rendering_paused = false; - }; - - self.cleanup_swapchain(); - - let (swapchain, extent) = self.base.create_swapchain()?; - self.swapchain = swapchain; - self.extent = extent; - self.image_views = self.base.create_image_views(self.swapchain)?; - self.framebuffers = - self.base - .create_framebuffers(&self.image_views, self.render_pass, extent)?; - self.viewports = Box::new([vk::Viewport { - x: 0.0, - y: extent.height as f32, - width: extent.width as f32, - height: -(extent.height as f32), - min_depth: 0.0, - max_depth: 1.0, - }]); - self.scissors = Box::new([vk::Rect2D { - offset: vk::Offset2D { x: 0, y: 0 }, - extent, - }]); - - Ok(()) - } - - pub fn render(&mut self) -> anyhow::Result<()> { - let present_index = unsafe { - match self.base.swapchain_loader.acquire_next_image( - self.swapchain, - u64::MAX, - self.sync.present_complete_semaphore, - vk::Fence::null(), - ) { - Ok((idx, _)) => idx, - Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => { - self.recreate_swapchain()?; - return Ok(()); - } - Err(err) => panic!("failed to acquire next image: {err:?}"), - } - }; + // Figuring out when the pipeline stops being used is hard, so we take this shortcut + self.device.device_wait_idle()?; - let framebuffer = self.framebuffers[present_index as usize]; - let clear_values = [vk::ClearValue { - color: vk::ClearColorValue { - float32: [0.0, 1.0, 0.0, 0.0], - }, - }]; - - self.draw(self.pipeline.as_ref().unwrap(), framebuffer, &clear_values); - - let wait_semaphors = [self.sync.rendering_complete_semaphore]; - let swapchains = [self.swapchain]; - let image_indices = [present_index]; - let present_info = vk::PresentInfoKHR::default() - .wait_semaphores(&wait_semaphors) - .swapchains(&swapchains) - .image_indices(&image_indices); - unsafe { - match self - .base - .swapchain_loader - .queue_present(self.base.main_queue, &present_info) - { - Err(vk::Result::ERROR_OUT_OF_DATE_KHR) | Ok(true) => self.recreate_swapchain(), - Ok(false) => Ok(()), - Err(err) => Err(anyhow!("failed to present queue: {err:?}")), + self.device.destroy_pipeline(pipeline.pipeline, None); + self.device + .destroy_pipeline_layout(pipeline.pipeline_layout, None); } + Ok(()) } } +} - pub fn draw( - &self, - pipeline: &Pipeline, - framebuffer: vk::Framebuffer, - clear_values: &[vk::ClearValue], - ) { - let render_pass_begin_info = vk::RenderPassBeginInfo::default() - .render_pass(self.render_pass) - .framebuffer(framebuffer) - .render_area(vk::Rect2D { - offset: vk::Offset2D { x: 0, y: 0 }, - extent: self.scissors[0].extent, - }) - .clear_values(clear_values); - self.record_submit_commandbuffer( - &[vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT], - |device, draw_command_buffer| unsafe { - device.cmd_begin_render_pass( - draw_command_buffer, - &render_pass_begin_info, - vk::SubpassContents::INLINE, - ); - device.cmd_bind_pipeline( - draw_command_buffer, - vk::PipelineBindPoint::GRAPHICS, - pipeline.pipeline, - ); - device.cmd_set_viewport(draw_command_buffer, 0, &self.viewports); - device.cmd_set_scissor(draw_command_buffer, 0, &self.scissors); - - let push_constants = ShaderConstants { - width: self.scissors[0].extent.width, - height: self.scissors[0].extent.height, - time: self.start.elapsed().as_secs_f32(), - - // FIXME(eddyb) implement mouse support for the ash runner. - cursor_x: 0.0, - cursor_y: 0.0, - drag_start_x: 0.0, - drag_start_y: 0.0, - drag_end_x: 0.0, - drag_end_y: 0.0, - mouse_button_pressed: 0, - mouse_button_press_time: [f32::NEG_INFINITY; 3], - }; - device.cmd_push_constants( - draw_command_buffer, - pipeline.pipeline_layout, - ash::vk::ShaderStageFlags::ALL, - 0, - bytemuck::bytes_of(&push_constants), - ); - - device.cmd_draw(draw_command_buffer, 3, 1, 0, 0); - device.cmd_end_render_pass(draw_command_buffer); - }, - ); - } - - /// Helper function for submitting command buffers. Immediately waits for the fence before the command buffer - /// is executed. That way we can delay the waiting for the fences by 1 frame which is good for performance. - /// Make sure to create the fence in a signaled state on the first use. - pub fn record_submit_commandbuffer( +impl MyRenderPipeline { + pub fn render( &self, - wait_mask: &[vk::PipelineStageFlags], - f: F, - ) { + device: &MyDevice, + cmd: vk::CommandBuffer, + color_out: vk::ImageView, + extent: vk::Extent2D, + push_constants: ShaderConstants, + ) -> anyhow::Result<()> { unsafe { - self.base - .device - .wait_for_fences(&[self.sync.draw_commands_reuse_fence], true, u64::MAX) - .expect("Wait for fence failed."); - - self.base - .device - .reset_fences(&[self.sync.draw_commands_reuse_fence]) - .expect("Reset fences failed."); - - // As we only have a single command buffer, we can simply reset the entire pool instead of just the buffer. - // Doing this is a little bit faster, see - // https://arm-software.github.io/vulkan_best_practice_for_mobile_developers/samples/performance/command_buffer_usage/command_buffer_usage_tutorial.html#resetting-the-command-pool - self.base - .device - .reset_command_pool(self.commands.pool, vk::CommandPoolResetFlags::empty()) - .expect("Reset command pool failed."); - - let command_buffer_begin_info = vk::CommandBufferBeginInfo::default() - .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT); - - self.base - .device - .begin_command_buffer( - self.commands.draw_command_buffer, - &command_buffer_begin_info, - ) - .expect("Begin commandbuffer"); - - f(&self.base.device, self.commands.draw_command_buffer); - - self.base - .device - .end_command_buffer(self.commands.draw_command_buffer) - .expect("End commandbuffer"); - - let command_buffers = vec![self.commands.draw_command_buffer]; - let wait_semaphores = &[self.sync.present_complete_semaphore]; - let signal_semaphores = &[self.sync.rendering_complete_semaphore]; - let submit_info = vk::SubmitInfo::default() - .wait_semaphores(wait_semaphores) - .wait_dst_stage_mask(wait_mask) - .command_buffers(&command_buffers) - .signal_semaphores(signal_semaphores); - - self.base - .device - .queue_submit( - self.base.main_queue, - &[submit_info], - self.sync.draw_commands_reuse_fence, - ) - .expect("queue submit failed."); + let render_area = vk::Rect2D { + offset: vk::Offset2D::default(), + extent, + }; + + device.cmd_begin_rendering( + cmd, + &vk::RenderingInfo::default() + .render_area(render_area) + .layer_count(1) + .color_attachments(&[vk::RenderingAttachmentInfo::default() + .image_view(color_out) + .load_op(vk::AttachmentLoadOp::CLEAR) + .store_op(vk::AttachmentStoreOp::STORE) + .clear_value(vk::ClearValue { + color: vk::ClearColorValue { + float32: [0.0, 1.0, 0.0, 0.0], + }, + }) + .image_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)]), + ); + device.cmd_bind_pipeline(cmd, vk::PipelineBindPoint::GRAPHICS, self.pipeline); + device.cmd_set_viewport( + cmd, + 0, + &[vk::Viewport { + // contains a y-flip + x: 0.0, + y: extent.height as f32, + width: extent.width as f32, + height: -(extent.height as f32), + min_depth: 0.0, + max_depth: 1.0, + }], + ); + device.cmd_set_scissor(cmd, 0, &[render_area]); + device.cmd_push_constants( + cmd, + self.pipeline_layout, + vk::ShaderStageFlags::ALL, + 0, + bytemuck::bytes_of(&push_constants), + ); + device.cmd_draw(cmd, 3, 1, 0, 0); + device.cmd_end_rendering(cmd); + Ok(()) } } } -impl Drop for RenderCtx { +impl Drop for MyRenderPipelineManager { fn drop(&mut self) { unsafe { - self.base.device.device_wait_idle().unwrap(); - self.base - .device - .destroy_semaphore(self.sync.present_complete_semaphore, None); - self.base - .device - .destroy_semaphore(self.sync.rendering_complete_semaphore, None); - self.base - .device - .destroy_fence(self.sync.draw_commands_reuse_fence, None); - self.base - .device - .free_command_buffers(self.commands.pool, &[self.commands.draw_command_buffer]); - self.base.device.destroy_render_pass(self.render_pass, None); - self.cleanup_pipeline(); - self.cleanup_swapchain(); - self.base - .device - .destroy_command_pool(self.commands.pool, None); + self.destroy_pipeline().ok(); } } } -pub struct RenderSync { - pub present_complete_semaphore: vk::Semaphore, - pub rendering_complete_semaphore: vk::Semaphore, - pub draw_commands_reuse_fence: vk::Fence, +/// The renderer manages our command buffer and submits the commands, using [`MyRenderPipeline`] for drawing. +pub struct MyRenderer { + pub device: Arc, + pub pipeline: MyRenderPipelineManager, + pub command: SingleCommandBuffer, } -impl RenderSync { - pub fn new(base: &RenderBase) -> anyhow::Result { - let fence_create_info = - vk::FenceCreateInfo::default().flags(vk::FenceCreateFlags::SIGNALED); - - let semaphore_create_info = vk::SemaphoreCreateInfo::default(); +impl MyRenderer { + pub fn new(pipeline: MyRenderPipelineManager) -> anyhow::Result { + Ok(Self { + command: SingleCommandBuffer::new(pipeline.device.clone())?, + device: pipeline.device.clone(), + pipeline, + }) + } + pub fn render_frame( + &mut self, + frame: DrawFrame, + push_constants: ShaderConstants, + ) -> anyhow::Result<()> { unsafe { - let draw_commands_reuse_fence = base.device.create_fence(&fence_create_info, None)?; - let present_complete_semaphore = - base.device.create_semaphore(&semaphore_create_info, None)?; - let rendering_complete_semaphore = - base.device.create_semaphore(&semaphore_create_info, None)?; + let device = &self.device; + let pipeline = self.pipeline.get_pipeline()?; + let cmd = self.command.cmd; - Ok(Self { - present_complete_semaphore, - rendering_complete_semaphore, - draw_commands_reuse_fence, - }) + device.reset_command_pool(self.command.pool, vk::CommandPoolResetFlags::default())?; + + { + device.begin_command_buffer( + cmd, + &vk::CommandBufferBeginInfo::default() + .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT), + )?; + device.cmd_pipeline_barrier2( + cmd, + &vk::DependencyInfo::default().image_memory_barriers(&[ + vk::ImageMemoryBarrier2::default() + .image(frame.image) + .src_access_mask(vk::AccessFlags2::NONE) + .src_stage_mask(vk::PipelineStageFlags2::ALL_COMMANDS) + .old_layout(vk::ImageLayout::UNDEFINED) + .dst_access_mask(vk::AccessFlags2::COLOR_ATTACHMENT_WRITE) + .dst_stage_mask(vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT) + .new_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL) + .subresource_range( + vk::ImageSubresourceRange::default() + .aspect_mask(vk::ImageAspectFlags::COLOR) + .base_mip_level(0) + .level_count(1) + .base_array_layer(0) + .layer_count(1), + ), + ]), + ); + pipeline.render(&device, cmd, frame.image_view, frame.extent, push_constants)?; + device.cmd_pipeline_barrier2( + cmd, + &vk::DependencyInfo::default().image_memory_barriers(&[ + vk::ImageMemoryBarrier2::default() + .image(frame.image) + .src_access_mask(vk::AccessFlags2::COLOR_ATTACHMENT_WRITE) + .src_stage_mask(vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT) + .old_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL) + .dst_access_mask(vk::AccessFlags2::NONE) + .dst_stage_mask(vk::PipelineStageFlags2::ALL_COMMANDS) + .new_layout(vk::ImageLayout::PRESENT_SRC_KHR) + .subresource_range( + vk::ImageSubresourceRange::default() + .aspect_mask(vk::ImageAspectFlags::COLOR) + .base_mip_level(0) + .level_count(1) + .base_array_layer(0) + .layer_count(1), + ), + ]), + ); + device.end_command_buffer(cmd)?; + } + + device.queue_submit2( + device.main_queue, + &[vk::SubmitInfo2::default() + .wait_semaphore_infos(&[vk::SemaphoreSubmitInfo::default() + .semaphore(frame.acquire_semaphore) + .stage_mask(vk::PipelineStageFlags2::TOP_OF_PIPE)]) + .command_buffer_infos(&[ + vk::CommandBufferSubmitInfo::default().command_buffer(cmd) + ]) + .signal_semaphore_infos(&[vk::SemaphoreSubmitInfo::default() + .semaphore(frame.draw_finished_semaphore) + .stage_mask(vk::PipelineStageFlags2::BOTTOM_OF_PIPE)])], + frame.draw_finished_fence, + )?; + Ok(()) } } } -pub struct RenderCommandPool { +/// A single command buffer with a pool +pub struct SingleCommandBuffer { + pub device: Arc, pub pool: vk::CommandPool, - pub draw_command_buffer: vk::CommandBuffer, + pub cmd: vk::CommandBuffer, } -impl RenderCommandPool { - pub fn new(base: &RenderBase) -> Self { - let pool = { - let pool_create_info = - vk::CommandPoolCreateInfo::default().queue_family_index(base.queue_family_index); - - unsafe { - base.device - .create_command_pool(&pool_create_info, None) - .unwrap() - } - }; - - let command_buffers = { - let command_buffer_allocate_info = vk::CommandBufferAllocateInfo::default() - .command_buffer_count(1) - .command_pool(pool) - .level(vk::CommandBufferLevel::PRIMARY); - - unsafe { - base.device - .allocate_command_buffers(&command_buffer_allocate_info) - .unwrap() - } - }; - - Self { - pool, - draw_command_buffer: command_buffers[0], +impl SingleCommandBuffer { + pub fn new(device: Arc) -> anyhow::Result { + unsafe { + let pool = device.device.create_command_pool( + &vk::CommandPoolCreateInfo::default().queue_family_index(device.main_queue_family), + None, + )?; + + let command_buffers = device.device.allocate_command_buffers( + &vk::CommandBufferAllocateInfo::default() + .command_buffer_count(1) + .command_pool(pool) + .level(vk::CommandBufferLevel::PRIMARY), + )?; + assert_eq!(command_buffers.len(), 1); + let cmd = command_buffers[0]; + + Ok(Self { device, pool, cmd }) } } } -pub struct Pipeline { - pub pipeline: vk::Pipeline, - pub pipeline_layout: vk::PipelineLayout, +impl Drop for SingleCommandBuffer { + fn drop(&mut self) { + unsafe { + let device = &self.device; + device.free_command_buffers(self.pool, &[self.cmd]); + device.destroy_command_pool(self.pool, None); + } + } } unsafe extern "system" fn vulkan_debug_callback( From 0ebe9dd34654410800c155858683da04ef6cea6e Mon Sep 17 00:00:00 2001 From: firestar99 Date: Tue, 30 Sep 2025 16:13:53 +0200 Subject: [PATCH 07/12] ash runner: split it into different modules --- examples/runners/ash/src/device.rs | 191 ++++ examples/runners/ash/src/graphics.rs | 359 +++++++ examples/runners/ash/src/main.rs | 935 +----------------- .../runners/ash/src/single_command_buffer.rs | 42 + examples/runners/ash/src/swapchain.rs | 348 +++++++ 5 files changed, 950 insertions(+), 925 deletions(-) create mode 100644 examples/runners/ash/src/device.rs create mode 100644 examples/runners/ash/src/graphics.rs create mode 100644 examples/runners/ash/src/single_command_buffer.rs create mode 100644 examples/runners/ash/src/swapchain.rs diff --git a/examples/runners/ash/src/device.rs b/examples/runners/ash/src/device.rs new file mode 100644 index 0000000000..9f658ad465 --- /dev/null +++ b/examples/runners/ash/src/device.rs @@ -0,0 +1,191 @@ +use crate::Options; +use anyhow::{Context, anyhow}; +use ash::{ext, khr, vk}; +use std::borrow::Cow; +use std::ffi::{CStr, c_char}; +use std::ops::Deref; +use std::sync::Arc; + +/// Central struct containing the Vulkan instance and device, among others +pub struct MyDevice { + pub entry: ash::Entry, + pub instance: ash::Instance, + pub physical_device: vk::PhysicalDevice, + pub device: ash::Device, + pub main_queue_family: u32, + pub main_queue: vk::Queue, + pub debug_ext_instance: ext::debug_utils::Instance, + pub debug_ext_device: ext::debug_utils::Device, + pub surface_ext: khr::surface::Instance, + pub swapchain_ext: khr::swapchain::Device, + debug_callback: vk::DebugUtilsMessengerEXT, +} + +impl Deref for MyDevice { + type Target = ash::Device; + + fn deref(&self) -> &Self::Target { + &self.device + } +} + +impl MyDevice { + pub fn new(extension_names: &[*const c_char], options: &Options) -> anyhow::Result> { + unsafe { + cfg_if::cfg_if! { + if #[cfg(target_os = "macos")] { + let entry = ash_molten::load(); + } else { + let entry = ash::Entry::load()?; + } + } + + let instance = { + let layer_names: &'static [_] = if options.debug_layer { + const { &[c"VK_LAYER_KHRONOS_validation".as_ptr()] } + } else { + &[] + }; + + let mut extension_names_raw = extension_names.to_vec(); + extension_names_raw.push(ext::debug_utils::NAME.as_ptr()); + + let app_name = c"VulkanTriangle"; + entry + .create_instance( + &vk::InstanceCreateInfo::default() + .application_info( + &vk::ApplicationInfo::default() + .application_name(app_name) + .application_version(0) + .engine_name(app_name) + .engine_version(0) + .api_version(vk::make_api_version(0, 1, 3, 0)), + ) + .enabled_layer_names(layer_names) + .enabled_extension_names(&extension_names_raw), + None, + ) + .context("create_instance")? + }; + + let debug_instance = ext::debug_utils::Instance::new(&entry, &instance); + let debug_callback = { + debug_instance.create_debug_utils_messenger( + &vk::DebugUtilsMessengerCreateInfoEXT::default() + .message_severity( + vk::DebugUtilsMessageSeverityFlagsEXT::ERROR + | vk::DebugUtilsMessageSeverityFlagsEXT::WARNING + | vk::DebugUtilsMessageSeverityFlagsEXT::INFO, + ) + .message_type( + vk::DebugUtilsMessageTypeFlagsEXT::GENERAL + | vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION + | vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE, + ) + .pfn_user_callback(Some(vulkan_debug_callback)), + None, + )? + }; + + let physical_device = { + instance + .enumerate_physical_devices()? + .into_iter() + .min_by_key(|phy| { + match instance.get_physical_device_properties(*phy).device_type { + vk::PhysicalDeviceType::DISCRETE_GPU => 1, + vk::PhysicalDeviceType::VIRTUAL_GPU => 2, + vk::PhysicalDeviceType::INTEGRATED_GPU => 3, + vk::PhysicalDeviceType::CPU => 4, + _ => 5, + } + }) + .ok_or(anyhow!("No physical devices available"))? + }; + + let main_queue_family = { + instance + .get_physical_device_queue_family_properties(physical_device) + .into_iter() + .enumerate() + .find(|(_, prop)| prop.queue_flags.contains(vk::QueueFlags::GRAPHICS)) + .ok_or(anyhow!( + "No graphics + compute queues on physical device available" + ))? + .0 as u32 + }; + + let device = instance + .create_device( + physical_device, + &vk::DeviceCreateInfo::default() + .push_next( + &mut vk::PhysicalDeviceVulkanMemoryModelFeatures::default() + .vulkan_memory_model(true), + ) + .push_next( + &mut vk::PhysicalDeviceVulkan13Features::default() + .synchronization2(true) + .dynamic_rendering(true), + ) + .queue_create_infos(&[vk::DeviceQueueCreateInfo::default() + .queue_family_index(main_queue_family) + .queue_priorities(&[1.0])]) + .enabled_extension_names(&[ + khr::swapchain::NAME.as_ptr(), + khr::shader_non_semantic_info::NAME.as_ptr(), + ]), + None, + ) + .context("create_device")?; + let main_queue = device.get_device_queue(main_queue_family, 0); + + Ok(Arc::new(Self { + debug_ext_device: ext::debug_utils::Device::new(&instance, &device), + surface_ext: khr::surface::Instance::new(&entry, &instance), + swapchain_ext: khr::swapchain::Device::new(&instance, &device), + entry, + instance, + physical_device, + device, + main_queue_family, + main_queue, + debug_ext_instance: debug_instance, + debug_callback, + })) + } + } +} + +impl Drop for MyDevice { + fn drop(&mut self) { + unsafe { + self.debug_ext_instance + .destroy_debug_utils_messenger(self.debug_callback, None); + self.device.destroy_device(None); + self.instance.destroy_instance(None); + } + } +} + +unsafe extern "system" fn vulkan_debug_callback( + message_severity: vk::DebugUtilsMessageSeverityFlagsEXT, + _message_type: vk::DebugUtilsMessageTypeFlagsEXT, + p_callback_data: *const vk::DebugUtilsMessengerCallbackDataEXT<'_>, + _user_data: *mut std::os::raw::c_void, +) -> vk::Bool32 { + let callback_data = unsafe { *p_callback_data }; + let message_id_name = if callback_data.p_message_id_name.is_null() { + Cow::from("") + } else { + unsafe { CStr::from_ptr(callback_data.p_message_id_name).to_string_lossy() } + }; + let message = if callback_data.p_message.is_null() { + Cow::from("") + } else { + unsafe { CStr::from_ptr(callback_data.p_message).to_string_lossy() } + }; + println!("{message_severity:?}: [{message_id_name}] : {message}"); + vk::FALSE +} diff --git a/examples/runners/ash/src/graphics.rs b/examples/runners/ash/src/graphics.rs new file mode 100644 index 0000000000..58bfc970a3 --- /dev/null +++ b/examples/runners/ash/src/graphics.rs @@ -0,0 +1,359 @@ +use crate::device::MyDevice; +use crate::single_command_buffer::SingleCommandBuffer; +use crate::swapchain::DrawFrame; +use anyhow::Context; +use ash::vk; +use shared::ShaderConstants; +use std::sync::Arc; + +/// Manages the creation and recreation of [`MyRenderPipeline`], whenever new shader code ([`Self::set_shader_code`]) +/// is submitted or the spec constant is changed ([`Self::set_sky_fs_sun_intensity_factor`]) +pub struct MyRenderPipelineManager { + pub device: Arc, + color_out_format: vk::Format, + shader_code: Vec, + pipeline: Option, + + // Only used for sky-shader. + // NOTE(eddyb) this acts like an integration test for specialization constants. + sky_fs_spec_id_0x5007_sun_intensity_extra_spec_const_factor: u32, + should_recreate: bool, +} + +pub struct MyRenderPipeline { + pub pipeline: vk::Pipeline, + pub pipeline_layout: vk::PipelineLayout, +} + +impl MyRenderPipelineManager { + pub fn new( + device: Arc, + color_out_format: vk::Format, + shader_code: Vec, + ) -> anyhow::Result { + Ok(Self { + device, + color_out_format, + shader_code, + pipeline: None, + sky_fs_spec_id_0x5007_sun_intensity_extra_spec_const_factor: 100, + should_recreate: true, + }) + } + + #[inline] + pub fn set_sky_fs_sun_intensity_factor(&mut self, factor: u32) { + self.sky_fs_spec_id_0x5007_sun_intensity_extra_spec_const_factor = factor; + self.should_recreate(); + } + + #[inline] + pub fn get_sky_fs_sun_intensity_factor(&self) -> u32 { + self.sky_fs_spec_id_0x5007_sun_intensity_extra_spec_const_factor + } + + #[inline] + pub fn set_shader_code(&mut self, shader_code: Vec) { + self.shader_code = shader_code; + self.should_recreate(); + } + + #[inline] + pub fn should_recreate(&mut self) { + self.should_recreate = true; + } + + pub fn get_pipeline(&mut self) -> anyhow::Result<&MyRenderPipeline> { + if self.should_recreate { + self.rebuild_pipeline()?; + } + Ok(self.pipeline.as_ref().unwrap()) + } + + /// Update shaders and rebuild the pipeline + fn rebuild_pipeline(&mut self) -> anyhow::Result<()> { + unsafe { + self.destroy_pipeline()?; + + let shader_module = self.device.create_shader_module( + &vk::ShaderModuleCreateInfo::default().code(&self.shader_code), + None, + )?; + + let pipeline_layout = self.device.create_pipeline_layout( + &vk::PipelineLayoutCreateInfo::default().push_constant_ranges(&[ + vk::PushConstantRange::default() + .offset(0) + .size(size_of::() as u32) + .stage_flags(vk::ShaderStageFlags::ALL), + ]), + None, + )?; + + let mut pipelines = + self + .device + .create_graphics_pipelines(vk::PipelineCache::null(), &[vk::GraphicsPipelineCreateInfo::default() + .stages( + &[ + vk::PipelineShaderStageCreateInfo { + module: shader_module, + p_name: c"main_vs".as_ptr(), + stage: vk::ShaderStageFlags::VERTEX, + ..Default::default() + }, + vk::PipelineShaderStageCreateInfo { + module: shader_module, + p_name: c"main_fs".as_ptr(), + stage: vk::ShaderStageFlags::FRAGMENT, + // NOTE(eddyb) this acts like an integration test for specialization constants. + p_specialization_info: &vk::SpecializationInfo::default() + .map_entries(&[vk::SpecializationMapEntry::default() + .constant_id(0x5007) + .offset(0) + .size(4)]) + .data(&u32::to_le_bytes( + self.sky_fs_spec_id_0x5007_sun_intensity_extra_spec_const_factor, + )), + ..Default::default() + }, + ], + ) + .vertex_input_state(&vk::PipelineVertexInputStateCreateInfo::default()) + .input_assembly_state(&vk::PipelineInputAssemblyStateCreateInfo { + topology: vk::PrimitiveTopology::TRIANGLE_LIST, + ..Default::default() + }) + .rasterization_state(&vk::PipelineRasterizationStateCreateInfo { + front_face: vk::FrontFace::COUNTER_CLOCKWISE, + line_width: 1.0, + ..Default::default() + }) + .multisample_state(&vk::PipelineMultisampleStateCreateInfo { + rasterization_samples: vk::SampleCountFlags::TYPE_1, + ..Default::default() + }) + .depth_stencil_state(&vk::PipelineDepthStencilStateCreateInfo::default()) + .color_blend_state( + &vk::PipelineColorBlendStateCreateInfo::default() + .attachments( + &[vk::PipelineColorBlendAttachmentState { + blend_enable: 0, + src_color_blend_factor: vk::BlendFactor::SRC_COLOR, + dst_color_blend_factor: vk::BlendFactor::ONE_MINUS_DST_COLOR, + color_blend_op: vk::BlendOp::ADD, + src_alpha_blend_factor: vk::BlendFactor::ZERO, + dst_alpha_blend_factor: vk::BlendFactor::ZERO, + alpha_blend_op: vk::BlendOp::ADD, + color_write_mask: vk::ColorComponentFlags::RGBA, + }], + ), + ) + .dynamic_state( + &vk::PipelineDynamicStateCreateInfo::default() + .dynamic_states(&[vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR]), + ) + .viewport_state( + &vk::PipelineViewportStateCreateInfo::default() + .scissor_count(1) + .viewport_count(1), + ) + .layout(pipeline_layout) + .push_next(&mut vk::PipelineRenderingCreateInfo::default().color_attachment_formats(&[self.color_out_format])) + ], None).map_err(|(_, e)| e) + .context("Unable to create graphics pipeline")?; + + // A single `pipeline_info` results in a single pipeline. + assert_eq!(pipelines.len(), 1); + self.pipeline = pipelines.pop().map(|pipeline| MyRenderPipeline { + pipeline, + pipeline_layout, + }); + + // shader modules are allowed to be deleted after the pipeline has been created + self.device.destroy_shader_module(shader_module, None); + Ok(()) + } + } + + unsafe fn destroy_pipeline(&mut self) -> anyhow::Result<()> { + unsafe { + if let Some(pipeline) = self.pipeline.take() { + // Figuring out when the pipeline stops being used is hard, so we take this shortcut + self.device.device_wait_idle()?; + + self.device.destroy_pipeline(pipeline.pipeline, None); + self.device + .destroy_pipeline_layout(pipeline.pipeline_layout, None); + } + Ok(()) + } + } +} + +impl MyRenderPipeline { + pub fn render( + &self, + device: &MyDevice, + cmd: vk::CommandBuffer, + color_out: vk::ImageView, + extent: vk::Extent2D, + push_constants: ShaderConstants, + ) -> anyhow::Result<()> { + unsafe { + let render_area = vk::Rect2D { + offset: vk::Offset2D::default(), + extent, + }; + + device.cmd_begin_rendering( + cmd, + &vk::RenderingInfo::default() + .render_area(render_area) + .layer_count(1) + .color_attachments(&[vk::RenderingAttachmentInfo::default() + .image_view(color_out) + .load_op(vk::AttachmentLoadOp::CLEAR) + .store_op(vk::AttachmentStoreOp::STORE) + .clear_value(vk::ClearValue { + color: vk::ClearColorValue { + float32: [0.0, 1.0, 0.0, 0.0], + }, + }) + .image_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)]), + ); + device.cmd_bind_pipeline(cmd, vk::PipelineBindPoint::GRAPHICS, self.pipeline); + device.cmd_set_viewport( + cmd, + 0, + &[vk::Viewport { + // contains a y-flip + x: 0.0, + y: extent.height as f32, + width: extent.width as f32, + height: -(extent.height as f32), + min_depth: 0.0, + max_depth: 1.0, + }], + ); + device.cmd_set_scissor(cmd, 0, &[render_area]); + device.cmd_push_constants( + cmd, + self.pipeline_layout, + vk::ShaderStageFlags::ALL, + 0, + bytemuck::bytes_of(&push_constants), + ); + device.cmd_draw(cmd, 3, 1, 0, 0); + device.cmd_end_rendering(cmd); + Ok(()) + } + } +} + +impl Drop for MyRenderPipelineManager { + fn drop(&mut self) { + unsafe { + self.destroy_pipeline().ok(); + } + } +} + +/// The renderer manages our command buffer and submits the commands, using [`MyRenderPipeline`] for drawing. +pub struct MyRenderer { + pub device: Arc, + pub pipeline: MyRenderPipelineManager, + pub command: SingleCommandBuffer, +} + +impl MyRenderer { + pub fn new(pipeline: MyRenderPipelineManager) -> anyhow::Result { + Ok(Self { + command: SingleCommandBuffer::new(pipeline.device.clone())?, + device: pipeline.device.clone(), + pipeline, + }) + } + + pub fn render_frame( + &mut self, + frame: DrawFrame, + push_constants: ShaderConstants, + ) -> anyhow::Result<()> { + unsafe { + let device = &self.device; + let pipeline = self.pipeline.get_pipeline()?; + let cmd = self.command.cmd; + + device.reset_command_pool(self.command.pool, vk::CommandPoolResetFlags::default())?; + + { + device.begin_command_buffer( + cmd, + &vk::CommandBufferBeginInfo::default() + .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT), + )?; + device.cmd_pipeline_barrier2( + cmd, + &vk::DependencyInfo::default().image_memory_barriers(&[ + vk::ImageMemoryBarrier2::default() + .image(frame.image) + .src_access_mask(vk::AccessFlags2::NONE) + .src_stage_mask(vk::PipelineStageFlags2::ALL_COMMANDS) + .old_layout(vk::ImageLayout::UNDEFINED) + .dst_access_mask(vk::AccessFlags2::COLOR_ATTACHMENT_WRITE) + .dst_stage_mask(vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT) + .new_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL) + .subresource_range( + vk::ImageSubresourceRange::default() + .aspect_mask(vk::ImageAspectFlags::COLOR) + .base_mip_level(0) + .level_count(1) + .base_array_layer(0) + .layer_count(1), + ), + ]), + ); + pipeline.render(&device, cmd, frame.image_view, frame.extent, push_constants)?; + device.cmd_pipeline_barrier2( + cmd, + &vk::DependencyInfo::default().image_memory_barriers(&[ + vk::ImageMemoryBarrier2::default() + .image(frame.image) + .src_access_mask(vk::AccessFlags2::COLOR_ATTACHMENT_WRITE) + .src_stage_mask(vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT) + .old_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL) + .dst_access_mask(vk::AccessFlags2::NONE) + .dst_stage_mask(vk::PipelineStageFlags2::ALL_COMMANDS) + .new_layout(vk::ImageLayout::PRESENT_SRC_KHR) + .subresource_range( + vk::ImageSubresourceRange::default() + .aspect_mask(vk::ImageAspectFlags::COLOR) + .base_mip_level(0) + .level_count(1) + .base_array_layer(0) + .layer_count(1), + ), + ]), + ); + device.end_command_buffer(cmd)?; + } + + device.queue_submit2( + device.main_queue, + &[vk::SubmitInfo2::default() + .wait_semaphore_infos(&[vk::SemaphoreSubmitInfo::default() + .semaphore(frame.acquire_semaphore) + .stage_mask(vk::PipelineStageFlags2::TOP_OF_PIPE)]) + .command_buffer_infos(&[ + vk::CommandBufferSubmitInfo::default().command_buffer(cmd) + ]) + .signal_semaphore_infos(&[vk::SemaphoreSubmitInfo::default() + .semaphore(frame.draw_finished_semaphore) + .stage_mask(vk::PipelineStageFlags2::BOTTOM_OF_PIPE)])], + frame.draw_finished_fence, + )?; + Ok(()) + } + } +} diff --git a/examples/runners/ash/src/main.rs b/examples/runners/ash/src/main.rs index 7f943ba8c5..7f97c281ad 100644 --- a/examples/runners/ash/src/main.rs +++ b/examples/runners/ash/src/main.rs @@ -70,18 +70,16 @@ // crate-specific exceptions: // #![allow()] +use crate::device::MyDevice; +use crate::graphics::{MyRenderPipelineManager, MyRenderer}; +use crate::swapchain::MySwapchainManager; use anyhow::{Context, anyhow}; -use ash::{ext, khr, util::read_spv, vk}; +use ash::util::read_spv; use clap::{Parser, ValueEnum}; -use raw_window_handle::{HasDisplayHandle as _, HasWindowHandle as _}; +use raw_window_handle::HasDisplayHandle as _; use shared::ShaderConstants; use spirv_builder::{MetadataPrintout, SpirvBuilder}; -use std::ffi::c_char; -use std::ops::Deref; -use std::sync::Arc; use std::{ - borrow::Cow, - ffi::CStr, fs::File, path::PathBuf, sync::mpsc::{TryRecvError, sync_channel}, @@ -93,6 +91,11 @@ use winit::{ event_loop::{ControlFlow, EventLoop}, }; +pub mod device; +pub mod graphics; +pub mod single_command_buffer; +pub mod swapchain; + // This runner currently doesn't run the `compute` shader example. #[derive(Debug, PartialEq, Eq, Copy, Clone, ValueEnum)] pub enum RustGPUShader { @@ -272,921 +275,3 @@ pub fn compile_shaders(shader: &RustGPUShader) -> anyhow::Result> { Ok(read_spv(&mut File::open(spv_path)?)?) } - -/// Central struct containing the Vulkan instance and device, among others -pub struct MyDevice { - pub entry: ash::Entry, - pub instance: ash::Instance, - pub physical_device: vk::PhysicalDevice, - pub device: ash::Device, - pub main_queue_family: u32, - pub main_queue: vk::Queue, - pub debug_ext_instance: ext::debug_utils::Instance, - pub debug_ext_device: ext::debug_utils::Device, - pub surface_ext: khr::surface::Instance, - pub swapchain_ext: khr::swapchain::Device, - debug_callback: vk::DebugUtilsMessengerEXT, -} - -impl Deref for MyDevice { - type Target = ash::Device; - - fn deref(&self) -> &Self::Target { - &self.device - } -} - -impl MyDevice { - pub fn new(extension_names: &[*const c_char], options: &Options) -> anyhow::Result> { - unsafe { - cfg_if::cfg_if! { - if #[cfg(target_os = "macos")] { - let entry = ash_molten::load(); - } else { - let entry = ash::Entry::load()?; - } - } - - let instance = { - let layer_names: &'static [_] = if options.debug_layer { - const { &[c"VK_LAYER_KHRONOS_validation".as_ptr()] } - } else { - &[] - }; - - let mut extension_names_raw = extension_names.to_vec(); - extension_names_raw.push(ext::debug_utils::NAME.as_ptr()); - - let app_name = c"VulkanTriangle"; - entry - .create_instance( - &vk::InstanceCreateInfo::default() - .application_info( - &vk::ApplicationInfo::default() - .application_name(app_name) - .application_version(0) - .engine_name(app_name) - .engine_version(0) - .api_version(vk::make_api_version(0, 1, 3, 0)), - ) - .enabled_layer_names(layer_names) - .enabled_extension_names(&extension_names_raw), - None, - ) - .context("create_instance")? - }; - - let debug_instance = ext::debug_utils::Instance::new(&entry, &instance); - let debug_callback = { - debug_instance.create_debug_utils_messenger( - &vk::DebugUtilsMessengerCreateInfoEXT::default() - .message_severity( - vk::DebugUtilsMessageSeverityFlagsEXT::ERROR - | vk::DebugUtilsMessageSeverityFlagsEXT::WARNING - | vk::DebugUtilsMessageSeverityFlagsEXT::INFO, - ) - .message_type( - vk::DebugUtilsMessageTypeFlagsEXT::GENERAL - | vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION - | vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE, - ) - .pfn_user_callback(Some(vulkan_debug_callback)), - None, - )? - }; - - let physical_device = { - instance - .enumerate_physical_devices()? - .into_iter() - .min_by_key(|phy| { - match instance.get_physical_device_properties(*phy).device_type { - vk::PhysicalDeviceType::DISCRETE_GPU => 1, - vk::PhysicalDeviceType::VIRTUAL_GPU => 2, - vk::PhysicalDeviceType::INTEGRATED_GPU => 3, - vk::PhysicalDeviceType::CPU => 4, - _ => 5, - } - }) - .ok_or(anyhow!("No physical devices available"))? - }; - - let main_queue_family = { - instance - .get_physical_device_queue_family_properties(physical_device) - .into_iter() - .enumerate() - .find(|(_, prop)| prop.queue_flags.contains(vk::QueueFlags::GRAPHICS)) - .ok_or(anyhow!( - "No graphics + compute queues on physical device available" - ))? - .0 as u32 - }; - - let device = instance - .create_device( - physical_device, - &vk::DeviceCreateInfo::default() - .push_next( - &mut vk::PhysicalDeviceVulkanMemoryModelFeatures::default() - .vulkan_memory_model(true), - ) - .push_next( - &mut vk::PhysicalDeviceVulkan13Features::default() - .synchronization2(true) - .dynamic_rendering(true), - ) - .queue_create_infos(&[vk::DeviceQueueCreateInfo::default() - .queue_family_index(main_queue_family) - .queue_priorities(&[1.0])]) - .enabled_extension_names(&[ - khr::swapchain::NAME.as_ptr(), - khr::shader_non_semantic_info::NAME.as_ptr(), - ]), - None, - ) - .context("create_device")?; - let main_queue = device.get_device_queue(main_queue_family, 0); - - Ok(Arc::new(Self { - debug_ext_device: ext::debug_utils::Device::new(&instance, &device), - surface_ext: khr::surface::Instance::new(&entry, &instance), - swapchain_ext: khr::swapchain::Device::new(&instance, &device), - entry, - instance, - physical_device, - device, - main_queue_family, - main_queue, - debug_ext_instance: debug_instance, - debug_callback, - })) - } - } -} - -impl Drop for MyDevice { - fn drop(&mut self) { - unsafe { - self.debug_ext_instance - .destroy_debug_utils_messenger(self.debug_callback, None); - self.device.destroy_device(None); - self.instance.destroy_instance(None); - } - } -} - -/// A binary semaphore for swapchain operations -pub struct SwapchainSync { - acquire_semaphore: vk::Semaphore, - render_semaphore: vk::Semaphore, - render_fence: vk::Fence, -} - -impl SwapchainSync { - pub unsafe fn new(device: &MyDevice) -> anyhow::Result { - unsafe { - let signaled_fence = - vk::FenceCreateInfo::default().flags(vk::FenceCreateFlags::SIGNALED); - Ok(Self { - acquire_semaphore: device - .create_semaphore(&vk::SemaphoreCreateInfo::default(), None)?, - render_semaphore: device - .create_semaphore(&vk::SemaphoreCreateInfo::default(), None)?, - render_fence: device.create_fence(&signaled_fence, None)?, - }) - } - } - - pub unsafe fn destroy(&self, device: &MyDevice) { - unsafe { - device.destroy_semaphore(self.acquire_semaphore, None); - device.destroy_semaphore(self.render_semaphore, None); - device.destroy_fence(self.render_fence, None); - } - } -} - -/// Takes care of all things swapchain related -/// -/// Intentionally kept simple and does not offer support for multiple frames in flight -pub struct MySwapchainManager { - pub device: Arc, - pub window: winit::window::Window, - pub surface: vk::SurfaceKHR, - pub surface_format: vk::SurfaceFormatKHR, - pub surface_capabilities: vk::SurfaceCapabilitiesKHR, - pub present_mode: vk::PresentModeKHR, - pub image_count: u32, - pub pre_transform: vk::SurfaceTransformFlagsKHR, - - // state below - active: Option, - should_recreate: bool, - sync: SwapchainSync, -} - -struct ActiveSwapchain { - extent: vk::Extent2D, - swapchain: vk::SwapchainKHR, - images: Vec<(vk::Image, vk::ImageView)>, -} - -impl MySwapchainManager { - pub fn new(device: Arc, window: winit::window::Window) -> anyhow::Result { - unsafe { - let surface_ext = &device.surface_ext; - - let surface = ash_window::create_surface( - &device.entry, - &device.instance, - window.display_handle().unwrap().into(), - window.window_handle().unwrap().into(), - None, - ) - .context("create_surface")?; - - let surface_format = { - let acceptable_formats = { - [ - vk::Format::R8G8B8_SRGB, - vk::Format::B8G8R8_SRGB, - vk::Format::R8G8B8A8_SRGB, - vk::Format::B8G8R8A8_SRGB, - vk::Format::A8B8G8R8_SRGB_PACK32, - ] - }; - *surface_ext - .get_physical_device_surface_formats(device.physical_device, surface)? - .iter() - .find(|sfmt| acceptable_formats.contains(&sfmt.format)) - .context("Unable to find suitable surface format.")? - }; - - let surface_capabilities = surface_ext - .get_physical_device_surface_capabilities(device.physical_device, surface)?; - let pre_transform = surface_capabilities.current_transform; - - let present_mode = surface_ext - .get_physical_device_surface_present_modes(device.physical_device, surface)? - .iter() - .cloned() - // Mailbox is preferred - .find(|&mode| mode == vk::PresentModeKHR::MAILBOX) - // FIFO is guaranteed to be available - .unwrap_or(vk::PresentModeKHR::FIFO); - - let image_count = { - let mut image_count = match present_mode { - // tripple buffering in mailbox mode:: one presenting, one ready and one drawing - vk::PresentModeKHR::MAILBOX => 3, - // double buffering in fifo mode: one presenting, one drawing - vk::PresentModeKHR::FIFO => 2, - _ => unreachable!(), - }; - if surface_capabilities.max_image_count != 0 { - image_count = image_count.min(surface_capabilities.max_image_count); - } - image_count.max(surface_capabilities.min_image_count) - }; - - let sync = SwapchainSync::new(&device)?; - Ok(Self { - device, - window, - surface, - surface_format, - surface_capabilities, - present_mode, - image_count, - pre_transform, - - active: None, - should_recreate: true, - sync, - }) - } - } - - #[inline] - fn should_recreate(&mut self) { - self.should_recreate = true; - } - - /// After this function is called, `Self.active` is initialized - unsafe fn recreate_swapchain(&mut self) -> anyhow::Result<()> { - unsafe { - let device = &self.device; - let swapchain_ext = &device.swapchain_ext; - let surface_ext = &self.device.surface_ext; - let format = self.surface_format.format; - - let extent = { - let window_size = self.window.inner_size(); - let capabilities = surface_ext.get_physical_device_surface_capabilities( - self.device.physical_device, - self.surface, - )?; - let min = capabilities.min_image_extent; - let max = capabilities.max_image_extent; - vk::Extent2D { - width: u32::clamp(window_size.width, min.width, max.width), - height: u32::clamp(window_size.height, min.height, max.height), - } - }; - - let old = self.active.take(); - if let Some(old) = old.as_ref() { - old.destroy_image_views(device); - } - - let swapchain = swapchain_ext - .create_swapchain( - &vk::SwapchainCreateInfoKHR::default() - .surface(self.surface) - .min_image_count(self.image_count) - .image_color_space(self.surface_format.color_space) - .image_format(format) - .image_extent(extent) - .image_usage(vk::ImageUsageFlags::COLOR_ATTACHMENT) - .image_sharing_mode(vk::SharingMode::EXCLUSIVE) - .pre_transform(self.pre_transform) - .composite_alpha(vk::CompositeAlphaFlagsKHR::OPAQUE) - .present_mode(self.present_mode) - .clipped(true) - .image_array_layers(1) - .old_swapchain( - old.as_ref() - .map_or(vk::SwapchainKHR::null(), |old| old.swapchain), - ), - None, - ) - .context("create_swapchain")?; - - if let Some(old) = old.as_ref() { - old.destroy_swapchain(device); - } - - let images = device.swapchain_ext.get_swapchain_images(swapchain)?; - let images = images - .into_iter() - .map(|image| { - let image_view = device.create_image_view( - &vk::ImageViewCreateInfo::default() - .image(image) - .view_type(vk::ImageViewType::TYPE_2D) - .format(format) - .components(vk::ComponentMapping::default()) // identity - .subresource_range(vk::ImageSubresourceRange { - aspect_mask: vk::ImageAspectFlags::COLOR, - base_mip_level: 0, - level_count: 1, - base_array_layer: 0, - layer_count: 1, - }), - None, - )?; - Ok::<_, anyhow::Error>((image, image_view)) - }) - .collect::, _>>()?; - - self.active = Some(ActiveSwapchain { - swapchain, - images, - extent, - }); - Ok(()) - } - } -} - -impl ActiveSwapchain { - /// We must destroy the image views we own, but not the images, those are owned by the swapchain. - unsafe fn destroy_image_views(&self, device: &MyDevice) { - unsafe { - for (_, image_view) in &self.images { - device.destroy_image_view(*image_view, None); - } - } - } - - /// Destroying the swapchain destroys* the images, so image views must be destroyed beforehand. - unsafe fn destroy_swapchain(&self, device: &MyDevice) { - unsafe { device.swapchain_ext.destroy_swapchain(self.swapchain, None) } - } -} - -impl Drop for MySwapchainManager { - fn drop(&mut self) { - unsafe { - self.sync.destroy(&self.device); - if let Some(active) = self.active.as_ref() { - active.destroy_image_views(&self.device); - active.destroy_swapchain(&self.device); - } - self.device.surface_ext.destroy_surface(self.surface, None); - } - } -} - -/// Metadata on drawing a single frame on some image -pub struct DrawFrame { - /// the size of the image - pub extent: vk::Extent2D, - /// the [`vk::Image`] to draw to - pub image: vk::Image, - /// the [`vk::ImageImage`] to draw to, created from `image` - pub image_view: vk::ImageView, - /// the `acquire_image` semaphore that must be waited for before draw commands are executed - pub acquire_semaphore: vk::Semaphore, - /// the `draw_finished` semaphore that must be signaled when drawing to the image has finished - pub draw_finished_semaphore: vk::Semaphore, - /// the `draw_finished` fence that must be signaled when drawing to the image has finished - pub draw_finished_fence: vk::Fence, -} - -impl MySwapchainManager { - pub fn render( - &mut self, - f: impl FnOnce(DrawFrame) -> anyhow::Result<()>, - ) -> anyhow::Result<()> { - unsafe { - self.device - .wait_for_fences(&[self.sync.render_fence], true, !0)?; - self.device.reset_fences(&[self.sync.render_fence])?; - - const RECREATE_ATTEMPTS: u32 = 10; - for _ in 0..RECREATE_ATTEMPTS { - if self.should_recreate { - self.should_recreate = false; - - // *In theory*, recreating the swapchain allows you to present any acquired images from the old - // swapchain. Which (iirc) requires you to wait for all images to be presented before destroying - // the old swapchain. But we just use the [`ash::Device::device_wait_idle`] "hack" to wait for all - // previous images to finish before immediately destroying the swapchain. - self.device.device_wait_idle()?; - self.recreate_swapchain()?; - } - - let active = self.active.as_ref().unwrap(); - let swapchain_ext = &self.device.swapchain_ext; - match swapchain_ext.acquire_next_image( - active.swapchain, - !0, - self.sync.acquire_semaphore, - vk::Fence::null(), - ) { - Ok((id, suboptimal)) => { - if suboptimal { - self.should_recreate = true; - } - let (image, image_view) = active.images[id as usize]; - f(DrawFrame { - extent: active.extent, - image, - image_view, - acquire_semaphore: self.sync.acquire_semaphore, - draw_finished_semaphore: self.sync.render_semaphore, - draw_finished_fence: self.sync.render_fence, - })?; - - let suboptimal = swapchain_ext.queue_present( - self.device.main_queue, - &vk::PresentInfoKHR::default() - .swapchains(&[active.swapchain]) - .image_indices(&[id]) - .wait_semaphores(&[self.sync.render_semaphore]), - )?; - if suboptimal { - self.should_recreate = true; - } - return Ok(()); - } - Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => { - // retry - self.should_recreate = true; - } - Err(e) => { - return Err(e.into()); - } - } - } - panic!( - "looped {} times trying to acquire swapchain image and failed repeatedly!", - RECREATE_ATTEMPTS - ); - } - } -} - -/// Manages the creation and recreation of [`MyRenderPipeline`], whenever new shader code ([`Self::set_shader_code`]) -/// is submitted or the spec constant is changed ([`Self::set_sky_fs_sun_intensity_factor`]) -pub struct MyRenderPipelineManager { - pub device: Arc, - color_out_format: vk::Format, - shader_code: Vec, - pipeline: Option, - - // Only used for sky-shader. - // NOTE(eddyb) this acts like an integration test for specialization constants. - sky_fs_spec_id_0x5007_sun_intensity_extra_spec_const_factor: u32, - should_recreate: bool, -} - -pub struct MyRenderPipeline { - pub pipeline: vk::Pipeline, - pub pipeline_layout: vk::PipelineLayout, -} - -impl MyRenderPipelineManager { - pub fn new( - device: Arc, - color_out_format: vk::Format, - shader_code: Vec, - ) -> anyhow::Result { - Ok(Self { - device, - color_out_format, - shader_code, - pipeline: None, - sky_fs_spec_id_0x5007_sun_intensity_extra_spec_const_factor: 100, - should_recreate: true, - }) - } - - #[inline] - pub fn set_sky_fs_sun_intensity_factor(&mut self, factor: u32) { - self.sky_fs_spec_id_0x5007_sun_intensity_extra_spec_const_factor = factor; - self.should_recreate(); - } - - #[inline] - pub fn get_sky_fs_sun_intensity_factor(&self) -> u32 { - self.sky_fs_spec_id_0x5007_sun_intensity_extra_spec_const_factor - } - - #[inline] - pub fn set_shader_code(&mut self, shader_code: Vec) { - self.shader_code = shader_code; - self.should_recreate(); - } - - #[inline] - pub fn should_recreate(&mut self) { - self.should_recreate = true; - } - - pub fn get_pipeline(&mut self) -> anyhow::Result<&MyRenderPipeline> { - if self.should_recreate { - self.rebuild_pipeline()?; - } - Ok(self.pipeline.as_ref().unwrap()) - } - - /// Update shaders and rebuild the pipeline - fn rebuild_pipeline(&mut self) -> anyhow::Result<()> { - unsafe { - self.destroy_pipeline()?; - - let shader_module = self.device.create_shader_module( - &vk::ShaderModuleCreateInfo::default().code(&self.shader_code), - None, - )?; - - let pipeline_layout = self.device.create_pipeline_layout( - &vk::PipelineLayoutCreateInfo::default().push_constant_ranges(&[ - vk::PushConstantRange::default() - .offset(0) - .size(size_of::() as u32) - .stage_flags(vk::ShaderStageFlags::ALL), - ]), - None, - )?; - - let mut pipelines = - self - .device - .create_graphics_pipelines(vk::PipelineCache::null(), &[vk::GraphicsPipelineCreateInfo::default() - .stages( - &[ - vk::PipelineShaderStageCreateInfo { - module: shader_module, - p_name: c"main_vs".as_ptr(), - stage: vk::ShaderStageFlags::VERTEX, - ..Default::default() - }, - vk::PipelineShaderStageCreateInfo { - module: shader_module, - p_name: c"main_fs".as_ptr(), - stage: vk::ShaderStageFlags::FRAGMENT, - // NOTE(eddyb) this acts like an integration test for specialization constants. - p_specialization_info: &vk::SpecializationInfo::default() - .map_entries(&[vk::SpecializationMapEntry::default() - .constant_id(0x5007) - .offset(0) - .size(4)]) - .data(&u32::to_le_bytes( - self.sky_fs_spec_id_0x5007_sun_intensity_extra_spec_const_factor, - )), - ..Default::default() - }, - ], - ) - .vertex_input_state(&vk::PipelineVertexInputStateCreateInfo::default()) - .input_assembly_state(&vk::PipelineInputAssemblyStateCreateInfo { - topology: vk::PrimitiveTopology::TRIANGLE_LIST, - ..Default::default() - }) - .rasterization_state(&vk::PipelineRasterizationStateCreateInfo { - front_face: vk::FrontFace::COUNTER_CLOCKWISE, - line_width: 1.0, - ..Default::default() - }) - .multisample_state(&vk::PipelineMultisampleStateCreateInfo { - rasterization_samples: vk::SampleCountFlags::TYPE_1, - ..Default::default() - }) - .depth_stencil_state(&vk::PipelineDepthStencilStateCreateInfo::default()) - .color_blend_state( - &vk::PipelineColorBlendStateCreateInfo::default() - .attachments( - &[vk::PipelineColorBlendAttachmentState { - blend_enable: 0, - src_color_blend_factor: vk::BlendFactor::SRC_COLOR, - dst_color_blend_factor: vk::BlendFactor::ONE_MINUS_DST_COLOR, - color_blend_op: vk::BlendOp::ADD, - src_alpha_blend_factor: vk::BlendFactor::ZERO, - dst_alpha_blend_factor: vk::BlendFactor::ZERO, - alpha_blend_op: vk::BlendOp::ADD, - color_write_mask: vk::ColorComponentFlags::RGBA, - }], - ), - ) - .dynamic_state( - &vk::PipelineDynamicStateCreateInfo::default() - .dynamic_states(&[vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR]), - ) - .viewport_state( - &vk::PipelineViewportStateCreateInfo::default() - .scissor_count(1) - .viewport_count(1), - ) - .layout(pipeline_layout) - .push_next(&mut vk::PipelineRenderingCreateInfo::default().color_attachment_formats(&[self.color_out_format])) - ], None).map_err(|(_, e)| e) - .context("Unable to create graphics pipeline")?; - - // A single `pipeline_info` results in a single pipeline. - assert_eq!(pipelines.len(), 1); - self.pipeline = pipelines.pop().map(|pipeline| MyRenderPipeline { - pipeline, - pipeline_layout, - }); - - // shader modules are allowed to be deleted after the pipeline has been created - self.device.destroy_shader_module(shader_module, None); - Ok(()) - } - } - - unsafe fn destroy_pipeline(&mut self) -> anyhow::Result<()> { - unsafe { - if let Some(pipeline) = self.pipeline.take() { - // Figuring out when the pipeline stops being used is hard, so we take this shortcut - self.device.device_wait_idle()?; - - self.device.destroy_pipeline(pipeline.pipeline, None); - self.device - .destroy_pipeline_layout(pipeline.pipeline_layout, None); - } - Ok(()) - } - } -} - -impl MyRenderPipeline { - pub fn render( - &self, - device: &MyDevice, - cmd: vk::CommandBuffer, - color_out: vk::ImageView, - extent: vk::Extent2D, - push_constants: ShaderConstants, - ) -> anyhow::Result<()> { - unsafe { - let render_area = vk::Rect2D { - offset: vk::Offset2D::default(), - extent, - }; - - device.cmd_begin_rendering( - cmd, - &vk::RenderingInfo::default() - .render_area(render_area) - .layer_count(1) - .color_attachments(&[vk::RenderingAttachmentInfo::default() - .image_view(color_out) - .load_op(vk::AttachmentLoadOp::CLEAR) - .store_op(vk::AttachmentStoreOp::STORE) - .clear_value(vk::ClearValue { - color: vk::ClearColorValue { - float32: [0.0, 1.0, 0.0, 0.0], - }, - }) - .image_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)]), - ); - device.cmd_bind_pipeline(cmd, vk::PipelineBindPoint::GRAPHICS, self.pipeline); - device.cmd_set_viewport( - cmd, - 0, - &[vk::Viewport { - // contains a y-flip - x: 0.0, - y: extent.height as f32, - width: extent.width as f32, - height: -(extent.height as f32), - min_depth: 0.0, - max_depth: 1.0, - }], - ); - device.cmd_set_scissor(cmd, 0, &[render_area]); - device.cmd_push_constants( - cmd, - self.pipeline_layout, - vk::ShaderStageFlags::ALL, - 0, - bytemuck::bytes_of(&push_constants), - ); - device.cmd_draw(cmd, 3, 1, 0, 0); - device.cmd_end_rendering(cmd); - Ok(()) - } - } -} - -impl Drop for MyRenderPipelineManager { - fn drop(&mut self) { - unsafe { - self.destroy_pipeline().ok(); - } - } -} - -/// The renderer manages our command buffer and submits the commands, using [`MyRenderPipeline`] for drawing. -pub struct MyRenderer { - pub device: Arc, - pub pipeline: MyRenderPipelineManager, - pub command: SingleCommandBuffer, -} - -impl MyRenderer { - pub fn new(pipeline: MyRenderPipelineManager) -> anyhow::Result { - Ok(Self { - command: SingleCommandBuffer::new(pipeline.device.clone())?, - device: pipeline.device.clone(), - pipeline, - }) - } - - pub fn render_frame( - &mut self, - frame: DrawFrame, - push_constants: ShaderConstants, - ) -> anyhow::Result<()> { - unsafe { - let device = &self.device; - let pipeline = self.pipeline.get_pipeline()?; - let cmd = self.command.cmd; - - device.reset_command_pool(self.command.pool, vk::CommandPoolResetFlags::default())?; - - { - device.begin_command_buffer( - cmd, - &vk::CommandBufferBeginInfo::default() - .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT), - )?; - device.cmd_pipeline_barrier2( - cmd, - &vk::DependencyInfo::default().image_memory_barriers(&[ - vk::ImageMemoryBarrier2::default() - .image(frame.image) - .src_access_mask(vk::AccessFlags2::NONE) - .src_stage_mask(vk::PipelineStageFlags2::ALL_COMMANDS) - .old_layout(vk::ImageLayout::UNDEFINED) - .dst_access_mask(vk::AccessFlags2::COLOR_ATTACHMENT_WRITE) - .dst_stage_mask(vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT) - .new_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL) - .subresource_range( - vk::ImageSubresourceRange::default() - .aspect_mask(vk::ImageAspectFlags::COLOR) - .base_mip_level(0) - .level_count(1) - .base_array_layer(0) - .layer_count(1), - ), - ]), - ); - pipeline.render(&device, cmd, frame.image_view, frame.extent, push_constants)?; - device.cmd_pipeline_barrier2( - cmd, - &vk::DependencyInfo::default().image_memory_barriers(&[ - vk::ImageMemoryBarrier2::default() - .image(frame.image) - .src_access_mask(vk::AccessFlags2::COLOR_ATTACHMENT_WRITE) - .src_stage_mask(vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT) - .old_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL) - .dst_access_mask(vk::AccessFlags2::NONE) - .dst_stage_mask(vk::PipelineStageFlags2::ALL_COMMANDS) - .new_layout(vk::ImageLayout::PRESENT_SRC_KHR) - .subresource_range( - vk::ImageSubresourceRange::default() - .aspect_mask(vk::ImageAspectFlags::COLOR) - .base_mip_level(0) - .level_count(1) - .base_array_layer(0) - .layer_count(1), - ), - ]), - ); - device.end_command_buffer(cmd)?; - } - - device.queue_submit2( - device.main_queue, - &[vk::SubmitInfo2::default() - .wait_semaphore_infos(&[vk::SemaphoreSubmitInfo::default() - .semaphore(frame.acquire_semaphore) - .stage_mask(vk::PipelineStageFlags2::TOP_OF_PIPE)]) - .command_buffer_infos(&[ - vk::CommandBufferSubmitInfo::default().command_buffer(cmd) - ]) - .signal_semaphore_infos(&[vk::SemaphoreSubmitInfo::default() - .semaphore(frame.draw_finished_semaphore) - .stage_mask(vk::PipelineStageFlags2::BOTTOM_OF_PIPE)])], - frame.draw_finished_fence, - )?; - Ok(()) - } - } -} - -/// A single command buffer with a pool -pub struct SingleCommandBuffer { - pub device: Arc, - pub pool: vk::CommandPool, - pub cmd: vk::CommandBuffer, -} - -impl SingleCommandBuffer { - pub fn new(device: Arc) -> anyhow::Result { - unsafe { - let pool = device.device.create_command_pool( - &vk::CommandPoolCreateInfo::default().queue_family_index(device.main_queue_family), - None, - )?; - - let command_buffers = device.device.allocate_command_buffers( - &vk::CommandBufferAllocateInfo::default() - .command_buffer_count(1) - .command_pool(pool) - .level(vk::CommandBufferLevel::PRIMARY), - )?; - assert_eq!(command_buffers.len(), 1); - let cmd = command_buffers[0]; - - Ok(Self { device, pool, cmd }) - } - } -} - -impl Drop for SingleCommandBuffer { - fn drop(&mut self) { - unsafe { - let device = &self.device; - device.free_command_buffers(self.pool, &[self.cmd]); - device.destroy_command_pool(self.pool, None); - } - } -} - -unsafe extern "system" fn vulkan_debug_callback( - message_severity: vk::DebugUtilsMessageSeverityFlagsEXT, - _message_type: vk::DebugUtilsMessageTypeFlagsEXT, - p_callback_data: *const vk::DebugUtilsMessengerCallbackDataEXT<'_>, - _user_data: *mut std::os::raw::c_void, -) -> vk::Bool32 { - let callback_data = unsafe { *p_callback_data }; - let message_id_name = if callback_data.p_message_id_name.is_null() { - Cow::from("") - } else { - unsafe { CStr::from_ptr(callback_data.p_message_id_name).to_string_lossy() } - }; - let message = if callback_data.p_message.is_null() { - Cow::from("") - } else { - unsafe { CStr::from_ptr(callback_data.p_message).to_string_lossy() } - }; - println!("{message_severity:?}: [{message_id_name}] : {message}"); - vk::FALSE -} diff --git a/examples/runners/ash/src/single_command_buffer.rs b/examples/runners/ash/src/single_command_buffer.rs new file mode 100644 index 0000000000..15137a1bd8 --- /dev/null +++ b/examples/runners/ash/src/single_command_buffer.rs @@ -0,0 +1,42 @@ +use crate::device::MyDevice; +use ash::vk; +use std::sync::Arc; + +/// A single command buffer with a pool +pub struct SingleCommandBuffer { + pub device: Arc, + pub pool: vk::CommandPool, + pub cmd: vk::CommandBuffer, +} + +impl SingleCommandBuffer { + pub fn new(device: Arc) -> anyhow::Result { + unsafe { + let pool = device.device.create_command_pool( + &vk::CommandPoolCreateInfo::default().queue_family_index(device.main_queue_family), + None, + )?; + + let command_buffers = device.device.allocate_command_buffers( + &vk::CommandBufferAllocateInfo::default() + .command_buffer_count(1) + .command_pool(pool) + .level(vk::CommandBufferLevel::PRIMARY), + )?; + assert_eq!(command_buffers.len(), 1); + let cmd = command_buffers[0]; + + Ok(Self { device, pool, cmd }) + } + } +} + +impl Drop for SingleCommandBuffer { + fn drop(&mut self) { + unsafe { + let device = &self.device; + device.free_command_buffers(self.pool, &[self.cmd]); + device.destroy_command_pool(self.pool, None); + } + } +} diff --git a/examples/runners/ash/src/swapchain.rs b/examples/runners/ash/src/swapchain.rs new file mode 100644 index 0000000000..90b96d4eb1 --- /dev/null +++ b/examples/runners/ash/src/swapchain.rs @@ -0,0 +1,348 @@ +use crate::device::MyDevice; +use anyhow::Context; +use ash::vk; +use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; +use std::sync::Arc; + +/// A binary semaphore for swapchain operations +pub struct SwapchainSync { + acquire_semaphore: vk::Semaphore, + render_semaphore: vk::Semaphore, + render_fence: vk::Fence, +} + +impl SwapchainSync { + pub unsafe fn new(device: &MyDevice) -> anyhow::Result { + unsafe { + let signaled_fence = + vk::FenceCreateInfo::default().flags(vk::FenceCreateFlags::SIGNALED); + Ok(Self { + acquire_semaphore: device + .create_semaphore(&vk::SemaphoreCreateInfo::default(), None)?, + render_semaphore: device + .create_semaphore(&vk::SemaphoreCreateInfo::default(), None)?, + render_fence: device.create_fence(&signaled_fence, None)?, + }) + } + } + + pub unsafe fn destroy(&self, device: &MyDevice) { + unsafe { + device.destroy_semaphore(self.acquire_semaphore, None); + device.destroy_semaphore(self.render_semaphore, None); + device.destroy_fence(self.render_fence, None); + } + } +} + +/// Takes care of all things swapchain related +/// +/// Intentionally kept simple and does not offer support for multiple frames in flight +pub struct MySwapchainManager { + pub device: Arc, + pub window: winit::window::Window, + pub surface: vk::SurfaceKHR, + pub surface_format: vk::SurfaceFormatKHR, + pub surface_capabilities: vk::SurfaceCapabilitiesKHR, + pub present_mode: vk::PresentModeKHR, + pub image_count: u32, + pub pre_transform: vk::SurfaceTransformFlagsKHR, + + // state below + active: Option, + should_recreate: bool, + sync: SwapchainSync, +} + +struct ActiveSwapchain { + extent: vk::Extent2D, + swapchain: vk::SwapchainKHR, + images: Vec<(vk::Image, vk::ImageView)>, +} + +impl MySwapchainManager { + pub fn new(device: Arc, window: winit::window::Window) -> anyhow::Result { + unsafe { + let surface_ext = &device.surface_ext; + + let surface = ash_window::create_surface( + &device.entry, + &device.instance, + window.display_handle().unwrap().into(), + window.window_handle().unwrap().into(), + None, + ) + .context("create_surface")?; + + let surface_format = { + let acceptable_formats = { + [ + vk::Format::R8G8B8_SRGB, + vk::Format::B8G8R8_SRGB, + vk::Format::R8G8B8A8_SRGB, + vk::Format::B8G8R8A8_SRGB, + vk::Format::A8B8G8R8_SRGB_PACK32, + ] + }; + *surface_ext + .get_physical_device_surface_formats(device.physical_device, surface)? + .iter() + .find(|sfmt| acceptable_formats.contains(&sfmt.format)) + .context("Unable to find suitable surface format.")? + }; + + let surface_capabilities = surface_ext + .get_physical_device_surface_capabilities(device.physical_device, surface)?; + let pre_transform = surface_capabilities.current_transform; + + let present_mode = surface_ext + .get_physical_device_surface_present_modes(device.physical_device, surface)? + .iter() + .cloned() + // Mailbox is preferred + .find(|&mode| mode == vk::PresentModeKHR::MAILBOX) + // FIFO is guaranteed to be available + .unwrap_or(vk::PresentModeKHR::FIFO); + + let image_count = { + let mut image_count = match present_mode { + // tripple buffering in mailbox mode:: one presenting, one ready and one drawing + vk::PresentModeKHR::MAILBOX => 3, + // double buffering in fifo mode: one presenting, one drawing + vk::PresentModeKHR::FIFO => 2, + _ => unreachable!(), + }; + if surface_capabilities.max_image_count != 0 { + image_count = image_count.min(surface_capabilities.max_image_count); + } + image_count.max(surface_capabilities.min_image_count) + }; + + let sync = SwapchainSync::new(&device)?; + Ok(Self { + device, + window, + surface, + surface_format, + surface_capabilities, + present_mode, + image_count, + pre_transform, + + active: None, + should_recreate: true, + sync, + }) + } + } + + #[inline] + pub fn should_recreate(&mut self) { + self.should_recreate = true; + } + + /// After this function is called, `Self.active` is initialized + unsafe fn recreate_swapchain(&mut self) -> anyhow::Result<()> { + unsafe { + let device = &self.device; + let swapchain_ext = &device.swapchain_ext; + let surface_ext = &self.device.surface_ext; + let format = self.surface_format.format; + + let extent = { + let window_size = self.window.inner_size(); + let capabilities = surface_ext.get_physical_device_surface_capabilities( + self.device.physical_device, + self.surface, + )?; + let min = capabilities.min_image_extent; + let max = capabilities.max_image_extent; + vk::Extent2D { + width: u32::clamp(window_size.width, min.width, max.width), + height: u32::clamp(window_size.height, min.height, max.height), + } + }; + + let old = self.active.take(); + if let Some(old) = old.as_ref() { + old.destroy_image_views(device); + } + + let swapchain = swapchain_ext + .create_swapchain( + &vk::SwapchainCreateInfoKHR::default() + .surface(self.surface) + .min_image_count(self.image_count) + .image_color_space(self.surface_format.color_space) + .image_format(format) + .image_extent(extent) + .image_usage(vk::ImageUsageFlags::COLOR_ATTACHMENT) + .image_sharing_mode(vk::SharingMode::EXCLUSIVE) + .pre_transform(self.pre_transform) + .composite_alpha(vk::CompositeAlphaFlagsKHR::OPAQUE) + .present_mode(self.present_mode) + .clipped(true) + .image_array_layers(1) + .old_swapchain( + old.as_ref() + .map_or(vk::SwapchainKHR::null(), |old| old.swapchain), + ), + None, + ) + .context("create_swapchain")?; + + if let Some(old) = old.as_ref() { + old.destroy_swapchain(device); + } + + let images = device.swapchain_ext.get_swapchain_images(swapchain)?; + let images = images + .into_iter() + .map(|image| { + let image_view = device.create_image_view( + &vk::ImageViewCreateInfo::default() + .image(image) + .view_type(vk::ImageViewType::TYPE_2D) + .format(format) + .components(vk::ComponentMapping::default()) // identity + .subresource_range(vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + base_mip_level: 0, + level_count: 1, + base_array_layer: 0, + layer_count: 1, + }), + None, + )?; + Ok::<_, anyhow::Error>((image, image_view)) + }) + .collect::, _>>()?; + + self.active = Some(ActiveSwapchain { + swapchain, + images, + extent, + }); + Ok(()) + } + } +} + +impl ActiveSwapchain { + /// We must destroy the image views we own, but not the images, those are owned by the swapchain. + unsafe fn destroy_image_views(&self, device: &MyDevice) { + unsafe { + for (_, image_view) in &self.images { + device.destroy_image_view(*image_view, None); + } + } + } + + /// Destroying the swapchain destroys* the images, so image views must be destroyed beforehand. + unsafe fn destroy_swapchain(&self, device: &MyDevice) { + unsafe { device.swapchain_ext.destroy_swapchain(self.swapchain, None) } + } +} + +impl Drop for MySwapchainManager { + fn drop(&mut self) { + unsafe { + self.sync.destroy(&self.device); + if let Some(active) = self.active.as_ref() { + active.destroy_image_views(&self.device); + active.destroy_swapchain(&self.device); + } + self.device.surface_ext.destroy_surface(self.surface, None); + } + } +} + +/// Metadata on drawing a single frame on some image +pub struct DrawFrame { + /// the size of the image + pub extent: vk::Extent2D, + /// the [`vk::Image`] to draw to + pub image: vk::Image, + /// the [`vk::ImageImage`] to draw to, created from `image` + pub image_view: vk::ImageView, + /// the `acquire_image` semaphore that must be waited for before draw commands are executed + pub acquire_semaphore: vk::Semaphore, + /// the `draw_finished` semaphore that must be signaled when drawing to the image has finished + pub draw_finished_semaphore: vk::Semaphore, + /// the `draw_finished` fence that must be signaled when drawing to the image has finished + pub draw_finished_fence: vk::Fence, +} + +impl MySwapchainManager { + pub fn render( + &mut self, + f: impl FnOnce(DrawFrame) -> anyhow::Result<()>, + ) -> anyhow::Result<()> { + unsafe { + self.device + .wait_for_fences(&[self.sync.render_fence], true, !0)?; + self.device.reset_fences(&[self.sync.render_fence])?; + + const RECREATE_ATTEMPTS: u32 = 10; + for _ in 0..RECREATE_ATTEMPTS { + if self.should_recreate { + self.should_recreate = false; + + // *In theory*, recreating the swapchain allows you to present any acquired images from the old + // swapchain. Which (iirc) requires you to wait for all images to be presented before destroying + // the old swapchain. But we just use the [`ash::Device::device_wait_idle`] "hack" to wait for all + // previous images to finish before immediately destroying the swapchain. + self.device.device_wait_idle()?; + self.recreate_swapchain()?; + } + + let active = self.active.as_ref().unwrap(); + let swapchain_ext = &self.device.swapchain_ext; + match swapchain_ext.acquire_next_image( + active.swapchain, + !0, + self.sync.acquire_semaphore, + vk::Fence::null(), + ) { + Ok((id, suboptimal)) => { + if suboptimal { + self.should_recreate = true; + } + let (image, image_view) = active.images[id as usize]; + f(DrawFrame { + extent: active.extent, + image, + image_view, + acquire_semaphore: self.sync.acquire_semaphore, + draw_finished_semaphore: self.sync.render_semaphore, + draw_finished_fence: self.sync.render_fence, + })?; + + let suboptimal = swapchain_ext.queue_present( + self.device.main_queue, + &vk::PresentInfoKHR::default() + .swapchains(&[active.swapchain]) + .image_indices(&[id]) + .wait_semaphores(&[self.sync.render_semaphore]), + )?; + if suboptimal { + self.should_recreate = true; + } + return Ok(()); + } + Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => { + // retry + self.should_recreate = true; + } + Err(e) => { + return Err(e.into()); + } + } + } + panic!( + "looped {} times trying to acquire swapchain image and failed repeatedly!", + RECREATE_ATTEMPTS + ); + } + } +} From def773507f3ca00e4b2a0d3b6f5ecb9a512b62d7 Mon Sep 17 00:00:00 2001 From: firestar99 Date: Tue, 30 Sep 2025 16:44:15 +0200 Subject: [PATCH 08/12] ash runner: fix lints --- examples/runners/ash/src/graphics.rs | 2 +- examples/runners/ash/src/swapchain.rs | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/examples/runners/ash/src/graphics.rs b/examples/runners/ash/src/graphics.rs index 58bfc970a3..c5e1431114 100644 --- a/examples/runners/ash/src/graphics.rs +++ b/examples/runners/ash/src/graphics.rs @@ -314,7 +314,7 @@ impl MyRenderer { ), ]), ); - pipeline.render(&device, cmd, frame.image_view, frame.extent, push_constants)?; + pipeline.render(device, cmd, frame.image_view, frame.extent, push_constants)?; device.cmd_pipeline_barrier2( cmd, &vk::DependencyInfo::default().image_memory_barriers(&[ diff --git a/examples/runners/ash/src/swapchain.rs b/examples/runners/ash/src/swapchain.rs index 90b96d4eb1..a3d703c649 100644 --- a/examples/runners/ash/src/swapchain.rs +++ b/examples/runners/ash/src/swapchain.rs @@ -5,14 +5,14 @@ use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; use std::sync::Arc; /// A binary semaphore for swapchain operations -pub struct SwapchainSync { +struct SwapchainSync { acquire_semaphore: vk::Semaphore, render_semaphore: vk::Semaphore, render_fence: vk::Fence, } impl SwapchainSync { - pub unsafe fn new(device: &MyDevice) -> anyhow::Result { + unsafe fn new(device: &MyDevice) -> anyhow::Result { unsafe { let signaled_fence = vk::FenceCreateInfo::default().flags(vk::FenceCreateFlags::SIGNALED); @@ -26,7 +26,7 @@ impl SwapchainSync { } } - pub unsafe fn destroy(&self, device: &MyDevice) { + unsafe fn destroy(&self, device: &MyDevice) { unsafe { device.destroy_semaphore(self.acquire_semaphore, None); device.destroy_semaphore(self.render_semaphore, None); @@ -263,7 +263,7 @@ pub struct DrawFrame { pub extent: vk::Extent2D, /// the [`vk::Image`] to draw to pub image: vk::Image, - /// the [`vk::ImageImage`] to draw to, created from `image` + /// the [`vk::Image`] to draw to, created from `image` pub image_view: vk::ImageView, /// the `acquire_image` semaphore that must be waited for before draw commands are executed pub acquire_semaphore: vk::Semaphore, @@ -340,8 +340,7 @@ impl MySwapchainManager { } } panic!( - "looped {} times trying to acquire swapchain image and failed repeatedly!", - RECREATE_ATTEMPTS + "looped {RECREATE_ATTEMPTS} times trying to acquire swapchain image and failed repeatedly!" ); } } From c1d2b480613aaeb7baf27cb887dcfba9260b95d0 Mon Sep 17 00:00:00 2001 From: firestar99 Date: Wed, 1 Oct 2025 14:56:23 +0200 Subject: [PATCH 09/12] ash & wgpu runner: remove build script forwarding `PROFILE` env var no longer necessary since d416b65572279c8bd5d1b7df9c04800d62061db5 --- examples/runners/ash/build.rs | 8 -------- examples/runners/wgpu/build.rs | 6 ++---- examples/runners/wgpu/src/lib.rs | 11 ----------- 3 files changed, 2 insertions(+), 23 deletions(-) delete mode 100644 examples/runners/ash/build.rs diff --git a/examples/runners/ash/build.rs b/examples/runners/ash/build.rs deleted file mode 100644 index 5aa3309e79..0000000000 --- a/examples/runners/ash/build.rs +++ /dev/null @@ -1,8 +0,0 @@ -use std::env; - -fn main() { - // While OUT_DIR is set for both build.rs and compiling the crate, PROFILE is only set in - // build.rs. So, export it to crate compilation as well. - let profile = env::var("PROFILE").unwrap(); - println!("cargo:rustc-env=PROFILE={profile}"); -} diff --git a/examples/runners/wgpu/build.rs b/examples/runners/wgpu/build.rs index 55f708728d..edbed7a88e 100644 --- a/examples/runners/wgpu/build.rs +++ b/examples/runners/wgpu/build.rs @@ -8,13 +8,11 @@ fn main() -> Result<(), Box> { println!("cargo:rerun-if-changed=build.rs"); println!("cargo:rerun-if-env-changed=CARGO_CFG_TARGET_OS"); println!("cargo:rerun-if-env-changed=CARGO_CFG_TARGET_ARCH"); - // While OUT_DIR is set for both build.rs and compiling the crate, PROFILE is only set in - // build.rs. So, export it to crate compilation as well. - let profile = env::var("PROFILE").unwrap(); - println!("cargo:rustc-env=PROFILE={profile}"); + if target_os != "android" && target_arch != "wasm32" { return Ok(()); } + let profile = env::var("PROFILE").unwrap(); let mut dir = PathBuf::from(env::var_os("OUT_DIR").unwrap()); // Strip `$profile/build/*/out`. let ok = dir.ends_with("out") diff --git a/examples/runners/wgpu/src/lib.rs b/examples/runners/wgpu/src/lib.rs index da3c705598..1d42f46a57 100644 --- a/examples/runners/wgpu/src/lib.rs +++ b/examples/runners/wgpu/src/lib.rs @@ -236,17 +236,6 @@ pub fn main( #[cfg(not(any(target_os = "android", target_arch = "wasm32")))] { - // Hack: spirv_builder builds into a custom directory if running under cargo, to not - // deadlock, and the default target directory if not. However, packages like `proc-macro2` - // have different configurations when being built here vs. when building - // rustc_codegen_spirv normally, so we *want* to build into a separate target directory, to - // not have to rebuild half the crate graph every time we run. So, pretend we're running - // under cargo by setting these environment variables. - unsafe { - std::env::set_var("OUT_DIR", env!("OUT_DIR")); - std::env::set_var("PROFILE", env!("PROFILE")); - } - if options.shader == RustGPUShader::Compute { return compute::start(&options); } From 71c633075586c06898e267ccd24dc8321b0b0e54 Mon Sep 17 00:00:00 2001 From: firestar99 Date: Wed, 1 Oct 2025 14:59:00 +0200 Subject: [PATCH 10/12] ash runner: remove `ash-molten`, require vulkan sdk on macos --- Cargo.lock | 88 +----------------------------- examples/README.md | 1 + examples/runners/ash/Cargo.toml | 3 - examples/runners/ash/src/device.rs | 8 +-- 4 files changed, 3 insertions(+), 97 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fd855795b7..e15d40352b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -194,18 +194,6 @@ dependencies = [ "libloading", ] -[[package]] -name = "ash-molten" -version = "0.20.0+1.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b4c4960621009149a2877457f6875185b894fe1b4f7a602167a3ac00758dc40" -dependencies = [ - "anyhow", - "ash", - "plist", - "serde", -] - [[package]] name = "ash-window" version = "0.13.0" @@ -728,15 +716,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" -[[package]] -name = "deranged" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" -dependencies = [ - "powerfmt", -] - [[package]] name = "derive_more" version = "0.99.20" @@ -936,7 +915,6 @@ version = "0.0.0" dependencies = [ "anyhow", "ash", - "ash-molten", "ash-window", "bytemuck", "cfg-if", @@ -2037,12 +2015,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - [[package]] name = "num-traits" version = "0.2.19" @@ -2476,19 +2448,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" -[[package]] -name = "plist" -version = "1.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" -dependencies = [ - "base64", - "indexmap 2.11.0", - "quick-xml 0.38.3", - "serde", - "time", -] - [[package]] name = "polling" version = "3.10.0" @@ -2527,12 +2486,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - [[package]] name = "pp-rs" version = "0.2.1" @@ -2591,15 +2544,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "quick-xml" -version = "0.38.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" -dependencies = [ - "memchr", -] - [[package]] name = "quote" version = "1.0.40" @@ -3417,36 +3361,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "time" -version = "0.3.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83bde6f1ec10e72d583d91623c939f623002284ef622b87de38cfd546cbf2031" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" - -[[package]] -name = "time-macros" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" -dependencies = [ - "num-conv", - "time-core", -] - [[package]] name = "tiny-skia" version = "0.11.4" @@ -4061,7 +3975,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" dependencies = [ "proc-macro2", - "quick-xml 0.37.5", + "quick-xml", "quote", ] diff --git a/examples/README.md b/examples/README.md index c90060e415..3b420400e0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -47,6 +47,7 @@ supported. on WGPU+wasm.) - ash runner: + - MacOS: requires MoltenVK (or the Vulkan SDK) to be installed - `cargo run --release -p example-runner-ash` runs the sky shader. Use the up and down arrows to adjust the sun's intensity. Use F5 to recompile the shader code (but note that the image won't redraw afterwards unless the intensity is diff --git a/examples/runners/ash/Cargo.toml b/examples/runners/ash/Cargo.toml index 0fa5fb6104..f25777d9f6 100644 --- a/examples/runners/ash/Cargo.toml +++ b/examples/runners/ash/Cargo.toml @@ -24,6 +24,3 @@ shared = { path = "../../shaders/shared" } spirv-builder = { workspace = true, default-features = false } anyhow = "1.0.98" bytemuck.workspace = true - -[target.'cfg(target_os = "macos")'.dependencies] -ash-molten = { version = "0.20", features = ["pre-built"] } diff --git a/examples/runners/ash/src/device.rs b/examples/runners/ash/src/device.rs index 9f658ad465..ec1beecb7f 100644 --- a/examples/runners/ash/src/device.rs +++ b/examples/runners/ash/src/device.rs @@ -32,13 +32,7 @@ impl Deref for MyDevice { impl MyDevice { pub fn new(extension_names: &[*const c_char], options: &Options) -> anyhow::Result> { unsafe { - cfg_if::cfg_if! { - if #[cfg(target_os = "macos")] { - let entry = ash_molten::load(); - } else { - let entry = ash::Entry::load()?; - } - } + let entry = ash::Entry::load()?; let instance = { let layer_names: &'static [_] = if options.debug_layer { From 3007d258f1b11f2c928b408a558710b154727f05 Mon Sep 17 00:00:00 2001 From: firestar99 Date: Wed, 1 Oct 2025 12:28:30 +0200 Subject: [PATCH 11/12] ash runner: switch Vulkan Memory Model from extension to core feature --- examples/runners/ash/src/device.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/runners/ash/src/device.rs b/examples/runners/ash/src/device.rs index ec1beecb7f..fd2af61370 100644 --- a/examples/runners/ash/src/device.rs +++ b/examples/runners/ash/src/device.rs @@ -115,7 +115,7 @@ impl MyDevice { physical_device, &vk::DeviceCreateInfo::default() .push_next( - &mut vk::PhysicalDeviceVulkanMemoryModelFeatures::default() + &mut vk::PhysicalDeviceVulkan12Features::default() .vulkan_memory_model(true), ) .push_next( From fb61b79127469e691e326eed696ede4e01d52d70 Mon Sep 17 00:00:00 2001 From: firestar99 Date: Thu, 2 Oct 2025 12:44:08 +0200 Subject: [PATCH 12/12] ash runner: cleanup debug callback --- examples/runners/ash/src/device.rs | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/examples/runners/ash/src/device.rs b/examples/runners/ash/src/device.rs index fd2af61370..cd409419f0 100644 --- a/examples/runners/ash/src/device.rs +++ b/examples/runners/ash/src/device.rs @@ -169,17 +169,16 @@ unsafe extern "system" fn vulkan_debug_callback( p_callback_data: *const vk::DebugUtilsMessengerCallbackDataEXT<'_>, _user_data: *mut std::os::raw::c_void, ) -> vk::Bool32 { - let callback_data = unsafe { *p_callback_data }; - let message_id_name = if callback_data.p_message_id_name.is_null() { - Cow::from("") - } else { - unsafe { CStr::from_ptr(callback_data.p_message_id_name).to_string_lossy() } - }; - let message = if callback_data.p_message.is_null() { - Cow::from("") - } else { - unsafe { CStr::from_ptr(callback_data.p_message).to_string_lossy() } - }; - println!("{message_severity:?}: [{message_id_name}] : {message}"); - vk::FALSE + unsafe { + let callback_data = *p_callback_data; + let message_id_name = callback_data + .message_id_name_as_c_str() + .map_or(Cow::Borrowed(""), CStr::to_string_lossy); + let message = callback_data + .message_as_c_str() + .map_or(Cow::Borrowed(""), CStr::to_string_lossy); + + println!("{message_severity:?}: [{message_id_name}] : {message}"); + vk::FALSE + } }