diff --git a/Cargo.lock b/Cargo.lock index b9bd4860..d8fb8986 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,12 +8,33 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "ash" +version = "0.37.3+1.3.251" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e9c3835d686b0a6084ab4234fcd1b07dbf6e4767dce60874b12356a25ecd4a" +dependencies = [ + "libloading", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + [[package]] name = "bytemuck" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "convert_case" version = "0.4.0" @@ -98,6 +119,22 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + [[package]] name = "longest-increasing-subsequence" version = "0.1.0" @@ -110,6 +147,30 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "portable-atomic" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" + [[package]] name = "proc-macro2" version = "1.0.86" @@ -215,6 +276,17 @@ dependencies = [ "smallvec", ] +[[package]] +name = "spirt-vk-layer" +version = "0.0.0" +dependencies = [ + "ash", + "bytemuck", + "lazy_static", + "spirt", + "vulkan-layer", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -232,8 +304,77 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "unicode-ident" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "vulkan-layer" +version = "0.1.0" +source = "git+https://github.com/google/vk-layer-for-rust?rev=a236360#a2363607922d97a4dfc5b62d05c54848c73d671c" +dependencies = [ + "ash", + "bytemuck", + "cfg-if", + "log", + "num-traits", + "once_cell", + "rustc_version", + "smallvec", + "thiserror", + "vulkan-layer-macros", +] + +[[package]] +name = "vulkan-layer-macros" +version = "0.1.0" +source = "git+https://github.com/google/vk-layer-for-rust?rev=a236360#a2363607922d97a4dfc5b62d05c54848c73d671c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index cb48495f..58eb221d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,6 @@ +[workspace] +members = ["vk-layer"] + [package] name = "spirt" description = "Shader-focused IR to target, transform and translate from." diff --git a/src/cf/structurize.rs b/src/cf/structurize.rs index d8ffa811..14a12674 100644 --- a/src/cf/structurize.rs +++ b/src/cf/structurize.rs @@ -991,24 +991,29 @@ impl<'a> Structurizer<'a> { .children .insert_last(loop_node, &mut self.func_def_body.nodes); - // HACK(eddyb) we've treated loop exits as extra "false edges", so - // here they have to be added to the loop (potentially unlocking - // structurization to the outside of the loop, in the caller). - if let Some(exit_targets) = self.loop_header_to_exit_targets.get(&target) { - for &exit_target in exit_targets { - // FIXME(eddyb) what if this is `None`, is that impossible? - if let Some(exit_edge_bundle) = deferred_edges - .get_edge_bundle_mut_by_target(DeferredTarget::Region(exit_target)) - { - exit_edge_bundle.accumulated_count += IncomingEdgeCount::ONE; - } - } - } - wrapper_region } else { target }; + + // HACK(eddyb) we've treated loop exits as extra "false edges", so + // here they have to be added to the loop (potentially unlocking + // structurization to the outside of the loop, in the caller). + // + // NOTE(eddyb) this is applied in all cases, not just loops, because + // SPIR-V allows `OpLoopMerge` to exist without a reachable backedge, + // and without this adjustment, structurization would remain incomplete. + if let Some(exit_targets) = self.loop_header_to_exit_targets.get(&target) { + for &exit_target in exit_targets { + // FIXME(eddyb) what if this is `None`, is that impossible? + if let Some(exit_edge_bundle) = deferred_edges + .get_edge_bundle_mut_by_target(DeferredTarget::Region(exit_target)) + { + exit_edge_bundle.accumulated_count += IncomingEdgeCount::ONE; + } + } + } + let IncomingEdgeBundle { attrs, target: _, accumulated_count: _, target_inputs } = edge_bundle; diff --git a/vk-layer/Cargo.toml b/vk-layer/Cargo.toml new file mode 100644 index 00000000..c6c3370e --- /dev/null +++ b/vk-layer/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "spirt-vk-layer" +description = "Vulkan layer injecting SPIR-T passes into the Vulkan CTS/apps/etc." +version = "0.0.0" +edition = "2021" +publish = false + +[lib] +name = 'VkLayer_spirt_shaders' +crate-type = ["cdylib"] + +[dependencies] +spirt = { path = ".." } + +ash = "0.37.2" +vulkan-layer = { git = "https://github.com/google/vk-layer-for-rust", rev = "a236360" } + +# FIXME(eddyb) deduplicate these by the workspace inheritance feature. +bytemuck = "1.12.3" +lazy_static = "1.4.0" diff --git a/vk-layer/manifests/target_debug_lib.so/VkLayer_spirt_shaders.json b/vk-layer/manifests/target_debug_lib.so/VkLayer_spirt_shaders.json new file mode 100644 index 00000000..639a872d --- /dev/null +++ b/vk-layer/manifests/target_debug_lib.so/VkLayer_spirt_shaders.json @@ -0,0 +1,15 @@ +{ + "_comments": [ + "HACK(eddyb) the weird directory this JSON is in encodes the `library_path` choice", + "FIXME(eddyb) these JSON files should be auto-generated instead (via artifact deps?)" + ], + "file_format_version": "1.2.1", + "layer": { + "name": "VK_LAYER_SPIRT_shaders", + "type": "INSTANCE", + "library_path": "../../../target/debug/libVkLayer_spirt_shaders.so", + "api_version": "1.3.0", + "implementation_version": "0", + "description": "SPIR-T-based shader rewriter layer" + } +} diff --git a/vk-layer/manifests/target_i686-unknown-linux-gnu_debug_lib.so/VkLayer_spirt_shaders.json b/vk-layer/manifests/target_i686-unknown-linux-gnu_debug_lib.so/VkLayer_spirt_shaders.json new file mode 100644 index 00000000..b3094634 --- /dev/null +++ b/vk-layer/manifests/target_i686-unknown-linux-gnu_debug_lib.so/VkLayer_spirt_shaders.json @@ -0,0 +1,15 @@ +{ + "_comments": [ + "HACK(eddyb) the weird directory this JSON is in encodes the `library_path` choice", + "FIXME(eddyb) these JSON files should be auto-generated instead (via artifact deps?)" + ], + "file_format_version": "1.2.1", + "layer": { + "name": "VK_LAYER_SPIRT_shaders", + "type": "INSTANCE", + "library_path": "../../../target/i686-unknown-linux-gnu/debug/libVkLayer_spirt_shaders.so", + "api_version": "1.3.0", + "implementation_version": "0", + "description": "SPIR-T-based shader rewriter layer" + } +} diff --git a/vk-layer/manifests/target_i686-unknown-linux-gnu_release_lib.so/VkLayer_spirt_shaders.json b/vk-layer/manifests/target_i686-unknown-linux-gnu_release_lib.so/VkLayer_spirt_shaders.json new file mode 100644 index 00000000..10892ab0 --- /dev/null +++ b/vk-layer/manifests/target_i686-unknown-linux-gnu_release_lib.so/VkLayer_spirt_shaders.json @@ -0,0 +1,15 @@ +{ + "_comments": [ + "HACK(eddyb) the weird directory this JSON is in encodes the `library_path` choice", + "FIXME(eddyb) these JSON files should be auto-generated instead (via artifact deps?)" + ], + "file_format_version": "1.2.1", + "layer": { + "name": "VK_LAYER_SPIRT_shaders", + "type": "INSTANCE", + "library_path": "../../../target/i686-unknown-linux-gnu/release/libVkLayer_spirt_shaders.so", + "api_version": "1.3.0", + "implementation_version": "0", + "description": "SPIR-T-based shader rewriter layer" + } +} diff --git a/vk-layer/manifests/target_release_lib.so/VkLayer_spirt_shaders.json b/vk-layer/manifests/target_release_lib.so/VkLayer_spirt_shaders.json new file mode 100644 index 00000000..1a24b1e0 --- /dev/null +++ b/vk-layer/manifests/target_release_lib.so/VkLayer_spirt_shaders.json @@ -0,0 +1,15 @@ +{ + "_comments": [ + "HACK(eddyb) the weird directory this JSON is in encodes the `library_path` choice", + "FIXME(eddyb) these JSON files should be auto-generated instead (via artifact deps?)" + ], + "file_format_version": "1.2.1", + "layer": { + "name": "VK_LAYER_SPIRT_shaders", + "type": "INSTANCE", + "library_path": "../../../target/release/libVkLayer_spirt_shaders.so", + "api_version": "1.3.0", + "implementation_version": "0", + "description": "SPIR-T-based shader rewriter layer" + } +} diff --git a/vk-layer/src/lib.rs b/vk-layer/src/lib.rs new file mode 100644 index 00000000..fc49c100 --- /dev/null +++ b/vk-layer/src/lib.rs @@ -0,0 +1,7 @@ +//! Vulkan layer injecting SPIR-T passes into the Vulkan CTS/apps/etc. + +// HACK(eddyb) this is really only about the crate itself. +#![allow(non_snake_case)] + +#[warn(non_snake_case)] +mod shaders_layer; diff --git a/vk-layer/src/shaders_layer.rs b/vk-layer/src/shaders_layer.rs new file mode 100644 index 00000000..3b716115 --- /dev/null +++ b/vk-layer/src/shaders_layer.rs @@ -0,0 +1,258 @@ +//! Vulkan layer that can rewrite SPIR-V shaders passed to Vulkan commands. + +use ash::{self, prelude::*, vk}; +use lazy_static::lazy_static; +use std::rc::Rc; +use std::sync::Arc; +use vulkan_layer::DeviceHooks; +use vulkan_layer::LayerResult; +use vulkan_layer::auto_deviceinfo_impl; +use vulkan_layer::{ + Global, Layer, LayerManifest, StubGlobalHooks, StubInstanceInfo, declare_introspection_queries, +}; + +#[derive(Default)] +struct Config { + verbose: bool, + spv_copy: bool, + spirt_passes: Option>, +} + +lazy_static! { + static ref CONFIG: Config = { + let var_name = "VK_LAYER_SPIRT_CONFIG"; + let var_contents = std::env::var(var_name) + .unwrap_or_else(|e| panic!("env var `{var_name}` is required: {e:?}")); + + let mut config = Config::default(); + for part in var_contents.split('+') { + if part == "verbose" { + config.verbose = true; + } else if part == "spv-copy" { + config.spv_copy = true; + } else if let Some(passes) = part.strip_prefix("spirt-passes=") { + config.spirt_passes = Some( + passes.split(',').filter(|s| !s.is_empty()).map(|s| s.to_string()).collect(), + ); + } else { + panic!( + "`{var_name}` should contain `+`-separated `verbose`/`spv-copy`/`spirt-passes=...`" + ); + } + } + if config.spv_copy == config.spirt_passes.is_some() { + panic!("`{var_name}` should have exactly one of `spv-copy` or `spirt-passes=...`"); + } + config + }; +} + +#[derive(Default)] +struct ShadersLayer(StubGlobalHooks); + +struct ShadersDeviceHooks { + config: &'static Config, + spv_spec: &'static spirt::spv::spec::Spec, + device_as_next: Arc, +} + +impl Layer for ShadersLayer { + type GlobalHooksInfo = StubGlobalHooks; + type InstanceInfo = StubInstanceInfo; + type DeviceInfo = ShadersDeviceHooks; + type InstanceInfoContainer = Self::InstanceInfo; + type DeviceInfoContainer = Self::DeviceInfo; + + fn global_instance() -> &'static Global { + lazy_static! { + static ref GLOBAL: Global = Default::default(); + } + &GLOBAL + } + + fn manifest() -> LayerManifest { + let mut manifest = LayerManifest::default(); + manifest.name = "VK_LAYER_SPIRT_shaders"; + manifest.spec_version = vk::API_VERSION_1_3; + manifest.description = "SPIR-T-based shader rewriter layer"; + manifest + } + + fn global_hooks_info(&self) -> &Self::GlobalHooksInfo { + &self.0 + } + + fn create_instance_info( + &self, + _: &vk::InstanceCreateInfo, + _: Option<&vk::AllocationCallbacks>, + _: Arc, + _next_get_instance_proc_addr: vk::PFN_vkGetInstanceProcAddr, + ) -> Self::InstanceInfoContainer { + Default::default() + } + + fn create_device_info( + &self, + _: vk::PhysicalDevice, + _: &vk::DeviceCreateInfo, + _: Option<&vk::AllocationCallbacks>, + device_as_next: Arc, + _next_get_device_proc_addr: vk::PFN_vkGetDeviceProcAddr, + ) -> ShadersDeviceHooks { + ShadersDeviceHooks { + config: &CONFIG, + spv_spec: spirt::spv::spec::Spec::get(), + device_as_next, + } + } +} + +declare_introspection_queries!(Global::); + +// FIXME(eddyb) stop abusing `io::Error` for error reporting. +fn invalid(reason: &str) -> std::io::Error { + std::io::Error::new(std::io::ErrorKind::InvalidData, format!("malformed SPIR-V ({reason})")) +} + +impl ShadersDeviceHooks { + fn timed_pass(&self, name: &str, f: impl FnOnce() -> R) -> R { + if !self.config.verbose { + return f(); + } + + let start = std::time::Instant::now(); + let r = f(); + eprintln!("[{:8.3}ms] {name}", start.elapsed().as_secs_f64() * 1000.0); + r + } + + fn process_shader(&self, in_spv_words: &[u32]) -> std::io::Result> { + if self.config.spv_copy { + if !in_spv_words.starts_with(&[self.spv_spec.magic]) { + return Err(invalid("does not start with SPIR-V magic number")); + } + return Ok(in_spv_words.to_vec()); + } + let spirt_passes = self.config.spirt_passes.as_ref().unwrap(); + + if self.config.verbose { + eprintln!(); + } + + // FIXME(eddyb) there should be no conversion necessary to use `&[u32]`. + let mut module = self.timed_pass("Module::lower_from_spv_bytes", || { + spirt::Module::lower_from_spv_bytes( + Rc::new(spirt::Context::new()), + bytemuck::cast_slice(in_spv_words).to_vec(), + ) + })?; + + // FIXME(eddyb) this should be its own + if self.config.verbose { + let (printed_deps, printed_exports) = + spirt::print::Plan::for_module(&module).pretty_print_deps_and_root_separately(); + let printed_deps = printed_deps.to_string(); + let printed_deps_lines = printed_deps.lines().collect::>(); + const CONTEXT: usize = 100; + eprintln!("```spirt"); + if printed_deps_lines.len() <= CONTEXT * 2 { + eprintln!("{}", printed_deps_lines.join("\n")); + } else { + eprintln!("{}", printed_deps_lines[..CONTEXT].join("\n")); + eprintln!("··· {} lines skipped ···", printed_deps_lines.len() - CONTEXT * 2); + eprintln!( + "{}", + printed_deps_lines[printed_deps_lines.len() - CONTEXT..].join("\n") + ); + } + eprintln!("{printed_exports}"); + eprintln!("```"); + } + + self.timed_pass("legalize::structurize_func_cfgs", || { + spirt::passes::legalize::structurize_func_cfgs(&mut module) + }); + + for pass in spirt_passes { + match &pass[..] { + // HACK(eddyb) testing aid (e.g. triggering validation error). + "hollow" => module.exports.clear(), + "qptr" => { + let layout_config = &spirt::mem::LayoutConfig { + abstract_bool_size_align: (1, 1), + logical_ptr_size_align: (8, 8), + ..spirt::mem::LayoutConfig::VULKAN_SCALAR_LAYOUT + }; + + self.timed_pass("qptr::lower_from_spv_ptrs", || { + spirt::passes::qptr::lower_from_spv_ptrs(&mut module, layout_config) + }); + self.timed_pass("mem::analyze_accesses", || { + spirt::passes::qptr::analyze_mem_accesses(&mut module, layout_config) + }); + self.timed_pass("qptr::lift_to_spv_ptrs", || { + spirt::passes::qptr::lift_to_spv_ptrs(&mut module, layout_config) + }); + } + _ => unimplemented!("unknown pass {pass}"), + } + } + + let out_spv_module_emitter = self.timed_pass("Module::lift_to_spv_module_emitter", || { + module.lift_to_spv_module_emitter() + }); + Ok(out_spv_module_emitter.unwrap().words) + } + + // FIXME(eddyb) indicate somehow that this is `nounwind`. + fn with_processed_shader_ffi_safe( + &self, + vk_cmd_name: &str, + in_spv_bytes: &[u8], + f: impl FnOnce(&[u8]) -> VkResult, + ) -> VkResult { + // FIXME(eddyb) should panic vs error return different Vulkan error codes? + let out_spv_words = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let in_spv_words = bytemuck::cast_slice::(in_spv_bytes); + self.process_shader(in_spv_words).map_err(|e| -> vk::Result { + eprintln!("[VK_LAYER_SPIRT_shaders] ERROR in {vk_cmd_name}: {e}"); + vk::Result::ERROR_INITIALIZATION_FAILED + }) + })) + .map_err(|_| { + // FIXME(eddyb) should the global panic handler be overriden? + eprintln!("[VK_LAYER_SPIRT_shaders] ^^^^^^ PANIC in {vk_cmd_name} ^^^^^^"); + vk::Result::ERROR_INITIALIZATION_FAILED + })??; + + f(bytemuck::cast_slice::(&out_spv_words)) + } +} + +#[auto_deviceinfo_impl] +impl DeviceHooks for ShadersDeviceHooks { + fn create_shader_module( + &self, + create_info: &vk::ShaderModuleCreateInfo, + allocation_callbacks: Option<&vk::AllocationCallbacks>, + ) -> LayerResult> { + let mut create_info = *create_info; + let in_spv_bytes = unsafe { + std::slice::from_raw_parts(create_info.p_code as *const u8, create_info.code_size) + }; + LayerResult::Handled(self.with_processed_shader_ffi_safe( + "vkCreateShaderModule", + in_spv_bytes, + |out_spv_bytes| { + // FIXME(eddyb) can we use the builder here somehow? + create_info.p_code = out_spv_bytes.as_ptr() as *const u32; + create_info.code_size = out_spv_bytes.len(); + + unsafe { + self.device_as_next.create_shader_module(&create_info, allocation_callbacks) + } + }, + )) + } +}