diff --git a/Cargo.lock b/Cargo.lock index fdc5a17b1c..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" @@ -934,9 +913,10 @@ dependencies = [ name = "example-runner-ash" version = "0.0.0" dependencies = [ + "anyhow", "ash", - "ash-molten", "ash-window", + "bytemuck", "cfg-if", "clap", "raw-window-handle 0.6.2", @@ -2035,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" @@ -2474,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" @@ -2525,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" @@ -2589,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" @@ -3415,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" @@ -4059,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 ea00020feb..f25777d9f6 100644 --- a/examples/runners/ash/Cargo.toml +++ b/examples/runners/ash/Cargo.toml @@ -22,6 +22,5 @@ clap = { version = "4", features = ["derive"] } cfg-if = "1.0.0" shared = { path = "../../shaders/shared" } spirv-builder = { workspace = true, default-features = false } - -[target.'cfg(target_os = "macos")'.dependencies] -ash-molten = { version = "0.20", features = ["pre-built"] } +anyhow = "1.0.98" +bytemuck.workspace = true 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/ash/src/device.rs b/examples/runners/ash/src/device.rs new file mode 100644 index 0000000000..cd409419f0 --- /dev/null +++ b/examples/runners/ash/src/device.rs @@ -0,0 +1,184 @@ +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 { + 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::PhysicalDeviceVulkan12Features::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 { + 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 + } +} diff --git a/examples/runners/ash/src/graphics.rs b/examples/runners/ash/src/graphics.rs new file mode 100644 index 0000000000..c5e1431114 --- /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 1551c22901..7f97c281ad 100644 --- a/examples/runners/ash/src/main.rs +++ b/examples/runners/ash/src/main.rs @@ -70,29 +70,31 @@ // crate-specific exceptions: // #![allow()] -use ash::{ext, khr, util::read_spv, vk}; - -use raw_window_handle::{HasDisplayHandle as _, HasWindowHandle as _}; -use winit::{ - event::{Event, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, -}; - +use crate::device::MyDevice; +use crate::graphics::{MyRenderPipelineManager, MyRenderer}; +use crate::swapchain::MySwapchainManager; +use anyhow::{Context, anyhow}; +use ash::util::read_spv; +use clap::{Parser, ValueEnum}; +use raw_window_handle::HasDisplayHandle as _; +use shared::ShaderConstants; +use spirv_builder::{MetadataPrintout, SpirvBuilder}; use std::{ - borrow::Cow, - ffi::{CStr, CString}, fs::File, - os::raw::c_char, path::PathBuf, - sync::mpsc::{TryRecvError, TrySendError, sync_channel}, + sync::mpsc::{TryRecvError, sync_channel}, thread, }; +use winit::event_loop::ActiveEventLoop; +use winit::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, +}; -use clap::{Parser, ValueEnum}; - -use spirv_builder::{MetadataPrintout, SpirvBuilder}; - -use shared::ShaderConstants; +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)] @@ -123,1230 +125,153 @@ pub struct Options { debug_layer: bool, } -pub fn main() { - // 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")); - } - +pub fn main() -> anyhow::Result<()> { let options = Options::parse(); - let (vert_data, frag_data) = compile_shaders(&options.shader); + let shader_code = compile_shaders(&options.shader)?; // runtime setup - let event_loop = EventLoop::new().unwrap(); - // 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 = 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 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)] - event_loop - .run(move |event, event_loop_window_target| match event { + 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((new_vert_data, new_frag_data)) => { - ctx.update_shader_modules(&new_vert_data, &new_frag_data); - ctx.recompiling_shaders = false; - ctx.rebuild_pipeline(vk::PipelineCache::null()); + Err(TryRecvError::Empty) => (), + Ok(shader_code) => { + recompiling_shaders = false; + renderer.pipeline.set_shader_code(shader_code); } Err(TryRecvError::Disconnected) => { - panic!("compiler receiver disconnected unexpectedly"); + return Err(anyhow!("compiler receiver disconnected unexpectedly")); } }; + + 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 { - 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)) - { - 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 !recompiling_shaders { + recompiling_shaders = true; + let compiler_sender = compiler_sender.clone(); + thread::spawn(move || { + 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 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) + }; + renderer.pipeline.set_sky_fs_sun_intensity_factor(factor); + } + _ => {} + }, + WindowEvent::Resized(_) => { + swapchain.should_recreate(); } - 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(vk::PipelineCache::null()); - } + 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::Wait), - }) - .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) -> (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.3") .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::>(); + .build()?; + let spv_path = compile_result.module.unwrap_single(); - // 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. + // 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) -} - -pub struct RenderBase { - pub window: winit::window::Window, - - pub entry: ash::Entry, - - pub instance: ash::Instance, - 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 present_queue: vk::Queue, - - pub surface: vk::SurfaceKHR, - pub surface_loader: khr::surface::Instance, - pub surface_format: vk::SurfaceFormatKHR, -} - -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()); - } - - 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_create_info = vk::InstanceCreateInfo::default() - .application_info(&appinfo) - .enabled_layer_names(&layers_names_raw) - .enabled_extension_names(&extension_names_raw); - - unsafe { - entry - .create_instance(&instance_create_info, None) - .expect("Instance creation error") - } - }; - - let surface = unsafe { - 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)); - - unsafe { - debug_utils_loader - .create_debug_utils_messenger(&debug_info, None) - .unwrap() - } - }; - - (Some(debug_utils_loader), Some(debug_call_back)) - } else { - (None, None) - }; - - let surface_loader = khr::surface::Instance::new(&entry, &instance); - - let (pdevice, queue_family_index) = unsafe { - instance - .enumerate_physical_devices() - .expect("Physical device error") - .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 - } - }) - }) - .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() - } - }; - - let swapchain_loader = khr::swapchain::Device::new(&instance, &device); - - let present_queue = unsafe { 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 { - *surface_loader - .get_physical_device_surface_formats(pdevice, surface) - .unwrap() - .iter() - .find(|sfmt| acceptable_formats.contains(&sfmt.format)) - .expect("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, - } - } - - 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, - } - } - - pub fn surface_capabilities(&self) -> vk::SurfaceCapabilitiesKHR { - unsafe { - self.surface_loader - .get_physical_device_surface_capabilities(self.pdevice, self.surface) - .unwrap() - } - } - - 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() - .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) - } - - pub fn create_image_views(&self, swapchain: vk::SwapchainKHR) -> Vec { - unsafe { - self.swapchain_loader - .get_swapchain_images(swapchain) - .unwrap() - .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() - }) - .collect() - } - } - - pub fn create_framebuffers( - &self, - image_views: &[vk::ImageView], - render_pass: vk::RenderPass, - extent: vk::Extent2D, - ) -> Vec { - 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() - } - }) - .collect() - } - - 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); - unsafe { - self.device - .create_render_pass(&renderpass_create_info, None) - .unwrap() - } - } - - pub fn create_render_sync(&self) -> RenderSync { - RenderSync::new(self) - } - - pub fn into_ctx(self) -> RenderCtx { - RenderCtx::from_base(self) - } -} - -impl Drop for RenderBase { - 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.instance.destroy_instance(None); - } - } -} - -pub struct RenderCtx { - pub base: RenderBase, - pub sync: RenderSync, - - pub swapchain: vk::SwapchainKHR, - 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 pipeline: Option, - pub vert_module: Option, - pub frag_module: Option, - - pub rendering_paused: bool, - pub recompiling_shaders: bool, - pub start: std::time::Instant, - - // 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, -} - -impl RenderCtx { - pub fn from_base(base: RenderBase) -> Self { - 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, - }]), - ) - }; - - Self { - sync, - base, - swapchain, - extent, - image_views, - commands, - render_pass, - framebuffers, - viewports, - scissors, - pipeline: None, - vert_module: None, - frag_module: None, - rendering_paused: false, - recompiling_shaders: false, - start: std::time::Instant::now(), - - sky_fs_spec_id_0x5007_sun_intensity_extra_spec_const_factor: 100, - } - } - - 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); - unsafe { - self.base - .device - .create_pipeline_layout( - &vk::PipelineLayoutCreateInfo::default() - .push_constant_ranges(&[push_constant_range]), - None, - ) - .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 - .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, - }); - } - - pub fn cleanup_pipeline(&mut self) { - 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); - } - } - } - - /// 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 { - 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) { - let surface_resolution = self.base.surface_resolution(); - - if surface_resolution.width == 0 || surface_resolution.height == 0 { - self.rendering_paused = true; - return; - } 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, - }]); - } - - pub fn render(&mut self) { - 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; - } - Err(err) => panic!("failed to acquire next image: {err:?}"), - } - }; - - 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.present_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:?}"), - }; - } - } - - 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, - any_as_u8_slice(&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( - &self, - wait_mask: &[vk::PipelineStageFlags], - f: F, - ) { - 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.present_queue, - &[submit_info], - self.sync.draw_commands_reuse_fence, - ) - .expect("queue submit failed."); - } - } -} - -impl Drop for RenderCtx { - 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.base - .device - .destroy_shader_module(self.vert_module.unwrap(), None); - self.base - .device - .destroy_shader_module(self.frag_module.unwrap(), None); - } - } -} - -pub struct RenderSync { - pub present_complete_semaphore: vk::Semaphore, - pub rendering_complete_semaphore: vk::Semaphore, - pub draw_commands_reuse_fence: vk::Fence, -} - -impl RenderSync { - pub fn new(base: &RenderBase) -> Self { - 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(); - - Self { - present_complete_semaphore, - rendering_complete_semaphore, - draw_commands_reuse_fence, - } - } - } -} - -pub struct RenderCommandPool { - pub pool: vk::CommandPool, - pub draw_command_buffer: 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], - } - } -} - -pub struct Pipeline { - pub pipeline: vk::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::()) - } -} - -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_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, - ); + assert!(shaders.contains(&"main_vs".to_string())); + assert!(shaders.contains(&"main_fs".to_string())); - vk::FALSE + Ok(read_spv(&mut File::open(spv_path)?)?) } 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..a3d703c649 --- /dev/null +++ b/examples/runners/ash/src/swapchain.rs @@ -0,0 +1,347 @@ +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 +struct SwapchainSync { + acquire_semaphore: vk::Semaphore, + render_semaphore: vk::Semaphore, + render_fence: vk::Fence, +} + +impl SwapchainSync { + 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)?, + }) + } + } + + 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::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, + /// 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 {RECREATE_ATTEMPTS} times trying to acquire swapchain image and failed repeatedly!" + ); + } + } +} 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); }