diff --git a/CHANGELOG.md b/CHANGELOG.md index 8df0d6f200..01c5f55c63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added ⭐ +- [PR#1036](https://github.com/EmbarkStudios/rust-gpu/pull/1036) added a `--force-spirv-passthru` flag to `example-runner-wgpu`, to bypass Naga (`wgpu`'s shader translator), + used it to test `debugPrintf` for `wgpu`, and updated `ShaderPanicStrategy::DebugPrintfThenExit` docs to reflect what "enabling `debugPrintf`" looks like for `wgpu` + (e.g. `VK_LOADER_LAYERS_ENABLE=VK_LAYER_KHRONOS_validation VK_LAYER_ENABLES=VK_VALIDATION_FEATURE_ENABLE_DEBUG_PRINTF_EXT DEBUG_PRINTF_TO_STDOUT=1`) - [PR#1080](https://github.com/EmbarkStudios/rust-gpu/pull/1080) added `debugPrintf`-based panic reporting, with the desired behavior selected via `spirv_builder::ShaderPanicStrategy` (see its documentation for more details about each available panic handling strategy) diff --git a/crates/rustc_codegen_spirv/src/builder/builder_methods.rs b/crates/rustc_codegen_spirv/src/builder/builder_methods.rs index 4fc502f083..430650b482 100644 --- a/crates/rustc_codegen_spirv/src/builder/builder_methods.rs +++ b/crates/rustc_codegen_spirv/src/builder/builder_methods.rs @@ -2648,7 +2648,7 @@ impl<'a, 'tcx> BuilderMethods<'a, 'tcx> for Builder<'a, 'tcx> { // HACK(eddyb) redirect any possible panic call to an abort, to avoid // needing to materialize `&core::panic::Location` or `format_args!`. // FIXME(eddyb) find a way to extract the original message. - self.abort_with_message("panic!(...)".into()); + self.abort_with_message("panicked: ".into()); self.undef(result_type) } else if let Some(mode) = buffer_load_intrinsic { self.codegen_buffer_load_intrinsic(result_type, args, mode) diff --git a/crates/rustc_codegen_spirv/src/builder/intrinsics.rs b/crates/rustc_codegen_spirv/src/builder/intrinsics.rs index 9f05f03cdb..b94475c5dc 100644 --- a/crates/rustc_codegen_spirv/src/builder/intrinsics.rs +++ b/crates/rustc_codegen_spirv/src/builder/intrinsics.rs @@ -339,7 +339,7 @@ impl<'a, 'tcx> IntrinsicCallMethods<'tcx> for Builder<'a, 'tcx> { } fn abort(&mut self) { - self.abort_with_message("intrinsics::abort()".into()); + self.abort_with_message("aborted: intrinsics::abort() called".into()); } fn assume(&mut self, _val: Self::Value) { diff --git a/crates/rustc_codegen_spirv/src/linker/spirt_passes/controlflow.rs b/crates/rustc_codegen_spirv/src/linker/spirt_passes/controlflow.rs index 5ced285a19..e22c94beab 100644 --- a/crates/rustc_codegen_spirv/src/linker/spirt_passes/controlflow.rs +++ b/crates/rustc_codegen_spirv/src/linker/spirt_passes/controlflow.rs @@ -330,12 +330,16 @@ pub fn convert_custom_aborts_to_unstructured_returns_in_entry_points( let mut fmt = String::new(); // HACK(eddyb) this improves readability w/ very verbose Vulkan loggers. - fmt += "\n "; + fmt += "\n"; + fmt += "[RUST-GPU] "; + fmt += &cx[const_str(message)].replace('%', "%%"); + + // FIXME(eddyb) deduplicate with "called at" form below + // (not trivial becasue both closures would borrow `fmt`). if let Some((file, line, col)) = current_debug_src_loc.take() { - fmt += &format!("{file}:{line}:{col}: ").replace('%', "%%"); + fmt += &format!("\n at {file}:{line}:{col}").replace('%', "%%"); } - fmt += &cx[const_str(message)].replace('%', "%%"); let mut innermost = true; let mut append_call = |callsite_debug_src_loc, callee: &str| { @@ -360,6 +364,8 @@ pub fn convert_custom_aborts_to_unstructured_returns_in_entry_points( } append_call(None, &debug_printf_context_fmt_str); + fmt += "\n"; + let abort_inst_def = &mut func_def_body.data_insts[abort_inst]; abort_inst_def.kind = DataInstKind::SpvExtInst { ext_set: cx.intern("NonSemantic.DebugPrintf"), diff --git a/crates/spirv-builder/src/lib.rs b/crates/spirv-builder/src/lib.rs index ed17a22ae0..5f516e3edd 100644 --- a/crates/spirv-builder/src/lib.rs +++ b/crates/spirv-builder/src/lib.rs @@ -193,15 +193,40 @@ pub enum ShaderPanicStrategy { /// If you have multiple entry-points, you *may* need to also enable the /// `multimodule` node (see ). /// - /// **Note**: actually obtaining the `debugPrintf` output requires enabling: - /// * `VK_KHR_shader_non_semantic_info` Vulkan *Device* extension - /// * Vulkan Validation Layers (which contain the `debugPrintf` implementation) - /// * `VK_VALIDATION_FEATURE_ENABLE_DEBUG_PRINTF_EXT` in Validation Layers, - /// either by using `VkValidationFeaturesEXT` during instance creating, - /// setting the `VK_LAYER_ENABLES` environment variable to that value, - /// or adding it to `khronos_validation.enables` in `vk_layer_settings.txt` - /// - /// See also: . + /// **Note**: actually obtaining the `debugPrintf` output requires: + /// * Vulkan Validation Layers (from e.g. the Vulkan SDK) + /// * (they contain the `debugPrintf` implementation, a SPIR-V -> SPIR-V translation) + /// * **set the `VK_LOADER_LAYERS_ENABLE=VK_LAYER_KHRONOS_validation` + /// environment variable** to easily enable them without any code changes + /// * alternatively, `"VK_LAYER_KHRONOS_validation"` can be passed during + /// instance creation, to enable them programmatically + /// * Validation Layers' `debugPrintf` support: + /// * **set the `VK_LAYER_ENABLES=VK_VALIDATION_FEATURE_ENABLE_DEBUG_PRINTF_EXT` + /// environment variable** to easily enable the `debugPrintf` support + /// * alternatively, `VkValidationFeaturesEXT` during instance creation, + /// or the `khronos_validation.enables` field in `vk_layer_settings.txt`, + /// can be used to enable `VK_VALIDATION_FEATURE_ENABLE_DEBUG_PRINTF_EXT` + /// (see also ) + /// * for outputting the `debugPrintf` messages sent back from the GPU: + /// * **set the `DEBUG_PRINTF_TO_STDOUT=1` environment variable** if you don't + /// plan on customizing the reporting (see below for alternatives) + /// * for `wgpu`: + /// * **required**: `wgpu::Features::SPIRV_SHADER_PASSTHROUGH` (Naga lacks `debugPrintf`) + /// * *optional*: building in debug mode (and/or with debug-assertions enabled), + /// to enable `wgpu` logging/debug support + /// * (the debug assertions requirement may be lifted in future `wgpu` versions) + /// * this uses `VK_EXT_debug_utils` internally, and is a better-integrated + /// alternative to just setting `DEBUG_PRINTF_TO_STDOUT=1` + /// * `RUST_LOG=wgpu_hal::vulkan=info` (or equivalent) will enable said + /// output (as `debugPrintf` messages have the "info" level) + /// * `RUST_LOG` controls `env_logger`, which isn't itself required, + /// but *some* `log`/`tracing` subscriber is needed to get any output + /// * for Vulkan (e.g. via `ash`): + /// * **required**: enabling the `VK_KHR_shader_non_semantic_info` Vulkan *Device* extension + /// * *optional*: as described above, enabling the Validation Layers and + /// their `debugPrintf` support can be done during instance creation + /// * *optional*: integrating [`VK_EXT_debug_utils`](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VK_EXT_debug_utils.html) + /// allows more reporting flexibility than `DEBUG_PRINTF_TO_STDOUT=1`) DebugPrintfThenExit { /// Whether to also print the entry-point inputs (excluding buffers/resources), /// which should uniquely identify the panicking shader invocation. diff --git a/examples/runners/ash/src/main.rs b/examples/runners/ash/src/main.rs index 8e65bd6a26..9ff4430b60 100644 --- a/examples/runners/ash/src/main.rs +++ b/examples/runners/ash/src/main.rs @@ -211,8 +211,6 @@ pub fn compile_shaders() -> Vec { SpirvBuilder::new("examples/shaders/sky-shader", "spirv-unknown-vulkan1.1") .print_metadata(MetadataPrintout::None) - // HACK(eddyb) having the `ash` runner do this is the easiest way I found - // to test this `panic!` feature with actual `debugPrintf` support. .shader_panic_strategy(spirv_builder::ShaderPanicStrategy::DebugPrintfThenExit { print_inputs: true, print_backtrace: true, @@ -380,10 +378,10 @@ impl RenderBase { }; let device: ash::Device = { - let mut device_extension_names_raw = vec![khr::Swapchain::name().as_ptr()]; - if options.debug_layer { - device_extension_names_raw.push(vk::KhrShaderNonSemanticInfoFn::name().as_ptr()); - } + let device_extension_names_raw = [ + khr::Swapchain::name().as_ptr(), + vk::KhrShaderNonSemanticInfoFn::name().as_ptr(), + ]; let features = vk::PhysicalDeviceFeatures { shader_clip_distance: 1, ..Default::default() diff --git a/examples/runners/wgpu/src/compute.rs b/examples/runners/wgpu/src/compute.rs index 6736642003..8e706e0bc5 100644 --- a/examples/runners/wgpu/src/compute.rs +++ b/examples/runners/wgpu/src/compute.rs @@ -1,20 +1,17 @@ -use wgpu::util::DeviceExt; +use crate::{maybe_watch, CompiledShaderModules, Options}; -use super::Options; use std::{convert::TryInto, time::Duration}; +use wgpu::util::DeviceExt; pub fn start(options: &Options) { env_logger::init(); - let shader_binary = crate::maybe_watch(options.shader, None); + let compiled_shader_modules = maybe_watch(options, None); - futures::executor::block_on(start_internal(options, shader_binary)); + futures::executor::block_on(start_internal(options, compiled_shader_modules)); } -pub async fn start_internal( - _options: &Options, - shader_binary: wgpu::ShaderModuleDescriptor<'static>, -) { +async fn start_internal(options: &Options, compiled_shader_modules: CompiledShaderModules) { let backends = wgpu::util::backend_bits_from_env().unwrap_or(wgpu::Backends::PRIMARY); let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { backends, @@ -24,11 +21,17 @@ pub async fn start_internal( .await .expect("Failed to find an appropriate adapter"); + let mut features = + wgpu::Features::TIMESTAMP_QUERY | wgpu::Features::TIMESTAMP_QUERY_INSIDE_PASSES; + if options.force_spirv_passthru { + features |= wgpu::Features::SPIRV_SHADER_PASSTHROUGH; + } + let (device, queue) = adapter .request_device( &wgpu::DeviceDescriptor { label: None, - features: wgpu::Features::TIMESTAMP_QUERY, + features, limits: wgpu::Limits::default(), }, None, @@ -40,8 +43,19 @@ pub async fn start_internal( let timestamp_period = queue.get_timestamp_period(); - // Load the shaders from disk - let module = device.create_shader_module(shader_binary); + let entry_point = "main_cs"; + + // FIXME(eddyb) automate this decision by default. + let module = compiled_shader_modules.spv_module_for_entry_point(entry_point); + let module = if options.force_spirv_passthru { + unsafe { device.create_shader_module_spirv(&module) } + } else { + let wgpu::ShaderModuleDescriptorSpirV { label, source } = module; + device.create_shader_module(wgpu::ShaderModuleDescriptor { + label, + source: wgpu::ShaderSource::SpirV(source), + }) + }; let top = 2u32.pow(20); let src_range = 1..top; @@ -75,7 +89,7 @@ pub async fn start_internal( label: None, layout: Some(&pipeline_layout), module: &module, - entry_point: "main_cs", + entry_point, }); let readback_buffer = device.create_buffer(&wgpu::BufferDescriptor { @@ -97,10 +111,17 @@ pub async fn start_internal( let timestamp_buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("Timestamps buffer"), size: 16, + usage: wgpu::BufferUsages::QUERY_RESOLVE | wgpu::BufferUsages::COPY_SRC, + mapped_at_creation: false, + }); + + let timestamp_readback_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: None, + size: 16, usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, mapped_at_creation: true, }); - timestamp_buffer.unmap(); + timestamp_readback_buffer.unmap(); let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: None, @@ -137,10 +158,17 @@ pub async fn start_internal( src.len() as wgpu::BufferAddress, ); encoder.resolve_query_set(&queries, 0..2, ×tamp_buffer, 0); + encoder.copy_buffer_to_buffer( + ×tamp_buffer, + 0, + ×tamp_readback_buffer, + 0, + timestamp_buffer.size(), + ); queue.submit(Some(encoder.finish())); let buffer_slice = readback_buffer.slice(..); - let timestamp_slice = timestamp_buffer.slice(..); + let timestamp_slice = timestamp_readback_buffer.slice(..); timestamp_slice.map_async(wgpu::MapMode::Read, |r| r.unwrap()); buffer_slice.map_async(wgpu::MapMode::Read, |r| r.unwrap()); // NOTE(eddyb) `poll` should return only after the above callbacks fire @@ -160,7 +188,7 @@ pub async fn start_internal( drop(data); readback_buffer.unmap(); drop(timing_data); - timestamp_buffer.unmap(); + timestamp_readback_buffer.unmap(); let mut max = 0; for (src, out) in src_range.zip(result.iter().copied()) { if out == u32::MAX { diff --git a/examples/runners/wgpu/src/graphics.rs b/examples/runners/wgpu/src/graphics.rs index 9a5bd43263..5d1cb202e5 100644 --- a/examples/runners/wgpu/src/graphics.rs +++ b/examples/runners/wgpu/src/graphics.rs @@ -1,6 +1,5 @@ -use crate::maybe_watch; +use crate::{maybe_watch, CompiledShaderModules, Options}; -use super::Options; use shared::ShaderConstants; use winit::{ event::{ElementState, Event, KeyboardInput, MouseButton, VirtualKeyCode, WindowEvent}, @@ -33,9 +32,10 @@ fn mouse_button_index(button: MouseButton) -> usize { } async fn run( - event_loop: EventLoop>, + options: Options, + event_loop: EventLoop, window: Window, - shader_binary: wgpu::ShaderModuleDescriptor<'static>, + compiled_shader_modules: CompiledShaderModules, ) { let backends = wgpu::util::backend_bits_from_env() .unwrap_or(wgpu::Backends::VULKAN | wgpu::Backends::METAL); @@ -69,7 +69,10 @@ async fn run( .await .expect("Failed to find an appropriate adapter"); - let features = wgpu::Features::PUSH_CONSTANTS; + let mut features = wgpu::Features::PUSH_CONSTANTS; + if options.force_spirv_passthru { + features |= wgpu::Features::SPIRV_SHADER_PASSTHROUGH; + } let limits = wgpu::Limits { max_push_constant_size: 128, ..Default::default() @@ -124,13 +127,14 @@ async fn run( }); let mut render_pipeline = create_pipeline( + &options, &device, &pipeline_layout, surface_with_config.as_ref().map_or_else( |pending| pending.preferred_format, |(_, surface_config)| surface_config.format, ), - shader_binary, + compiled_shader_modules, ); let start = std::time::Instant::now(); @@ -309,6 +313,7 @@ async fn run( } Event::UserEvent(new_module) => { *render_pipeline = create_pipeline( + &options, &device, &pipeline_layout, surface_with_config.as_ref().map_or_else( @@ -326,18 +331,49 @@ async fn run( } fn create_pipeline( + options: &Options, device: &wgpu::Device, pipeline_layout: &wgpu::PipelineLayout, surface_format: wgpu::TextureFormat, - shader_binary: wgpu::ShaderModuleDescriptor<'_>, + compiled_shader_modules: CompiledShaderModules, ) -> wgpu::RenderPipeline { - let module = device.create_shader_module(shader_binary); + // FIXME(eddyb) automate this decision by default. + let create_module = |module| { + if options.force_spirv_passthru { + unsafe { device.create_shader_module_spirv(&module) } + } else { + let wgpu::ShaderModuleDescriptorSpirV { label, source } = module; + device.create_shader_module(wgpu::ShaderModuleDescriptor { + label, + source: wgpu::ShaderSource::SpirV(source), + }) + } + }; + + let vs_entry_point = shaders::main_vs; + let fs_entry_point = shaders::main_fs; + + let vs_module_descr = compiled_shader_modules.spv_module_for_entry_point(vs_entry_point); + let fs_module_descr = compiled_shader_modules.spv_module_for_entry_point(fs_entry_point); + + // HACK(eddyb) avoid calling `device.create_shader_module` twice unnecessarily. + let vs_fs_same_module = std::ptr::eq(&vs_module_descr.source[..], &fs_module_descr.source[..]); + + let vs_module = &create_module(vs_module_descr); + let fs_module; + let fs_module = if vs_fs_same_module { + vs_module + } else { + fs_module = create_module(fs_module_descr); + &fs_module + }; + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: None, layout: Some(pipeline_layout), vertex: wgpu::VertexState { - module: &module, - entry_point: shaders::main_vs, + module: vs_module, + entry_point: vs_entry_point, buffers: &[], }, primitive: wgpu::PrimitiveState { @@ -356,8 +392,8 @@ fn create_pipeline( alpha_to_coverage_enabled: false, }, fragment: Some(wgpu::FragmentState { - module: &module, - entry_point: shaders::main_fs, + module: fs_module, + entry_point: fs_entry_point, targets: &[Some(wgpu::ColorTargetState { format: surface_format, blend: None, @@ -394,7 +430,7 @@ pub fn start( // Build the shader before we pop open a window, since it might take a while. let initial_shader = maybe_watch( - options.shader, + options, #[cfg(not(any(target_os = "android", target_arch = "wasm32")))] { let proxy = event_loop.create_proxy(); @@ -425,16 +461,17 @@ pub fn start( }) .expect("couldn't append canvas to document body"); wasm_bindgen_futures::spawn_local(run( + options.clone(), event_loop, window, initial_shader, )); } else { futures::executor::block_on(run( + options.clone(), event_loop, window, initial_shader, - )); } } diff --git a/examples/runners/wgpu/src/lib.rs b/examples/runners/wgpu/src/lib.rs index 4bdb7cd9cc..2fe3fa7bcf 100644 --- a/examples/runners/wgpu/src/lib.rs +++ b/examples/runners/wgpu/src/lib.rs @@ -70,6 +70,7 @@ // crate-specific exceptions: // #![allow()] +use std::borrow::Cow; use structopt::StructOpt; use strum::{Display, EnumString}; @@ -87,16 +88,45 @@ pub enum RustGPUShader { Mouse, } +struct CompiledShaderModules { + named_spv_modules: Vec<(Option, wgpu::ShaderModuleDescriptorSpirV<'static>)>, +} + +impl CompiledShaderModules { + fn spv_module_for_entry_point<'a>( + &'a self, + wanted_entry: &str, + ) -> wgpu::ShaderModuleDescriptorSpirV<'a> { + for (name, spv_module) in &self.named_spv_modules { + match name { + Some(name) if name != wanted_entry => continue, + _ => { + return wgpu::ShaderModuleDescriptorSpirV { + label: name.as_deref(), + source: Cow::Borrowed(&spv_module.source), + }; + } + } + } + unreachable!( + "{wanted_entry:?} not found in modules {:?}", + self.named_spv_modules + .iter() + .map(|(name, _)| name) + .collect::>() + ); + } +} + fn maybe_watch( - shader: RustGPUShader, + options: &Options, #[cfg(not(any(target_os = "android", target_arch = "wasm32")))] on_watch: Option< - Box) + Send + 'static>, + Box, >, -) -> wgpu::ShaderModuleDescriptor<'static> { +) -> CompiledShaderModules { #[cfg(not(any(target_os = "android", target_arch = "wasm32")))] { use spirv_builder::{CompileResult, MetadataPrintout, SpirvBuilder}; - use std::borrow::Cow; use std::path::PathBuf; // 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` @@ -106,7 +136,7 @@ fn maybe_watch( // under cargo by setting these environment variables. std::env::set_var("OUT_DIR", env!("OUT_DIR")); std::env::set_var("PROFILE", env!("PROFILE")); - let crate_name = match shader { + let crate_name = match options.shader { RustGPUShader::Simplest => "simplest-shader", RustGPUShader::Sky => "sky-shader", RustGPUShader::Compute => "compute-shader", @@ -117,8 +147,22 @@ fn maybe_watch( .iter() .copied() .collect::(); + + let has_debug_printf = options.force_spirv_passthru; + let builder = SpirvBuilder::new(crate_path, "spirv-unknown-vulkan1.1") - .print_metadata(MetadataPrintout::None); + .print_metadata(MetadataPrintout::None) + .shader_panic_strategy(if has_debug_printf { + spirv_builder::ShaderPanicStrategy::DebugPrintfThenExit { + print_inputs: true, + print_backtrace: true, + } + } else { + spirv_builder::ShaderPanicStrategy::SilentExit + }) + // HACK(eddyb) needed because of `debugPrintf` instrumentation limitations + // (see https://github.com/KhronosGroup/SPIRV-Tools/issues/4892). + .multimodule(has_debug_printf); let initial_result = if let Some(mut f) = on_watch { builder .watch(move |compile_result| f(handle_compile_result(compile_result))) @@ -126,35 +170,55 @@ fn maybe_watch( } else { builder.build().unwrap() }; - fn handle_compile_result( - compile_result: CompileResult, - ) -> wgpu::ShaderModuleDescriptor<'static> { - let module_path = compile_result.module.unwrap_single(); - let data = std::fs::read(module_path).unwrap(); - let spirv = Cow::Owned(wgpu::util::make_spirv_raw(&data).into_owned()); - wgpu::ShaderModuleDescriptor { - label: None, - source: wgpu::ShaderSource::SpirV(spirv), + fn handle_compile_result(compile_result: CompileResult) -> CompiledShaderModules { + let load_spv_module = |path| { + let data = std::fs::read(path).unwrap(); + // FIXME(eddyb) this reallocates all the data pointlessly, there is + // not a good reason to use `ShaderModuleDescriptorSpirV` specifically. + let spirv = Cow::Owned(wgpu::util::make_spirv_raw(&data).into_owned()); + wgpu::ShaderModuleDescriptorSpirV { + label: None, + source: spirv, + } + }; + CompiledShaderModules { + named_spv_modules: match compile_result.module { + spirv_builder::ModuleResult::SingleModule(path) => { + vec![(None, load_spv_module(path))] + } + spirv_builder::ModuleResult::MultiModule(modules) => modules + .into_iter() + .map(|(name, path)| (Some(name), load_spv_module(path))) + .collect(), + }, } } handle_compile_result(initial_result) } #[cfg(any(target_os = "android", target_arch = "wasm32"))] { - match shader { - RustGPUShader::Simplest => wgpu::include_spirv!(env!("simplest_shader.spv")), - RustGPUShader::Sky => wgpu::include_spirv!(env!("sky_shader.spv")), - RustGPUShader::Compute => wgpu::include_spirv!(env!("compute_shader.spv")), - RustGPUShader::Mouse => wgpu::include_spirv!(env!("mouse_shader.spv")), + let module = match options.shader { + RustGPUShader::Simplest => { + wgpu::include_spirv_raw!(env!("simplest_shader.spv")) + } + RustGPUShader::Sky => wgpu::include_spirv_raw!(env!("sky_shader.spv")), + RustGPUShader::Compute => wgpu::include_spirv_raw!(env!("compute_shader.spv")), + RustGPUShader::Mouse => wgpu::include_spirv_raw!(env!("mouse_shader.spv")), + }; + CompiledShaderModules { + named_spv_modules: vec![(None, module)], } } } -#[derive(StructOpt)] +#[derive(StructOpt, Clone)] #[structopt(name = "example-runner-wgpu")] pub struct Options { #[structopt(short, long, default_value = "Sky")] shader: RustGPUShader, + + #[structopt(long)] + force_spirv_passthru: bool, } #[cfg_attr(target_os = "android", export_name = "android_main")]