diff --git a/Cargo.lock b/Cargo.lock index fbc2f16..d7ccf8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "anstream" version = "0.6.18" @@ -67,12 +73,33 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "1.3.2" @@ -108,6 +135,7 @@ dependencies = [ "directories", "env_logger 0.10.2", "log", + "naga", "relative-path", "rustc_codegen_spirv-target-specs", "semver", @@ -134,6 +162,7 @@ checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" dependencies = [ "camino", "cargo-platform", + "derive_builder", "semver", "serde", "serde_json", @@ -146,6 +175,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "clap" version = "4.5.37" @@ -186,6 +221,17 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +[[package]] +name = "codespan-reporting" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + [[package]] name = "colorchoice" version = "1.0.3" @@ -217,6 +263,78 @@ dependencies = [ "winapi", ] +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", +] + [[package]] name = "directories" version = "5.0.1" @@ -300,6 +418,24 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -326,11 +462,27 @@ dependencies = [ "wasi", ] +[[package]] +name = "half" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", +] + [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] [[package]] name = "heck" @@ -350,6 +502,12 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "indexmap" version = "2.9.0" @@ -444,6 +602,12 @@ version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + [[package]] name = "libredox" version = "0.1.3" @@ -504,6 +668,29 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "naga" +version = "25.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b977c445f26e49757f9aca3631c3b8b836942cb278d69a92e7b80d3b24da632" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.9.0", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown", + "indexmap", + "log", + "num-traits", + "once_cell", + "petgraph", + "rustc-hash", + "spirv", + "thiserror 2.0.12", +] + [[package]] name = "notify" version = "7.0.0" @@ -542,6 +729,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -583,6 +780,18 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "petgraph" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a98c6720655620a521dcc722d0ad66cd8afd5d86e34a89ef691c50b7b24de06" +dependencies = [ + "fixedbitset", + "hashbrown", + "indexmap", + "serde", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -754,7 +963,7 @@ checksum = "6c89eaf493b3dfc730cda42a77014aad65e03213992c7afe0dff60a9f7d3dd94" [[package]] name = "rustc_codegen_spirv-types" version = "0.9.0" -source = "git+https://github.com/Rust-GPU/rust-gpu?rev=86fc48032c4cd4afb74f1d81ae859711d20386a1#86fc48032c4cd4afb74f1d81ae859711d20386a1" +source = "git+https://github.com/Rust-GPU/rust-gpu?rev=8cb17db18d8a44e1de7c9b3ea2b65d5aaf24b919#8cb17db18d8a44e1de7c9b3ea2b65d5aaf24b919" dependencies = [ "rspirv", "serde", @@ -904,7 +1113,7 @@ dependencies = [ [[package]] name = "spirv-builder" version = "0.9.0" -source = "git+https://github.com/Rust-GPU/rust-gpu?rev=86fc48032c4cd4afb74f1d81ae859711d20386a1#86fc48032c4cd4afb74f1d81ae859711d20386a1" +source = "git+https://github.com/Rust-GPU/rust-gpu?rev=8cb17db18d8a44e1de7c9b3ea2b65d5aaf24b919#8cb17db18d8a44e1de7c9b3ea2b65d5aaf24b919" dependencies = [ "clap", "memchr", @@ -1120,6 +1329,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + [[package]] name = "utf8parse" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index 78a6b52..db26592 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ exclude = [ resolver = "2" [workspace.dependencies] -spirv-builder = { git = "https://github.com/Rust-GPU/rust-gpu", rev = "86fc48032c4cd4afb74f1d81ae859711d20386a1", default-features = false } +spirv-builder = { git = "https://github.com/Rust-GPU/rust-gpu", rev = "8cb17db18d8a44e1de7c9b3ea2b65d5aaf24b919", default-features = false } anyhow = "1.0.94" clap = { version = "4.5.37", features = ["derive"] } crossterm = "0.28.1" @@ -28,6 +28,7 @@ tempdir = "0.3.7" test-log = "0.2.16" cargo_metadata = "0.19.2" semver = "1.0.26" +naga = "25.0.1" # This crate MUST NEVER be upgraded, we need this particular "first" version to support old rust-gpu builds legacy_target_specs = { package = "rustc_codegen_spirv-target-specs", version = "0.9.0", features = ["include_str"] } diff --git a/crates/cargo-gpu/Cargo.toml b/crates/cargo-gpu/Cargo.toml index 9152876..1e267de 100644 --- a/crates/cargo-gpu/Cargo.toml +++ b/crates/cargo-gpu/Cargo.toml @@ -10,6 +10,12 @@ build = "build.rs" default-run = "cargo-gpu" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +# Enable naga transpiling +naga = ["dep:naga"] +# Enable naga transpiling to wgsl +wgsl-out = ["naga", "naga/wgsl-out"] + [dependencies] cargo_metadata.workspace = true anyhow.workspace = true @@ -24,9 +30,11 @@ serde.workspace = true serde_json.workspace = true crossterm.workspace = true semver.workspace = true +naga = { workspace = true, optional = true, features = ["spv-in"] } [dev-dependencies] test-log.workspace = true +cargo_metadata = { workspace = true, features = ["builder"] } [lints] workspace = true diff --git a/crates/cargo-gpu/src/lib.rs b/crates/cargo-gpu/src/lib.rs index b3adabf..c5293b2 100644 --- a/crates/cargo-gpu/src/lib.rs +++ b/crates/cargo-gpu/src/lib.rs @@ -65,11 +65,15 @@ mod legacy_target_specs; mod linkage; mod lockfile; mod metadata; +#[cfg(feature = "naga")] +mod naga_transpile; mod show; mod spirv_source; mod test; pub use install::*; +#[cfg(feature = "naga")] +pub use naga_transpile::*; pub use spirv_builder; /// Central function to write to the user. diff --git a/crates/cargo-gpu/src/linkage.rs b/crates/cargo-gpu/src/linkage.rs index 346bdaf..9a2eeb8 100644 --- a/crates/cargo-gpu/src/linkage.rs +++ b/crates/cargo-gpu/src/linkage.rs @@ -23,8 +23,13 @@ impl Linkage { .map(|comp| comp.as_os_str().to_string_lossy()) .collect::>() .join("/"), - wgsl_entry_point: entry_point.as_ref().replace("::", ""), + wgsl_entry_point: spv_entry_point_to_wgsl(entry_point.as_ref()), entry_point: entry_point.as_ref().to_owned(), } } } + +/// Convert a spirv entry point to a valid wgsl entry point +pub fn spv_entry_point_to_wgsl(entry_point: &str) -> String { + entry_point.replace("::", "") +} diff --git a/crates/cargo-gpu/src/naga_transpile.rs b/crates/cargo-gpu/src/naga_transpile.rs new file mode 100644 index 0000000..2ac8d00 --- /dev/null +++ b/crates/cargo-gpu/src/naga_transpile.rs @@ -0,0 +1,123 @@ +//! naga transpiling to wgsl support, hidden behind feature `naga` + +use anyhow::Context as _; +use naga::error::ShaderError; +use naga::valid::Capabilities; +use naga::valid::ModuleInfo; +use naga::Module; +use spirv_builder::{CompileResult, GenericCompileResult}; +use std::path::{Path, PathBuf}; + +pub use naga; + +/// Naga [`Module`] with [`ModuleInfo`] +#[derive(Clone, Debug)] +#[expect( + clippy::exhaustive_structs, + reason = "never adding private members to this struct" +)] +pub struct NagaModule { + /// path to the original spv + pub spv_path: PathBuf, + /// naga shader [`Module`] + pub module: Module, + /// naga [`ModuleInfo`] from validation + pub info: ModuleInfo, +} + +/// convert a single spv file to a wgsl file using naga +fn parse_spv(spv_src: &Path, capabilities: Capabilities) -> anyhow::Result { + let inner = || -> anyhow::Result<_> { + let spv_bytes = std::fs::read(spv_src).context("could not read spv file")?; + let opts = naga::front::spv::Options::default(); + let module = naga::front::spv::parse_u8_slice(&spv_bytes, &opts) + .map_err(|err| ShaderError { + source: String::new(), + label: None, + inner: Box::new(err), + }) + .context("naga could not parse spv")?; + let mut validator = + naga::valid::Validator::new(naga::valid::ValidationFlags::default(), capabilities); + let info = validator + .validate(&module) + .map_err(|err| ShaderError { + source: String::new(), + label: None, + inner: Box::new(err), + }) + .context("validation of naga module failed")?; + Ok(NagaModule { + module, + info, + spv_path: PathBuf::from(spv_src), + }) + }; + inner().with_context(|| format!("parsing spv '{}' failed", spv_src.display())) +} + +/// Extension trait for naga transpiling +pub trait CompileResultNagaExt { + /// Transpile the spirv binaries to some other format using [`naga`]. + /// + /// # Errors + /// [`naga`] transpile may error in various ways + fn naga_transpile(&self, capabilities: Capabilities) -> anyhow::Result; +} + +impl CompileResultNagaExt for CompileResult { + #[inline] + fn naga_transpile(&self, capabilities: Capabilities) -> anyhow::Result { + Ok(NagaTranspile(self.try_map( + |entry| Ok(entry.clone()), + |spv| parse_spv(spv, capabilities), + )?)) + } +} + +/// Main struct for naga transpilation +#[expect( + clippy::exhaustive_structs, + reason = "never adding private members to this struct" +)] +pub struct NagaTranspile(pub GenericCompileResult); + +/// feature gate `wgsl-out` +#[cfg(feature = "wgsl-out")] +mod wgsl_out { + use crate::NagaTranspile; + use anyhow::Context as _; + use naga::back::wgsl::WriterFlags; + use spirv_builder::CompileResult; + + impl NagaTranspile { + /// Transpile to wgsl source code, typically for webgpu compatibility. + /// + /// Returns a [`CompileResult`] of wgsl source code files and their associated wgsl entry points. + /// + /// # Errors + /// converting naga module to wgsl may fail + #[inline] + pub fn to_wgsl(&self, writer_flags: WriterFlags) -> anyhow::Result { + self.0.try_map( + |entry| Ok(crate::linkage::spv_entry_point_to_wgsl(entry)), + |module| { + let inner = || -> anyhow::Result<_> { + let wgsl_dst = module.spv_path.with_extension("wgsl"); + let wgsl = naga::back::wgsl::write_string( + &module.module, + &module.info, + writer_flags, + ) + .context("naga conversion to wgsl failed")?; + std::fs::write(&wgsl_dst, wgsl).context("failed to write wgsl file")?; + Ok(wgsl_dst) + }; + inner().with_context(|| { + format!("transpiling to wgsl '{}'", module.spv_path.display()) + }) + }, + ) + } + } +} diff --git a/crates/cargo-gpu/src/spirv_source.rs b/crates/cargo-gpu/src/spirv_source.rs index 28e93bd..e21a075 100644 --- a/crates/cargo-gpu/src/spirv_source.rs +++ b/crates/cargo-gpu/src/spirv_source.rs @@ -88,8 +88,12 @@ impl SpirvSource { Self::CratesIO(Version::parse(rust_gpu_version)?) } } else { - Self::get_rust_gpu_deps_from_shader(shader_crate_path) - .context("get_rust_gpu_deps_from_shader")? + Self::get_rust_gpu_deps_from_shader(shader_crate_path).with_context(|| { + format!( + "get spirv-std dependency from shader crate '{}'", + shader_crate_path.display() + ) + })? }; Ok(source) } @@ -144,8 +148,8 @@ impl SpirvSource { let parse_git = || { let link = &source.repr.get(4..)?; let sharp_index = link.find('#')?; - let question_mark_index = link.find('?')?; - let url = link.get(..question_mark_index)?.to_owned(); + let url_end = link.find('?').unwrap_or(sharp_index); + let url = link.get(..url_end)?.to_owned(); let rev = link.get(sharp_index + 1..)?.to_owned(); Some(Self::Git { url, rev }) }; @@ -244,6 +248,7 @@ pub fn get_channel_from_rustc_codegen_spirv_build_script( #[cfg(test)] mod test { use super::*; + use cargo_metadata::{PackageBuilder, PackageId, Source}; #[test_log::test] fn parsing_spirv_std_dep_for_shader_template() { @@ -277,4 +282,50 @@ mod test { .unwrap(); assert_eq!("https___github_com_Rust-GPU_rust-gpu+86fc4803", &name); } + + #[test_log::test] + fn parse_git_with_rev() { + let source = parse_git( + "git+https://github.com/Rust-GPU/rust-gpu?rev=86fc48032c4cd4afb74f1d81ae859711d20386a1#86fc4803", + ); + assert_eq!( + source, + SpirvSource::Git { + url: "https://github.com/Rust-GPU/rust-gpu".to_owned(), + rev: "86fc4803".to_owned(), + } + ) + } + + #[test_log::test] + fn parse_git_no_question_mark() { + // taken directly from Graphite + let source = parse_git( + "git+https://github.com/Rust-GPU/rust-gpu.git#6e2c84d4fe64e32df4c060c5a7f3e35a32e45421", + ); + assert_eq!( + source, + SpirvSource::Git { + url: "https://github.com/Rust-GPU/rust-gpu.git".to_owned(), + rev: "6e2c84d4fe64e32df4c060c5a7f3e35a32e45421".to_owned(), + } + ) + } + + fn parse_git(source: &str) -> SpirvSource { + let package = PackageBuilder::new( + "spirv-std", + Version::new(0, 9, 0), + PackageId { + repr: "".to_owned(), + }, + "", + ) + .source(Some(Source { + repr: source.to_owned(), + })) + .build() + .unwrap(); + SpirvSource::parse_spirv_std_source_and_version(&package).unwrap() + } }