diff --git a/.gitignore b/.gitignore index 410633c8d7..07ba091a65 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ target/ tests/Cargo.lock .github/install-spirv-tools/Cargo.lock rustc-ice-*.txt +.idea diff --git a/Cargo.lock b/Cargo.lock index bb709cad86..eb177ffe93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -70,7 +70,7 @@ dependencies = [ "ndk-context", "ndk-sys 0.6.0+11769913", "num_enum", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -260,6 +260,9 @@ name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] [[package]] name = "block" @@ -319,7 +322,7 @@ dependencies = [ "polling", "rustix", "slab", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -371,9 +374,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "clap" -version = "4.5.23" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" +checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" dependencies = [ "clap_builder", "clap_derive", @@ -381,9 +384,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.23" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" +checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" dependencies = [ "anstream", "anstyle", @@ -393,9 +396,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" dependencies = [ "heck", "proc-macro2", @@ -1107,7 +1110,7 @@ checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" dependencies = [ "log", "presser", - "thiserror", + "thiserror 1.0.69", "windows", ] @@ -1260,7 +1263,7 @@ dependencies = [ "combine", "jni-sys", "log", - "thiserror", + "thiserror 1.0.69", "walkdir", "windows-sys 0.45.0", ] @@ -1546,7 +1549,7 @@ dependencies = [ "rustc-hash", "spirv", "termcolor", - "thiserror", + "thiserror 1.0.69", "unicode-xid", ] @@ -1563,7 +1566,7 @@ dependencies = [ "num_enum", "raw-window-handle 0.5.2", "raw-window-handle 0.6.2", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2231,7 +2234,7 @@ checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom", "libredox", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2350,6 +2353,7 @@ dependencies = [ "rspirv", "serde", "serde_json", + "spirv", ] [[package]] @@ -2481,6 +2485,9 @@ name = "semver" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" +dependencies = [ + "serde", +] [[package]] name = "serde" @@ -2594,7 +2601,7 @@ dependencies = [ "log", "memmap2", "rustix", - "thiserror", + "thiserror 1.0.69", "wayland-backend", "wayland-client 0.31.7", "wayland-csd-frame", @@ -2642,19 +2649,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ "bitflags 2.6.0", + "serde", ] [[package]] name = "spirv-builder" version = "0.9.0" dependencies = [ + "clap", "memchr", "notify", "raw-string", "rustc_codegen_spirv", "rustc_codegen_spirv-types", + "semver", "serde", "serde_json", + "thiserror 2.0.12", ] [[package]] @@ -2813,7 +2824,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", ] [[package]] @@ -2827,6 +2847,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thorin-dwp" version = "0.8.0" @@ -3412,7 +3443,7 @@ dependencies = [ "raw-window-handle 0.6.2", "rustc-hash", "smallvec", - "thiserror", + "thiserror 1.0.69", "wgpu-hal", "wgpu-types", ] @@ -3454,7 +3485,7 @@ dependencies = [ "renderdoc-sys", "rustc-hash", "smallvec", - "thiserror", + "thiserror 1.0.69", "wasm-bindgen", "web-sys", "wgpu-types", diff --git a/crates/rustc_codegen_spirv-types/Cargo.toml b/crates/rustc_codegen_spirv-types/Cargo.toml index e2d64d35cf..36bd737f7e 100644 --- a/crates/rustc_codegen_spirv-types/Cargo.toml +++ b/crates/rustc_codegen_spirv-types/Cargo.toml @@ -8,6 +8,7 @@ license.workspace = true repository.workspace = true [dependencies] +spirv = { version = "0.3.0", features = ["serialize", "deserialize"] } rspirv = "0.12" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/crates/rustc_codegen_spirv/build.rs b/crates/rustc_codegen_spirv/build.rs index b82a0e31c6..c7cafd699f 100644 --- a/crates/rustc_codegen_spirv/build.rs +++ b/crates/rustc_codegen_spirv/build.rs @@ -11,6 +11,9 @@ use std::process::{Command, ExitCode}; use std::{env, fs, mem}; /// Current `rust-toolchain.toml` file +/// WARNING!!! cargo-gpu is now relying on this being a string literal! It will +/// scan `build.rs` for any line starting with `channel = "..."` to figure out +/// which toolchain version to use! This also allows backwards compat. /// Unfortunately, directly including the actual workspace `rust-toolchain.toml` doesn't work together with /// `cargo publish`. We need to figure out a way to do this properly, but let's hardcode it for now :/ //const REQUIRED_RUST_TOOLCHAIN: &str = include_str!("../../rust-toolchain.toml"); diff --git a/crates/spirv-builder/Cargo.toml b/crates/spirv-builder/Cargo.toml index 125bcbe72d..545085c114 100644 --- a/crates/spirv-builder/Cargo.toml +++ b/crates/spirv-builder/Cargo.toml @@ -18,13 +18,17 @@ no-default-features = true # that optional dependency, from being automatically created by Cargo, see: # https://doc.rust-lang.org/cargo/reference/features.html#optional-dependencies [features] -# See `rustc_codegen_spirv/Cargo.toml` for details on these features. default = ["use-compiled-tools"] -use-installed-tools = ["dep:rustc_codegen_spirv", "rustc_codegen_spirv?/use-installed-tools"] -use-compiled-tools = ["dep:rustc_codegen_spirv", "rustc_codegen_spirv?/use-compiled-tools"] +# Compile `rustc_codegen_spirv`, allows constructing SpirvBuilder without +# explicitly passing in a path to a compiled `rustc_codegen_spirv.so` (or dll) +rustc_codegen_spirv = ["dep:rustc_codegen_spirv"] +# See `rustc_codegen_spirv/Cargo.toml` for details on these features. +use-installed-tools = ["rustc_codegen_spirv", "rustc_codegen_spirv?/use-installed-tools"] +use-compiled-tools = ["rustc_codegen_spirv", "rustc_codegen_spirv?/use-compiled-tools"] skip-toolchain-check = ["rustc_codegen_spirv?/skip-toolchain-check"] watch = ["dep:notify"] +clap = ["dep:clap"] [dependencies] # See comment in `src/lib.rs` `invoke_rustc` regarding `rustc_codegen_spirv` dep. @@ -38,5 +42,9 @@ memchr = "2.4" raw-string = "0.3.5" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +thiserror = "2.0.12" +semver = { version = "1.0.24", features = ["serde"] } notify = { version = "7.0", optional = true } +# Pinning clap, as newer versions have raised min rustc version without being marked a breaking change +clap = { version = "=4.5.37", optional = true, features = ["derive"] } diff --git a/crates/spirv-builder/src/lib.rs b/crates/spirv-builder/src/lib.rs index 488f71508e..ea017ed526 100644 --- a/crates/spirv-builder/src/lib.rs +++ b/crates/spirv-builder/src/lib.rs @@ -72,116 +72,65 @@ // #![allow()] #![doc = include_str!("../README.md")] -// HACK(eddyb) try to catch misuse of Cargo package features very early on -// (see `spirv-builder/Cargo.toml` for why we go through all of this). -#[cfg(all( - not(any(feature = "use-compiled-tools", feature = "use-installed-tools")), - not(doc) -))] -compile_error!( - "at least one of `use-compiled-tools` or `use-installed-tools` features must be enabled -(outside of documentation builds, which require disabling both to build on stable)" -); - -#[cfg(doc)] -fn _ensure_cfg_doc_means_rustdoc() { - // HACK(eddyb) this relies on specific `rustdoc` behavior (i.e. it skips - // type-checking function bodies, so we trigger a compile-time `panic! from - // a type) to check that we're in fact under `rustdoc`, not just `--cfg doc`. - #[rustfmt::skip] - let _: [(); panic!(" - - `--cfg doc` was set outside of `rustdoc` - (if you are running `rustdoc` or `cargo doc`, please file an issue) - -")]; -} - mod depfile; +mod target_specs; #[cfg(feature = "watch")] mod watch; use raw_string::{RawStr, RawString}; +use semver::Version; use serde::Deserialize; use std::borrow::Borrow; use std::collections::HashMap; use std::env; -use std::error::Error; -use std::fmt; use std::fs::File; use std::io::BufReader; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; +use thiserror::Error; pub use rustc_codegen_spirv_types::Capability; pub use rustc_codegen_spirv_types::{CompileResult, ModuleResult}; +pub use target_specs::TARGET_SPECS; -#[derive(Debug)] +#[derive(Debug, Error)] #[non_exhaustive] pub enum SpirvBuilderError { + #[error("`target` must be set, for example `spirv-unknown-vulkan1.2`")] + MissingTarget, + #[error("expected `{SPIRV_TARGET_PREFIX}...` target, found `{target}`")] NonSpirvTarget { target: String }, + #[error("SPIR-V target `{SPIRV_TARGET_PREFIX}-{target_env}` is not supported")] UnsupportedSpirvTargetEnv { target_env: String }, + #[error("`path_to_crate` must be set")] + MissingCratePath, + #[error("crate path '{0}' does not exist")] CratePathDoesntExist(PathBuf), + #[error( + "Without feature `rustc_codegen_spirv`, you need to set the path of the dylib using `rustc_codegen_spirv_location(...)`" + )] + MissingRustcCodegenSpirvDylib, + #[error("`rustc_codegen_spirv_location` path '{0}' is not a file")] + RustcCodegenSpirvDylibDoesNotExist(PathBuf), + #[error("build failed")] BuildFailed, + #[error("multi-module build cannot be used with print_metadata = MetadataPrintout::Full")] MultiModuleWithPrintMetadata, + #[error("watching within build scripts will prevent build completion")] WatchWithPrintMetadata, - MetadataFileMissing(std::io::Error), - MetadataFileMalformed(serde_json::Error), + #[error("multi-module metadata file missing")] + MetadataFileMissing(#[from] std::io::Error), + #[error("unable to parse multi-module metadata file")] + MetadataFileMalformed(#[from] serde_json::Error), } const SPIRV_TARGET_PREFIX: &str = "spirv-unknown-"; -impl fmt::Display for SpirvBuilderError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::NonSpirvTarget { target } => { - write!( - f, - "expected `{SPIRV_TARGET_PREFIX}...` target, found `{target}`" - ) - } - Self::UnsupportedSpirvTargetEnv { target_env } if target_env.starts_with("opencl") => { - write!( - f, - "OpenCL targets like `{SPIRV_TARGET_PREFIX}-{target_env}` are not supported" - ) - } - Self::UnsupportedSpirvTargetEnv { target_env } if target_env.starts_with("webgpu") => { - write!( - f, - "WebGPU targets like `{SPIRV_TARGET_PREFIX}-{target_env}` are not supported, \ - consider using `{SPIRV_TARGET_PREFIX}-vulkan1.0` instead" - ) - } - Self::UnsupportedSpirvTargetEnv { target_env } => { - write!( - f, - "SPIR-V target `{SPIRV_TARGET_PREFIX}-{target_env}` is not supported" - ) - } - Self::CratePathDoesntExist(path) => { - write!(f, "crate path {} does not exist", path.display()) - } - Self::BuildFailed => f.write_str("build failed"), - Self::MultiModuleWithPrintMetadata => f.write_str( - "multi-module build cannot be used with print_metadata = MetadataPrintout::Full", - ), - Self::WatchWithPrintMetadata => { - f.write_str("watching within build scripts will prevent build completion") - } - Self::MetadataFileMissing(_) => f.write_str("multi-module metadata file missing"), - Self::MetadataFileMalformed(_) => { - f.write_str("unable to parse multi-module metadata file") - } - } - } -} - -impl Error for SpirvBuilderError {} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, serde::Deserialize, serde::Serialize)] +#[cfg_attr(feature = "clap", derive(clap::ValueEnum))] pub enum MetadataPrintout { /// Print no cargo metadata. + #[default] None, /// Print only dependency information (eg for multiple modules). DependencyOnly, @@ -191,9 +140,11 @@ pub enum MetadataPrintout { Full, } -#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, serde::Deserialize, serde::Serialize)] +#[cfg_attr(feature = "clap", derive(clap::ValueEnum))] pub enum SpirvMetadata { /// Strip all names and other debug information from SPIR-V output. + #[default] None, /// Only include `OpName`s for public interface variables (uniforms and the like), to allow /// shader reflection. @@ -203,13 +154,15 @@ pub enum SpirvMetadata { } /// Strategy used to handle Rust `panic!`s in shaders compiled to SPIR-V. -#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, serde::Deserialize, serde::Serialize)] +#[cfg_attr(feature = "clap", derive(clap::ValueEnum))] pub enum ShaderPanicStrategy { /// Return from shader entry-point with no side-effects **(default)**. /// /// While similar to the standard SPIR-V `OpTerminateInvocation`, this is /// *not* limited to fragment shaders, and instead supports all shaders /// (as it's handled via control-flow rewriting, instead of SPIR-V features). + #[default] SilentExit, /// Like `SilentExit`, but also using `debugPrintf` to report the panic in @@ -255,6 +208,7 @@ pub enum ShaderPanicStrategy { /// 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`) + #[cfg_attr(feature = "clap", clap(skip))] DebugPrintfThenExit { /// Whether to also print the entry-point inputs (excluding buffers/resources), /// which should uniquely identify the panicking shader invocation. @@ -283,76 +237,250 @@ pub enum ShaderPanicStrategy { UNSOUND_DO_NOT_USE_UndefinedBehaviorViaUnreachable, } +/// Options for specifying the behavior of the validator +/// Copied from `spirv-tools/src/val.rs` struct `ValidatorOptions`, with some fields disabled. +#[derive(Default, Debug, Clone, serde::Deserialize, serde::Serialize)] +#[cfg_attr(feature = "clap", derive(clap::Parser))] +pub struct ValidatorOptions { + /// Record whether or not the validator should relax the rules on types for + /// stores to structs. When relaxed, it will allow a type mismatch as long as + /// the types are structs with the same layout. Two structs have the same layout + /// if + /// + /// 1) the members of the structs are either the same type or are structs with + /// same layout, and + /// + /// 2) the decorations that affect the memory layout are identical for both + /// types. Other decorations are not relevant. + #[cfg_attr(feature = "clap", arg(long, default_value = "false"))] + pub relax_struct_store: bool, + /// Records whether or not the validator should relax the rules on pointer usage + /// in logical addressing mode. + /// + /// When relaxed, it will allow the following usage cases of pointers: + /// 1) `OpVariable` allocating an object whose type is a pointer type + /// 2) `OpReturnValue` returning a pointer value + #[cfg_attr(feature = "clap", arg(long, default_value = "false"))] + pub relax_logical_pointer: bool, + // /// Records whether or not the validator should relax the rules because it is + // /// expected that the optimizations will make the code legal. + // /// + // /// When relaxed, it will allow the following: + // /// 1) It will allow relaxed logical pointers. Setting this option will also + // /// set that option. + // /// 2) Pointers that are pass as parameters to function calls do not have to + // /// match the storage class of the formal parameter. + // /// 3) Pointers that are actaul parameters on function calls do not have to point + // /// to the same type pointed as the formal parameter. The types just need to + // /// logically match. + // pub before_legalization: bool, + /// Records whether the validator should use "relaxed" block layout rules. + /// Relaxed layout rules are described by Vulkan extension + /// `VK_KHR_relaxed_block_layout`, and they affect uniform blocks, storage blocks, + /// and push constants. + /// + /// This is enabled by default when targeting Vulkan 1.1 or later. + /// Relaxed layout is more permissive than the default rules in Vulkan 1.0. + #[cfg_attr(feature = "clap", arg(long, default_value = "false"))] + pub relax_block_layout: Option, + /// Records whether the validator should use standard block layout rules for + /// uniform blocks. + #[cfg_attr(feature = "clap", arg(long, default_value = "false"))] + pub uniform_buffer_standard_layout: bool, + /// Records whether the validator should use "scalar" block layout rules. + /// Scalar layout rules are more permissive than relaxed block layout. + /// + /// See Vulkan extnesion `VK_EXT_scalar_block_layout`. The scalar alignment is + /// defined as follows: + /// - scalar alignment of a scalar is the scalar size + /// - scalar alignment of a vector is the scalar alignment of its component + /// - scalar alignment of a matrix is the scalar alignment of its component + /// - scalar alignment of an array is the scalar alignment of its element + /// - scalar alignment of a struct is the max scalar alignment among its + /// members + /// + /// For a struct in Uniform, `StorageClass`, or `PushConstant`: + /// - a member Offset must be a multiple of the member's scalar alignment + /// - `ArrayStride` or `MatrixStride` must be a multiple of the array or matrix + /// scalar alignment + #[cfg_attr(feature = "clap", arg(long, default_value = "false"))] + pub scalar_block_layout: bool, + /// Records whether or not the validator should skip validating standard + /// uniform/storage block layout. + #[cfg_attr(feature = "clap", arg(long, default_value = "false"))] + pub skip_block_layout: bool, + // /// Applies a maximum to one or more Universal limits + // pub max_limits: Vec<(ValidatorLimits, u32)>, +} + +/// Options for specifying the behavior of the optimizer +/// Copied from `spirv-tools/src/opt.rs` struct `Options`, with some fields disabled. +#[derive(Default, Debug, Clone, serde::Deserialize, serde::Serialize)] +#[cfg_attr(feature = "clap", derive(clap::Parser))] +pub struct OptimizerOptions { + // /// Records the validator options that should be passed to the validator, + // /// the validator will run with the options before optimizer. + // pub validator_options: Option, + // /// Records the maximum possible value for the id bound. + // pub max_id_bound: Option, + /// Records whether all bindings within the module should be preserved. + #[cfg_attr(feature = "clap", arg(long, default_value = "false"))] + pub preserve_bindings: bool, + // /// Records whether all specialization constants within the module + // /// should be preserved. + // pub preserve_spec_constants: bool, +} + /// Cargo features specification for building the shader crate. -#[derive(Default)] -struct ShaderCrateFeatures { - default_features: Option, - features: Vec, +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[cfg_attr(feature = "clap", derive(clap::Parser))] +pub struct ShaderCrateFeatures { + /// Set --default-features for the target shader crate. + #[cfg_attr(feature = "clap", clap(long = "no-default-features", default_value = "true", action = clap::ArgAction::SetFalse))] + pub default_features: bool, + /// Set --features for the target shader crate. + #[cfg_attr(feature = "clap", clap(long))] + pub features: Vec, +} + +impl Default for ShaderCrateFeatures { + fn default() -> Self { + Self { + default_features: true, + features: Vec::new(), + } + } } +#[non_exhaustive] +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[cfg_attr(feature = "clap", derive(clap::Parser))] pub struct SpirvBuilder { - path_to_crate: PathBuf, - print_metadata: MetadataPrintout, - release: bool, - target: String, - shader_crate_features: ShaderCrateFeatures, - deny_warnings: bool, - multimodule: bool, - spirv_metadata: SpirvMetadata, - capabilities: Vec, - extensions: Vec, - extra_args: Vec, - // Optional location of a known `rustc_codegen_spirv` dylib - rustc_codegen_spirv_location: Option, - // Optional location of a known "target-spec" file - path_to_target_spec: Option, - target_dir_path: Option, + #[cfg_attr(feature = "clap", clap(skip))] + pub path_to_crate: Option, + /// Whether to print build.rs cargo metadata (e.g. cargo:rustc-env=var=val). Defaults to [`MetadataPrintout::Full`]. + #[cfg_attr(feature = "clap", clap(skip))] + pub print_metadata: MetadataPrintout, + /// Build in release. Defaults to true. + #[cfg_attr(feature = "clap", clap(long = "debug", default_value = "true", action = clap::ArgAction::SetFalse))] + pub release: bool, + /// The target triple, eg. `spirv-unknown-vulkan1.2` + #[cfg_attr( + feature = "clap", + clap(long, default_value = "spirv-unknown-vulkan1.2") + )] + pub target: Option, + /// Cargo features specification for building the shader crate. + #[cfg_attr(feature = "clap", clap(flatten))] + #[serde(flatten)] + pub shader_crate_features: ShaderCrateFeatures, + /// Deny any warnings, as they may never be printed when building within a build script. Defaults to false. + #[cfg_attr(feature = "clap", arg(long, default_value = "false"))] + pub deny_warnings: bool, + /// Splits the resulting SPIR-V file into one module per entry point. This is useful in cases + /// where ecosystem tooling has bugs around multiple entry points per module - having all entry + /// points bundled into a single file is the preferred system. + #[cfg_attr(feature = "clap", arg(long, default_value = "false"))] + pub multimodule: bool, + /// Sets the level of metadata (primarily `OpName` and `OpLine`) included in the SPIR-V binary. + /// Including metadata significantly increases binary size. + #[cfg_attr(feature = "clap", arg(long, default_value = "none"))] + pub spirv_metadata: SpirvMetadata, + /// Adds a capability to the SPIR-V module. Checking if a capability is enabled in code can be + /// done via `#[cfg(target_feature = "TheCapability")]`. + #[cfg_attr(feature = "clap", arg(long, value_parser=Self::parse_spirv_capability))] + pub capabilities: Vec, + /// Adds an extension to the SPIR-V module. Checking if an extension is enabled in code can be + /// done via `#[cfg(target_feature = "ext:the_extension")]`. + #[cfg_attr(feature = "clap", arg(long))] + pub extensions: Vec, + /// Set additional "codegen arg". Note: the `RUSTGPU_CODEGEN_ARGS` environment variable + /// takes precedence over any set arguments using this function. + #[cfg_attr(feature = "clap", clap(skip))] + pub extra_args: Vec, + // Location of a known `rustc_codegen_spirv` dylib, only required without feature `rustc_codegen_spirv`. + #[cfg_attr(feature = "clap", clap(skip))] + pub rustc_codegen_spirv_location: Option, + // Overwrite the toolchain like `cargo +nightly` + #[cfg_attr(feature = "clap", clap(skip))] + pub toolchain_overwrite: Option, + // Set the rustc version of the toolchain, used to adjust params to support older toolchains + #[cfg_attr(feature = "clap", clap(skip))] + pub toolchain_rustc_version: Option, + + /// The path of the "target specification" file. + /// + /// For more info on "target specification" see + /// [this RFC](https://rust-lang.github.io/rfcs/0131-target-specification.html). + #[cfg_attr(feature = "clap", clap(skip))] + pub path_to_target_spec: Option, + /// Set the target dir path within `./target` to use for building shaders. Defaults to `spirv-builder`, resulting + /// in the path `./target/spirv-builder`. + #[cfg_attr(feature = "clap", clap(skip))] + pub target_dir_path: Option, // `rustc_codegen_spirv::linker` codegen args + /// Change the shader `panic!` handling strategy (see [`ShaderPanicStrategy`]). + #[cfg_attr(feature = "clap", clap(skip))] pub shader_panic_strategy: ShaderPanicStrategy, - // spirv-val flags - pub relax_struct_store: bool, - pub relax_logical_pointer: bool, - pub relax_block_layout: bool, - pub uniform_buffer_standard_layout: bool, - pub scalar_block_layout: bool, - pub skip_block_layout: bool, + /// spirv-val flags + #[cfg_attr(feature = "clap", clap(flatten))] + #[serde(flatten)] + pub validator: ValidatorOptions, - // spirv-opt flags - pub preserve_bindings: bool, + /// spirv-opt flags + #[cfg_attr(feature = "clap", clap(flatten))] + #[serde(flatten)] + pub optimizer: OptimizerOptions, } +#[cfg(feature = "clap")] impl SpirvBuilder { - pub fn new(path_to_crate: impl AsRef, target: impl Into) -> Self { + /// Clap value parser for `Capability`. + fn parse_spirv_capability(capability: &str) -> Result { + use core::str::FromStr; + Capability::from_str(capability).map_or_else( + |()| Err(clap::Error::new(clap::error::ErrorKind::InvalidValue)), + Ok, + ) + } +} + +impl Default for SpirvBuilder { + fn default() -> Self { Self { - path_to_crate: path_to_crate.as_ref().to_owned(), - print_metadata: MetadataPrintout::Full, + path_to_crate: None, + print_metadata: MetadataPrintout::default(), release: true, - target: target.into(), + target: None, deny_warnings: false, multimodule: false, - spirv_metadata: SpirvMetadata::None, + spirv_metadata: SpirvMetadata::default(), capabilities: Vec::new(), extensions: Vec::new(), extra_args: Vec::new(), rustc_codegen_spirv_location: None, path_to_target_spec: None, target_dir_path: None, - - shader_panic_strategy: ShaderPanicStrategy::SilentExit, - - relax_struct_store: false, - relax_logical_pointer: false, - relax_block_layout: false, - uniform_buffer_standard_layout: false, - scalar_block_layout: false, - skip_block_layout: false, - - preserve_bindings: false, + toolchain_overwrite: None, + toolchain_rustc_version: None, + shader_panic_strategy: ShaderPanicStrategy::default(), + validator: ValidatorOptions::default(), + optimizer: OptimizerOptions::default(), shader_crate_features: ShaderCrateFeatures::default(), } } +} + +impl SpirvBuilder { + pub fn new(path_to_crate: impl AsRef, target: impl Into) -> Self { + Self { + path_to_crate: Some(path_to_crate.as_ref().to_owned()), + target: Some(target.into()), + ..SpirvBuilder::default() + } + } /// Sets the path of the "target specification" file. /// @@ -427,7 +555,7 @@ impl SpirvBuilder { /// Allow store from one struct type to a different type with compatible layout and members. #[must_use] pub fn relax_struct_store(mut self, v: bool) -> Self { - self.relax_struct_store = v; + self.validator.relax_struct_store = v; self } @@ -435,7 +563,7 @@ impl SpirvBuilder { /// in logical addressing mode #[must_use] pub fn relax_logical_pointer(mut self, v: bool) -> Self { - self.relax_logical_pointer = v; + self.validator.relax_logical_pointer = v; self } @@ -443,7 +571,7 @@ impl SpirvBuilder { /// push constant layouts. This is the default when targeting Vulkan 1.1 or later. #[must_use] pub fn relax_block_layout(mut self, v: bool) -> Self { - self.relax_block_layout = v; + self.validator.relax_block_layout = Some(v); self } @@ -451,7 +579,7 @@ impl SpirvBuilder { /// layouts. #[must_use] pub fn uniform_buffer_standard_layout(mut self, v: bool) -> Self { - self.uniform_buffer_standard_layout = v; + self.validator.uniform_buffer_standard_layout = v; self } @@ -460,7 +588,7 @@ impl SpirvBuilder { /// in effect this will override the --relax-block-layout option. #[must_use] pub fn scalar_block_layout(mut self, v: bool) -> Self { - self.scalar_block_layout = v; + self.validator.scalar_block_layout = v; self } @@ -468,14 +596,14 @@ impl SpirvBuilder { /// --scalar-block-layout option. #[must_use] pub fn skip_block_layout(mut self, v: bool) -> Self { - self.skip_block_layout = v; + self.validator.skip_block_layout = v; self } /// Preserve unused descriptor bindings. Useful for reflection. #[must_use] pub fn preserve_bindings(mut self, v: bool) -> Self { - self.preserve_bindings = v; + self.optimizer.preserve_bindings = v; self } @@ -490,7 +618,7 @@ impl SpirvBuilder { /// Set --default-features for the target shader crate. #[must_use] pub fn shader_crate_default_features(mut self, default_features: bool) -> Self { - self.shader_crate_features.default_features = Some(default_features); + self.shader_crate_features.default_features = default_features; self } @@ -502,17 +630,8 @@ impl SpirvBuilder { } #[must_use] - pub fn rustc_codegen_spirv_location( - mut self, - path_to_dylib: impl AsRef, - ) -> Self { - let path_to_dylib = path_to_dylib.as_ref().to_path_buf(); - assert!( - path_to_dylib.is_file(), - "Provided path to dylib '{}' is not a file", - path_to_dylib.display() - ); - self.rustc_codegen_spirv_location = Some(path_to_dylib); + pub fn rustc_codegen_spirv_location(mut self, path_to_dylib: impl AsRef) -> Self { + self.rustc_codegen_spirv_location = Some(path_to_dylib.as_ref().to_path_buf()); self } @@ -526,9 +645,8 @@ impl SpirvBuilder { /// Builds the module. If `print_metadata` is [`MetadataPrintout::Full`], you usually don't have to inspect the path /// in the result, as the environment variable for the path to the module will already be set. - pub fn build(mut self) -> Result { - self.validate_running_conditions()?; - let metadata_file = invoke_rustc(&self)?; + pub fn build(&self) -> Result { + let metadata_file = invoke_rustc(self)?; match self.print_metadata { MetadataPrintout::Full | MetadataPrintout::DependencyOnly => { leaf_deps(&metadata_file, |artifact| { @@ -544,43 +662,6 @@ impl SpirvBuilder { Ok(metadata) } - pub(crate) fn validate_running_conditions(&mut self) -> Result<(), SpirvBuilderError> { - let target_env = self - .target - .strip_prefix(SPIRV_TARGET_PREFIX) - .ok_or_else(|| SpirvBuilderError::NonSpirvTarget { - target: self.target.clone(), - })?; - // HACK(eddyb) used only to split the full list into groups. - #[allow(clippy::match_same_arms)] - match target_env { - // HACK(eddyb) hardcoded list to avoid checking if the JSON file - // for a particular target exists (and sanitizing strings for paths). - // - // FIXME(eddyb) consider moving this list, or even `target-specs`, - // into `rustc_codegen_spirv_types`'s code/source. - "spv1.0" | "spv1.1" | "spv1.2" | "spv1.3" | "spv1.4" | "spv1.5" => {} - "opengl4.0" | "opengl4.1" | "opengl4.2" | "opengl4.3" | "opengl4.5" => {} - "vulkan1.0" | "vulkan1.1" | "vulkan1.1spv1.4" | "vulkan1.2" => {} - - _ => { - return Err(SpirvBuilderError::UnsupportedSpirvTargetEnv { - target_env: target_env.into(), - }); - } - } - - if (self.print_metadata == MetadataPrintout::Full) && self.multimodule { - return Err(SpirvBuilderError::MultiModuleWithPrintMetadata); - } - if !self.path_to_crate.is_dir() { - return Err(SpirvBuilderError::CratePathDoesntExist(std::mem::take( - &mut self.path_to_crate, - ))); - } - Ok(()) - } - pub(crate) fn parse_metadata_file( &self, at: &Path, @@ -631,19 +712,23 @@ fn dylib_path() -> Vec { } } -fn find_rustc_codegen_spirv() -> PathBuf { - let filename = format!( - "{}rustc_codegen_spirv{}", - env::consts::DLL_PREFIX, - env::consts::DLL_SUFFIX - ); - for mut path in dylib_path() { - path.push(&filename); - if path.is_file() { - return path; +fn find_rustc_codegen_spirv() -> Result { + if cfg!(feature = "rustc_codegen_spirv") { + let filename = format!( + "{}rustc_codegen_spirv{}", + env::consts::DLL_PREFIX, + env::consts::DLL_SUFFIX + ); + for mut path in dylib_path() { + path.push(&filename); + if path.is_file() { + return Ok(path); + } } + panic!("Could not find {filename} in library path"); + } else { + Err(SpirvBuilderError::MissingRustcCodegenSpirvDylib) } - panic!("Could not find {filename} in library path"); } /// Joins strings together while ensuring none of the strings contain the separator. @@ -658,6 +743,56 @@ fn join_checking_for_separators(strings: Vec>, sep: &str) -> St // Returns path to the metadata json. fn invoke_rustc(builder: &SpirvBuilder) -> Result { + let target = builder + .target + .as_ref() + .ok_or(SpirvBuilderError::MissingTarget)?; + let path_to_crate = builder + .path_to_crate + .as_ref() + .ok_or(SpirvBuilderError::MissingCratePath)?; + { + let target_env = target.strip_prefix(SPIRV_TARGET_PREFIX).ok_or_else(|| { + SpirvBuilderError::NonSpirvTarget { + target: target.clone(), + } + })?; + // HACK(eddyb) used only to split the full list into groups. + #[allow(clippy::match_same_arms)] + match target_env { + // HACK(eddyb) hardcoded list to avoid checking if the JSON file + // for a particular target exists (and sanitizing strings for paths). + // + // FIXME(eddyb) consider moving this list, or even `target-specs`, + // into `rustc_codegen_spirv_types`'s code/source. + "spv1.0" | "spv1.1" | "spv1.2" | "spv1.3" | "spv1.4" | "spv1.5" => {} + "opengl4.0" | "opengl4.1" | "opengl4.2" | "opengl4.3" | "opengl4.5" => {} + "vulkan1.0" | "vulkan1.1" | "vulkan1.1spv1.4" | "vulkan1.2" => {} + + _ => { + return Err(SpirvBuilderError::UnsupportedSpirvTargetEnv { + target_env: target_env.into(), + }); + } + } + + if (builder.print_metadata == MetadataPrintout::Full) && builder.multimodule { + return Err(SpirvBuilderError::MultiModuleWithPrintMetadata); + } + if !path_to_crate.is_dir() { + return Err(SpirvBuilderError::CratePathDoesntExist( + path_to_crate.clone(), + )); + } + } + + let toolchain_rustc_version = + if let Some(toolchain_rustc_version) = &builder.toolchain_rustc_version { + toolchain_rustc_version.clone() + } else { + query_rustc_version(builder.toolchain_overwrite.as_deref())? + }; + // Okay, this is a little bonkers: in a normal world, we'd have the user clone // rustc_codegen_spirv and pass in the path to it, and then we'd invoke cargo to build it, grab // the resulting .so, and pass it into -Z codegen-backend. But that's really gross: the user @@ -666,10 +801,14 @@ fn invoke_rustc(builder: &SpirvBuilder) -> Result { // alongside build.rs, and cargo will helpfully add it to LD_LIBRARY_PATH for us! However, // rustc expects a full path, instead of a filename looked up via LD_LIBRARY_PATH, so we need // to copy cargo's understanding of library lookup and find the library and its full path. - let rustc_codegen_spirv = builder - .rustc_codegen_spirv_location - .clone() - .unwrap_or_else(find_rustc_codegen_spirv); + let rustc_codegen_spirv = Ok(builder.rustc_codegen_spirv_location.clone()) + .transpose() + .unwrap_or_else(find_rustc_codegen_spirv)?; + if !rustc_codegen_spirv.is_file() { + return Err(SpirvBuilderError::RustcCodegenSpirvDylibDoesNotExist( + rustc_codegen_spirv, + )); + } let mut rustflags = vec![ format!("-Zcodegen-backend={}", rustc_codegen_spirv.display()), @@ -713,25 +852,25 @@ fn invoke_rustc(builder: &SpirvBuilder) -> Result { } SpirvMetadata::Full => llvm_args.push("--spirv-metadata=full".to_string()), } - if builder.relax_struct_store { + if builder.validator.relax_struct_store { llvm_args.push("--relax-struct-store".to_string()); } - if builder.relax_logical_pointer { + if builder.validator.relax_logical_pointer { llvm_args.push("--relax-logical-pointer".to_string()); } - if builder.relax_block_layout { + if builder.validator.relax_block_layout.unwrap_or(false) { llvm_args.push("--relax-block-layout".to_string()); } - if builder.uniform_buffer_standard_layout { + if builder.validator.uniform_buffer_standard_layout { llvm_args.push("--uniform-buffer-standard-layout".to_string()); } - if builder.scalar_block_layout { + if builder.validator.scalar_block_layout { llvm_args.push("--scalar-block-layout".to_string()); } - if builder.skip_block_layout { + if builder.validator.skip_block_layout { llvm_args.push("--skip-block-layout".to_string()); } - if builder.preserve_bindings { + if builder.optimizer.preserve_bindings { llvm_args.push("--preserve-bindings".to_string()); } let mut target_features = vec![]; @@ -812,6 +951,9 @@ fn invoke_rustc(builder: &SpirvBuilder) -> Result { let profile = if builder.release { "release" } else { "dev" }; let mut cargo = Command::new("cargo"); + if let Some(toolchain) = &builder.toolchain_overwrite { + cargo.arg(format!("+{}", toolchain)); + } cargo.args([ "build", "--lib", @@ -828,18 +970,24 @@ fn invoke_rustc(builder: &SpirvBuilder) -> Result { // FIXME(eddyb) consider moving `target-specs` into `rustc_codegen_spirv_types`. // FIXME(eddyb) consider the `RUST_TARGET_PATH` env var alternative. - cargo - .arg("--target") - .arg(builder.path_to_target_spec.clone().unwrap_or_else(|| { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("target-specs") - .join(format!("{}.json", builder.target)) - })); - - if let Some(default_features) = builder.shader_crate_features.default_features { - if !default_features { - cargo.arg("--no-default-features"); - } + + // NOTE(firestar99) rustc 1.76 has been tested to correctly parse modern + // target_spec jsons, some later version requires them, some earlier + // version fails with them (notably our 0.9.0 release) + if toolchain_rustc_version >= Version::new(1, 76, 0) { + cargo + .arg("--target") + .arg(builder.path_to_target_spec.clone().unwrap_or_else(|| { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("target-specs") + .join(format!("{}.json", target)) + })); + } else { + cargo.arg("--target").arg(target); + } + + if !builder.shader_crate_features.default_features { + cargo.arg("--no-default-features"); } if !builder.shader_crate_features.features.is_empty() { @@ -871,6 +1019,12 @@ fn invoke_rustc(builder: &SpirvBuilder) -> Result { // so we turn off that caching with an env var, just to avoid any issues. cargo.env("CARGO_CACHE_RUSTC_INFO", "0"); + // NOTE(firestar99) If you call SpirvBuilder in a build script, it will + // set `RUSTC` before calling it. And if we were to propagate it to our + // cargo invocation, it will take precedence over the `+toolchain` we + // previously set. + cargo.env_remove("RUSTC"); + // NOTE(eddyb) this used to be just `RUSTFLAGS` but at some point Cargo // added a separate environment variable using `\x1f` instead of spaces, // which allows us to have spaces within individual `rustc` flags. @@ -891,7 +1045,7 @@ fn invoke_rustc(builder: &SpirvBuilder) -> Result { let build = cargo .stderr(Stdio::inherit()) - .current_dir(&builder.path_to_crate) + .current_dir(path_to_crate) .output() .expect("failed to execute cargo build"); @@ -974,3 +1128,21 @@ fn leaf_deps(artifact: &Path, mut handle: impl FnMut(&RawStr)) -> std::io::Resul recurse(&deps_map, artifact.to_str().unwrap().into(), &mut handle); Ok(()) } + +pub fn query_rustc_version(toolchain: Option<&str>) -> std::io::Result { + let mut cmd = Command::new("rustc"); + if let Some(toolchain) = toolchain { + cmd.arg(format!("+{}", toolchain)); + } + cmd.arg("--version"); + let output = cmd.output()?; + + let stdout = String::from_utf8(output.stdout).expect("stdout must be utf-8"); + let parse = |output: &str| { + let output = output.strip_prefix("rustc ")?; + let version = &output[..output.find(|c| !"0123456789.".contains(c))?]; + Version::parse(version).ok() + }; + Ok(parse(&stdout) + .unwrap_or_else(|| panic!("failed parsing `rustc --version` output `{}`", stdout))) +} diff --git a/crates/spirv-builder/src/target_specs.rs b/crates/spirv-builder/src/target_specs.rs new file mode 100644 index 0000000000..7b52d6da3d --- /dev/null +++ b/crates/spirv-builder/src/target_specs.rs @@ -0,0 +1,63 @@ +/// Metadata for the compile targets supported by `rust-gpu` +pub const TARGET_SPECS: &[(&str, &str)] = &[ + ( + "spirv-unknown-opengl4.0.json", + include_str!("../target-specs/spirv-unknown-opengl4.0.json"), + ), + ( + "spirv-unknown-opengl4.1.json", + include_str!("../target-specs/spirv-unknown-opengl4.1.json"), + ), + ( + "spirv-unknown-opengl4.2.json", + include_str!("../target-specs/spirv-unknown-opengl4.2.json"), + ), + ( + "spirv-unknown-opengl4.3.json", + include_str!("../target-specs/spirv-unknown-opengl4.3.json"), + ), + ( + "spirv-unknown-opengl4.5.json", + include_str!("../target-specs/spirv-unknown-opengl4.5.json"), + ), + ( + "spirv-unknown-spv1.0.json", + include_str!("../target-specs/spirv-unknown-spv1.0.json"), + ), + ( + "spirv-unknown-spv1.1.json", + include_str!("../target-specs/spirv-unknown-spv1.1.json"), + ), + ( + "spirv-unknown-spv1.2.json", + include_str!("../target-specs/spirv-unknown-spv1.2.json"), + ), + ( + "spirv-unknown-spv1.3.json", + include_str!("../target-specs/spirv-unknown-spv1.3.json"), + ), + ( + "spirv-unknown-spv1.4.json", + include_str!("../target-specs/spirv-unknown-spv1.4.json"), + ), + ( + "spirv-unknown-spv1.5.json", + include_str!("../target-specs/spirv-unknown-spv1.5.json"), + ), + ( + "spirv-unknown-vulkan1.0.json", + include_str!("../target-specs/spirv-unknown-vulkan1.0.json"), + ), + ( + "spirv-unknown-vulkan1.1.json", + include_str!("../target-specs/spirv-unknown-vulkan1.1.json"), + ), + ( + "spirv-unknown-vulkan1.1spv1.4.json", + include_str!("../target-specs/spirv-unknown-vulkan1.1spv1.4.json"), + ), + ( + "spirv-unknown-vulkan1.2.json", + include_str!("../target-specs/spirv-unknown-vulkan1.2.json"), + ), +]; diff --git a/crates/spirv-builder/src/watch.rs b/crates/spirv-builder/src/watch.rs index 9c011fd649..a1f68d62f3 100644 --- a/crates/spirv-builder/src/watch.rs +++ b/crates/spirv-builder/src/watch.rs @@ -1,111 +1,132 @@ -use std::{collections::HashSet, sync::mpsc::sync_channel}; - -use notify::{Event, RecursiveMode, Watcher}; -use rustc_codegen_spirv_types::CompileResult; - use crate::{SpirvBuilder, SpirvBuilderError, leaf_deps}; +use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher as _}; +use rustc_codegen_spirv_types::CompileResult; +use std::path::{Path, PathBuf}; +use std::sync::mpsc::Receiver; +use std::{collections::HashSet, sync::mpsc::sync_channel}; impl SpirvBuilder { /// Watches the module for changes using [`notify`](https://crates.io/crates/notify), - /// and rebuild it upon changes - /// - /// Returns the result of the first successful compilation, then calls - /// `on_compilation_finishes` for each subsequent compilation. - pub fn watch( - mut self, - mut on_compilation_finishes: impl FnMut(CompileResult) + Send + 'static, - ) -> Result { - self.validate_running_conditions()?; + /// and rebuild it upon changes. Calls `on_compilation_finishes` after each + /// successful compilation. The second `Option>` + /// param allows you to return some `T` on the first compile, which is + /// then returned by this function (wrapped in Option). + pub fn watch( + &self, + mut on_compilation_finishes: impl FnMut(CompileResult, Option>) + + Send + + 'static, + ) -> Result, SpirvBuilderError> { + let path_to_crate = self + .path_to_crate + .as_ref() + .ok_or(SpirvBuilderError::MissingCratePath)?; if !matches!(self.print_metadata, crate::MetadataPrintout::None) { return Err(SpirvBuilderError::WatchWithPrintMetadata); } - let metadata_result = crate::invoke_rustc(&self); + + let metadata_result = crate::invoke_rustc(self); // Load the dependencies of the thing let metadata_file = if let Ok(path) = metadata_result { path } else { - let (tx, rx) = sync_channel(0); // Fall back to watching from the crate root if the initial compilation fails - let mut watcher = - notify::recommended_watcher(move |event: notify::Result| match event { - Ok(e) => match e.kind { - notify::EventKind::Access(_) => (), - notify::EventKind::Any - | notify::EventKind::Create(_) - | notify::EventKind::Modify(_) - | notify::EventKind::Remove(_) - | notify::EventKind::Other => { - let _ = tx.try_send(()); - } - }, - Err(e) => println!("notify error: {e:?}"), - }) - .expect("Could create watcher"); // This is likely to notice changes in the `target` dir, however, given that `cargo watch` doesn't seem to handle that, + let mut watcher = Watcher::new(); watcher - .watch(&self.path_to_crate, RecursiveMode::Recursive) + .watcher + .watch(path_to_crate, RecursiveMode::Recursive) .expect("Could watch crate root"); loop { - rx.recv().expect("Watcher still alive"); - let metadata_file = crate::invoke_rustc(&self); + watcher.recv(); + let metadata_file = crate::invoke_rustc(self); if let Ok(f) = metadata_file { break f; } } }; let metadata = self.parse_metadata_file(&metadata_file)?; - let first_result = metadata; + let mut out = None; + on_compilation_finishes(metadata, Some(AcceptFirstCompile(&mut out))); + let builder = self.clone(); let thread = std::thread::spawn(move || { - let mut watched_paths = HashSet::new(); - let (tx, rx) = sync_channel(0); - let mut watcher = - notify::recommended_watcher(move |event: notify::Result| match event { - Ok(e) => match e.kind { - notify::EventKind::Access(_) => (), - notify::EventKind::Any - | notify::EventKind::Create(_) - | notify::EventKind::Modify(_) - | notify::EventKind::Remove(_) - | notify::EventKind::Other => { - let _ = tx.try_send(()); - } - }, - Err(e) => println!("notify error: {e:?}"), - }) - .expect("Could create watcher"); - leaf_deps(&metadata_file, |it| { - let path = it.to_path().unwrap(); - if watched_paths.insert(path.to_owned()) { - watcher - .watch(it.to_path().unwrap(), RecursiveMode::NonRecursive) - .expect("Cargo dependencies are valid files"); - } - }) - .expect("Could read dependencies file"); + let mut watcher = Watcher::new(); + watcher.watch_leaf_deps(&metadata_file); + loop { - rx.recv().expect("Watcher still alive"); - let metadata_result = crate::invoke_rustc(&self); + watcher.recv(); + let metadata_result = crate::invoke_rustc(&builder); if let Ok(file) = metadata_result { - let metadata = self + let metadata = builder .parse_metadata_file(&file) .expect("Metadata file is correct"); - - leaf_deps(&file, |it| { - let path = it.to_path().unwrap(); - if watched_paths.insert(path.to_owned()) { - watcher - .watch(it.to_path().unwrap(), RecursiveMode::NonRecursive) - .expect("Cargo dependencies are valid files"); - } - }) - .expect("Could read dependencies file"); - - on_compilation_finishes(metadata); + watcher.watch_leaf_deps(&metadata_file); + on_compilation_finishes(metadata, None); } } }); - std::mem::drop(thread); - Ok(first_result) + drop(thread); + Ok(out) + } +} + +pub struct AcceptFirstCompile<'a, T>(&'a mut Option); + +impl<'a, T> AcceptFirstCompile<'a, T> { + pub fn new(write: &'a mut Option) -> Self { + Self(write) + } + + pub fn submit(self, t: T) { + *self.0 = Some(t); + } +} + +struct Watcher { + watcher: RecommendedWatcher, + rx: Receiver<()>, + watched_paths: HashSet, +} + +impl Watcher { + fn new() -> Self { + let (tx, rx) = sync_channel(0); + let watcher = + notify::recommended_watcher(move |event: notify::Result| match event { + Ok(e) => match e.kind { + notify::EventKind::Access(_) => (), + notify::EventKind::Any + | notify::EventKind::Create(_) + | notify::EventKind::Modify(_) + | notify::EventKind::Remove(_) + | notify::EventKind::Other => { + let _ = tx.try_send(()); + } + }, + Err(e) => println!("notify error: {e:?}"), + }) + .expect("Could create watcher"); + Self { + watcher, + rx, + watched_paths: HashSet::new(), + } + } + + fn watch_leaf_deps(&mut self, metadata_file: &Path) { + leaf_deps(metadata_file, |it| { + let path = it.to_path().unwrap(); + if self.watched_paths.insert(path.to_owned()) { + self.watcher + .watch(it.to_path().unwrap(), RecursiveMode::NonRecursive) + .expect("Cargo dependencies are valid files"); + } + }) + .expect("Could read dependencies file"); + } + + fn recv(&self) { + self.rx.recv().expect("Watcher still alive"); } } diff --git a/examples/runners/wgpu/builder/src/main.rs b/examples/runners/wgpu/builder/src/main.rs index bee3f074d0..7fb9735381 100644 --- a/examples/runners/wgpu/builder/src/main.rs +++ b/examples/runners/wgpu/builder/src/main.rs @@ -1,4 +1,4 @@ -use spirv_builder::SpirvBuilder; +use spirv_builder::{MetadataPrintout, SpirvBuilder}; use std::env; use std::error::Error; use std::fs; @@ -7,7 +7,9 @@ use std::path::Path; fn build_shader(path_to_crate: &str, codegen_names: bool) -> Result<(), Box> { let builder_dir = &Path::new(env!("CARGO_MANIFEST_DIR")); let path_to_crate = builder_dir.join(path_to_crate); - let result = SpirvBuilder::new(path_to_crate, "spirv-unknown-vulkan1.1").build()?; + let result = SpirvBuilder::new(path_to_crate, "spirv-unknown-vulkan1.1") + .print_metadata(MetadataPrintout::Full) + .build()?; if codegen_names { let out_dir = env::var_os("OUT_DIR").unwrap(); let dest_path = Path::new(&out_dir).join("entry_points.rs"); diff --git a/examples/runners/wgpu/src/lib.rs b/examples/runners/wgpu/src/lib.rs index 82d0e758fb..11cba07c39 100644 --- a/examples/runners/wgpu/src/lib.rs +++ b/examples/runners/wgpu/src/lib.rs @@ -164,13 +164,7 @@ fn maybe_watch( // 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))) - .expect("Configuration is correct for watching") - } else { - builder.build().unwrap() - }; + fn handle_compile_result(compile_result: CompileResult) -> CompiledShaderModules { let load_spv_module = |path| { let data = std::fs::read(path).unwrap(); @@ -194,7 +188,22 @@ fn maybe_watch( }, } } - handle_compile_result(initial_result) + + if let Some(mut f) = on_watch { + builder + .watch(move |compile_result, accept| { + let modules = handle_compile_result(compile_result); + if let Some(accept) = accept { + accept.submit(modules); + } else { + f(modules); + } + }) + .expect("Configuration is correct for watching") + .unwrap() + } else { + handle_compile_result(builder.build().unwrap()) + } } #[cfg(any(target_os = "android", target_arch = "wasm32"))] {