tirbofish/dropbear · diff
ensured that SlangShaderBuilder works, made the starts of it. need to implement more arguments. Unverified
@@ -75,6 +75,8 @@ postcard = { version = "1.1"} pollster = "0.4" yakui = { git = "https://github.com/SecondHalfGames/yakui", rev = "ddf7614" } yakui-wgpu = { git = "https://github.com/SecondHalfGames/yakui", rev = "ddf7614" } +thiserror = "2.0" +tempfile = "3.24" [workspace.dependencies.image] version = "0.25" @@ -12,6 +12,7 @@ readme.workspace = true dropbear_future-queue = { path = "../dropbear_future-queue" } dropbear-traits = { path = "../dropbear-traits" } dropbear-macro = { path = "../dropbear-macro" } +slank = { path = "../slank", features = ["download-slang"] } anyhow.workspace = true app_dirs2.workspace = true @@ -0,0 +1,57 @@ +struct Light { + float4 position; + float4 direction; // x, y, z, outer_cutoff_angle + float4 color; // r, g, b, light_type (0, 1, 2) + + float constant; + float lin; + float quadratic; + + float cutoff; +}; + + +struct CameraUniform { + float4 view_pos; + float4x4 view_proj; +}; + +[[vk::binding(0, 0)]] +ConstantBuffer<CameraUniform> camera; + +[[vk::binding(1, 0)]] +ConstantBuffer<Light> light; + +struct VertexInput { + float3 position : POSITION; + float4 model_matrix_0 : TEXCOORD5; + float4 model_matrix_1 : TEXCOORD6; + float4 model_matrix_2 : TEXCOORD7; + float4 model_matrix_3 : TEXCOORD8; +}; + +struct VertexOutput { + float4 clip_position : SV_Position; + float3 color : COLOR0; +}; + + +[shader("vertex")] +VertexOutput vs_main(VertexInput input) { + float4x4 model_matrix = float4x4( + input.model_matrix_0, + input.model_matrix_1, + input.model_matrix_2, + input.model_matrix_3 + ); + + VertexOutput output; + output.clip_position = mul(camera.view_proj, mul(model_matrix, float4(input.position, 1.0))); + output.color = light.color.xyz; + return output; +} + +[shader("fragment")] +float4 fs_main(VertexOutput input) : SV_Target0 { + return float4(input.color, 1.0); +} @@ -0,0 +1,31 @@ +[package] +name = "slank" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +readme = "README.md" + +[dependencies] + +wgpu = { workspace = true, optional = true } +thiserror.workspace = true +tempfile.workspace = true +anyhow.workspace = true + +[features] +# this feature downloads the slang compiler for the correct operating system and stores it for later. +download-slang = [] + +# allows for the wgpu dependency to be available. +use-wgpu = ["wgpu/default", "wgpu/spirv"] + +[build-dependencies] +dirs = "6.0.0" +reqwest = { version = "0.13.1", features = ["blocking", "json"] } +zip = "7.2.0" +flate2 = "1.1.8" +tar = "0.4.44" +anyhow.workspace = true +serde = { workspace = true } +serde_json = "1.0" @@ -0,0 +1,4 @@ +# slank + +A rust version of the slang compiler, ready to be used by wgpu, or vulkan (or any graphics library really, i personally +use wgpu). @@ -0,0 +1,283 @@ +use std::path::PathBuf; +use std::process::Command; + +fn main() -> anyhow::Result<()> { + println!("cargo:rerun-if-changed=build.rs"); + + let path = find_or_download_slangc()?; + + println!("cargo:rustc-env=SLANG_DIR={}", path.display()); + println!("cargo:warning=Found slangc at: {}", path.display()); + Ok(()) +} + +fn find_or_download_slangc() -> anyhow::Result<PathBuf> { + if let Ok(path) = std::env::var("SLANG_DIR") { + let path = PathBuf::from(path); + let exe_path = path.join("bin").join("slangc"); + if is_valid_slangc(&exe_path)? { + return Ok(path); + } else { + anyhow::bail!("Not valid slangc installation at: {}", path.display()); + } + } + + if let Some(exe_path) = find_in_path("slangc") { + if is_valid_slangc(&exe_path)? { + if let Some(root) = exe_path.parent().and_then(|p| p.parent()) { + return Ok(root.to_path_buf()); + } + } + } + + if let Some(exe_path) = check_cached_download() { + if is_valid_slangc(&exe_path)? { + if let Some(root) = exe_path.parent().and_then(|p| p.parent()) { + return Ok(root.to_path_buf()); + } + } + } + + #[cfg(feature = "download-slang")] + { + return download_slang(); + } + + #[cfg(not(feature = "download-slang"))] + { + Err(anyhow::anyhow!( + "slangc not found. Either:\n\ + 1. Install slangc and add it to PATH\n\ + 2. Set SLANG_DIR environment variable\n\ + 3. Enable the 'download-slang' feature" + )) + } +} + +fn is_valid_slangc(path: &PathBuf) -> anyhow::Result<bool> { + if !path.exists() { + return Ok(false); + } + + Ok(Command::new(path) + .arg("-version") + .output() + .map(|output| output.status.success()) + .unwrap_or(false)) +} + +fn find_in_path(name: &str) -> Option<PathBuf> { + std::env::var_os("PATH").and_then(|paths| { + std::env::split_paths(&paths) + .filter_map(|dir| { + let full_path = dir.join(name); + // On Windows, also try with .exe extension + #[cfg(target_os = "windows")] + { + if full_path.with_extension("exe").exists() { + return Some(full_path.with_extension("exe")); + } + } + if full_path.exists() { + Some(full_path) + } else { + None + } + }) + .next() + }) +} + +fn check_cached_download() -> Option<PathBuf> { + if let Ok(out_dir) = std::env::var("OUT_DIR") { + let cached = PathBuf::from(out_dir) + .join("slangc") + .join("bin") + .join(if cfg!(target_os = "windows") { + "slangc.exe" + } else { + "slangc" + }); + + if cached.exists() { + return Some(cached); + } + } + + if let Some(cache_dir) = dirs::cache_dir() { + let cached = cache_dir + .join("slank") + .join("slangc") + .join("bin") + .join(if cfg!(target_os = "windows") { + "slangc.exe" + } else { + "slangc" + }); + + if cached.exists() { + return Some(cached); + } + } + + None +} + +#[cfg(feature = "download-slang")] +fn download_slang() -> anyhow::Result<PathBuf> { + use std::fs; + + println!("cargo:warning=Downloading slangc compiler..."); + + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap(); + + let version = get_latest_slang_version()?; + + let (download_url, archive_name) = get_download_url(&version, &target_os, &target_arch)?; + + let cache_dir = dirs::cache_dir() + .ok_or_else(|| anyhow::anyhow!("Could not find cache directory"))? + .join("slank") + .join("slangc"); + + fs::create_dir_all(&cache_dir) + .map_err(|e| anyhow::anyhow!("Failed to create cache directory: {}", e))?; + + let archive_path = cache_dir.join(&archive_name); + + if !archive_path.exists() { + println!("cargo:warning=Downloading from: {}", download_url); + download_file(&download_url, &archive_path)?; + } else { + println!("cargo:warning=Using cached download: {}", archive_path.display()); + } + + extract_archive(&archive_path, &cache_dir)?; + + let slangc_exe = cache_dir + .join("bin") + .join(if cfg!(target_os = "windows") { + "slangc.exe" + } else { + "slangc" + }); + + if !slangc_exe.exists() { + return Err(anyhow::anyhow!("slangc not found after extraction at: {}", slangc_exe.display())); + } + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&slangc_exe) + .map_err(|e| anyhow::anyhow!("Failed to get permissions: {}", e))? + .permissions(); + perms.set_mode(0o755); + fs::set_permissions(&slangc_exe, perms) + .map_err(|e| anyhow::anyhow!("Failed to set permissions: {}", e))?; + } + + Ok(cache_dir) +} + +#[cfg(feature = "download-slang")] +fn get_latest_slang_version() -> anyhow::Result<String> { + use serde::Deserialize; + + #[derive(Deserialize)] + struct Release { + tag_name: String, + } + + let client = reqwest::blocking::Client::new(); + let response = client + .get("https://api.github.com/repos/shader-slang/slang/releases/latest") + .header(reqwest::header::USER_AGENT, "dropbear-slank-build") + .send() + .map_err(|e| anyhow::anyhow!("Failed to fetch latest version from GitHub: {}", e))?; + + if !response.status().is_success() { + return Err(anyhow::anyhow!("GitHub API failed with status: {}", response.status())); + } + + let release: Release = response.json() + .map_err(|e| anyhow::anyhow!("Failed to parse GitHub response: {}", e))?; + + Ok(release.tag_name.trim_start_matches('v').to_string()) +} + +#[cfg(feature = "download-slang")] +fn get_download_url(version: &str, os: &str, arch: &str) -> anyhow::Result<(String, String)> { + let (platform, ext) = match (os, arch) { + ("windows", "x86_64") => ("windows-x86_64", "zip"), + ("windows", "aarch64") => ("windows-aarch64", "zip"), + ("linux", "x86_64") => ("linux-x86_64", "tar.gz"), + ("linux", "aarch64") => ("linux-aarch64", "tar.gz"), + ("macos", "x86_64") => ("macos-x86_64", "zip"), + ("macos", "aarch64") => ("macos-aarch64", "zip"), + _ => return Err(anyhow::anyhow!("Unsupported platform: {}-{}", os, arch)), + }; + + let archive_name = format!("slang-{}-{}.{}", version, platform, ext); + let url = format!( + "https://github.com/shader-slang/slang/releases/download/v{}/{}", + version, archive_name + ); + + Ok((url, archive_name)) +} + +#[cfg(feature = "download-slang")] +fn download_file(url: &str, dest: &PathBuf) -> anyhow::Result<()> { + use std::fs::File; + + let client = reqwest::blocking::Client::builder() + .timeout(std::time::Duration::from_secs(600)) + .build() + .map_err(|e| anyhow::anyhow!("Failed to create HTTP client: {}", e))?; + + let mut response = client.get(url) + .header(reqwest::header::USER_AGENT, "dropbear-slank-build") + .send() + .map_err(|e| anyhow::anyhow!("Failed to download: {}", e))?; + + if !response.status().is_success() { + return Err(anyhow::anyhow!("Download failed with status: {}", response.status())); + } + + let mut file = File::create(dest) + .map_err(|e| anyhow::anyhow!("Failed to create file: {}", e))?; + + response.copy_to(&mut file) + .map_err(|e| anyhow::anyhow!("Failed to save file content at {}: {}", dest.display(), e))?; + + Ok(()) +} + +#[cfg(feature = "download-slang")] +fn extract_archive(archive: &PathBuf, dest: &PathBuf) -> anyhow::Result<()> { + use std::fs::File; + + let file = File::open(archive) + .map_err(|e| anyhow::anyhow!("Failed to open archive: {}", e))?; + + if archive.extension().and_then(|s| s.to_str()) == Some("zip") { + let mut archive = zip::ZipArchive::new(file) + .map_err(|e| anyhow::anyhow!("Failed to read zip: {}", e))?; + + archive.extract(dest) + .map_err(|e| anyhow::anyhow!("Failed to extract zip: {}", e))?; + } else { + use flate2::read::GzDecoder; + use tar::Archive; + + let tar = GzDecoder::new(file); + let mut archive = Archive::new(tar); + + archive.unpack(dest) + .map_err(|e| anyhow::anyhow!("Failed to extract tar.gz: {}", e))?; + } + + Ok(()) +} @@ -0,0 +1,29 @@ +use std::path::Path; + +pub struct CompiledSlangShader { + args: String, + pub source: Vec<u8>, +} + +impl CompiledSlangShader { + /// Internal: Creates a new instance of [CompiledSlangShader] from the compiled args + /// of [crate::SlangShaderBuilder] + pub(crate) fn new(args: String, source: Vec<u8>) -> Self { + Self { + args, + source + } + } + + /// Fetches the command line arguments provided into [crate::SlangShaderBuilder] + pub fn args(&self) -> String { + self.args.clone() + } + + /// Writes the output to a file. + pub fn output(&self, path: impl AsRef<Path>) -> std::io::Result<()> { + let path = path.as_ref(); + + std::fs::write(path, &self.source) + } +} @@ -0,0 +1,479 @@ +//! slank - slangc for rust +//! +//! Check out [`SlangShaderBuilder`] to get started. + +pub mod compiled; + +pub mod utils; + +use std::path::{Path, PathBuf}; +use crate::compiled::CompiledSlangShader; + +#[derive(Debug, Clone)] +pub struct SourceFile { + pub path: Option<PathBuf>, + pub content: String, +} + +#[derive(Debug, Clone)] +pub struct EntryPoint { + pub name: String, + pub stage: Option<ShaderStage>, + /// Index into the sources vec - which file this entry point comes from + pub source_index: Option<usize>, +} + +/// Allows you to pass arguments and make an argument builder to chuck into the slangc +/// compiler. +/// +/// This is the entry point of the library. +/// # Usage +/// ```rust +/// use slank::{ShaderStage, SlangShaderBuilder, SlangTarget}; +/// +/// SlangShaderBuilder::new() +/// .add_source_str(include_str!("../test/light.slang")) +/// .entry_with_stage("vs_main", ShaderStage::Vertex) +/// .entry_with_stage("fs_main", ShaderStage::Fragment) +/// .build(SlangTarget::SpirV).unwrap(); +/// +/// ``` +pub struct SlangShaderBuilder { + sources: Vec<SourceFile>, + entries: Vec<EntryPoint>, +} + +impl SlangShaderBuilder { + /// Creates a new instance of a [SlangShaderBuilder]. + pub fn new() -> Self { + Self { + sources: Vec::new(), + entries: Vec::new(), + } + } + + /// Adds a source file by path. + /// + /// Returns the index of this source, which can be used with + /// `entry_from_source()` to associate entry points with specific files. + pub fn add_source_path(mut self, path: impl AsRef<Path>) -> Result<Self, std::io::Error> { + let path_buf = path.as_ref().to_path_buf(); + let content = std::fs::read_to_string(&path_buf)?; + + self.sources.push(SourceFile { + path: Some(path_buf), + content, + }); + + Ok(self) + } + + /// Adds a source file by path (non-consuming version). + pub fn source_path(&mut self, path: impl AsRef<Path>) -> Result<usize, std::io::Error> { + let path_buf = path.as_ref().to_path_buf(); + let content = std::fs::read_to_string(&path_buf)?; + + self.sources.push(SourceFile { + path: Some(path_buf), + content, + }); + + Ok(self.sources.len() - 1) + } + + /// Adds source code as a string. + /// + /// Returns the index of this source. + pub fn add_source_str(mut self, content: &str) -> Self { + self.sources.push(SourceFile { + path: None, + content: content.to_string(), + }); + self + } + + /// Adds source code as a string (non-consuming version). + pub fn source_str(&mut self, content: &str) -> usize { + self.sources.push(SourceFile { + path: None, + content: content.to_string(), + }); + self.sources.len() - 1 + } + + /// Adds multiple source files at once. + pub fn add_source_paths(mut self, paths: &[impl AsRef<Path>]) -> Result<Self, std::io::Error> { + for path in paths { + let path_buf = path.as_ref().to_path_buf(); + let content = std::fs::read_to_string(&path_buf)?; + + self.sources.push(SourceFile { + path: Some(path_buf), + content, + }); + } + Ok(self) + } + + /// Adds an entry point from the most recently added source. + /// + /// According to Slang docs: "the file associated with the entry point + /// will be the first one found when searching to the left in the command line." + pub fn entry(mut self, name: &str) -> Self { + let source_index = if self.sources.is_empty() { + None + } else { + Some(self.sources.len() - 1) + }; + + self.entries.push(EntryPoint { + name: name.to_string(), + stage: None, + source_index, + }); + self + } + + /// Adds an entry point with an explicit shader stage. + pub fn entry_with_stage(mut self, name: &str, stage: ShaderStage) -> Self { + let source_index = if self.sources.is_empty() { + None + } else { + Some(self.sources.len() - 1) + }; + + self.entries.push(EntryPoint { + name: name.to_string(), + stage: Some(stage), + source_index, + }); + self + } + + /// Adds an entry point from a specific source file (by index). + pub fn entry_from_source(mut self, name: &str, source_index: usize) -> Self { + self.entries.push(EntryPoint { + name: name.to_string(), + stage: None, + source_index: Some(source_index), + }); + self + } + + /// Adds an entry point with stage from a specific source file. + pub fn entry_from_source_with_stage( + mut self, + name: &str, + source_index: usize, + stage: ShaderStage + ) -> Self { + self.entries.push(EntryPoint { + name: name.to_string(), + stage: Some(stage), + source_index: Some(source_index), + }); + self + } + + pub fn build(self, target: SlangTarget) -> anyhow::Result<CompiledSlangShader> { + let slang_dir = PathBuf::from(env!("SLANG_DIR")); + let slangc_path = slang_dir.join("bin").join(if cfg!(windows) { "slangc.exe" } else { "slangc" }); + + if !slangc_path.exists() { + anyhow::bail!("slangc executable not found at {}", slangc_path.display()); + } + + let mut cmd = std::process::Command::new(slangc_path); + let mut args_record = String::from("slangc"); + + let mut temp_files = Vec::new(); + + cmd.arg("-target").arg(target.as_arg()); + + use std::fmt::Write; + write!(&mut args_record, " -target {}", target.as_arg()).unwrap(); + + for (source_idx, source) in self.sources.iter().enumerate() { + let path_to_use = if let Some(path) = &source.path { + path.clone() + } else { + let mut temp = tempfile::Builder::new() + .suffix(".slang") + .tempfile()?; + + use std::io::Write as IoWrite; + temp.write_all(source.content.as_bytes())?; + + let p = temp.path().to_path_buf(); + temp_files.push(temp); + p + }; + + cmd.arg(&path_to_use); + write!(&mut args_record, " {}", path_to_use.display())?; + + for entry in &self.entries { + if entry.source_index == Some(source_idx) { + cmd.arg("-entry").arg(&entry.name); + write!(&mut args_record, " -entry {}", entry.name)?; + + if let Some(stage) = entry.stage { + cmd.arg("-stage").arg(stage.as_arg()); + write!(&mut args_record, " -stage {}", stage.as_arg())?; + } + } + } + } + + let temp_dir = tempfile::tempdir()?; + let output_path = temp_dir.path().join("output"); + + cmd.arg("-o").arg(&output_path); + write!(&mut args_record, " -o {}", output_path.display())?; + println!("cmd: {:?}", cmd.get_args()); + let output = cmd.output()?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + return Err(anyhow::anyhow!("Compilation error: {}", stderr)); + } + + + let binary_output = std::fs::read(&output_path)?; + + Ok(CompiledSlangShader::new(args_record, binary_output)) + } +} + +impl Default for SlangShaderBuilder { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ShaderStage { + Vertex, + Fragment, + Compute, + // wgpu doesnt support anything below this + Geometry, + Hull, + Domain, + RayGen, + Intersection, + AnyHit, + ClosestHit, + Miss, + Callable, +} + +impl ShaderStage { + /// Convert to the slangc command-line argument + pub fn as_arg(&self) -> &'static str { + match self { + Self::Vertex => "vertex", + Self::Fragment => "fragment", + Self::Compute => "compute", + Self::Geometry => "geometry", + Self::Hull => "hull", + Self::Domain => "domain", + Self::RayGen => "raygeneration", + Self::Intersection => "intersection", + Self::AnyHit => "anyhit", + Self::ClosestHit => "closesthit", + Self::Miss => "miss", + Self::Callable => "callable", + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SlangTarget { + Unknown, + None, + + // HLSL/DirectX + Hlsl, + Dxbc, + DxbcAssembly, + Dxil, + DxilAssembly, + + // GLSL/Vulkan/SPIR-V + Glsl, + SpirV, + SpirVAssembly, + + // C/C++ + C, + Cpp, + CppHeader, + TorchBinding, + HostCpp, + + // Executables and Libraries + Executable, + ShaderSharedLibrary, + SharedLibrary, + + // CUDA + Cuda, + CudaHeader, + Ptx, + CuBin, + + // Host Callable + HostCallable, + ShaderObjectCode, + HostHostCallable, + + // Metal + Metal, + MetalLib, + MetalLibAssembly, + + // WebGPU + Wgsl, + WgslSpirVAssembly, + WgslSpirV, + + // Slang VM + SlangVm, + + // LLVM + HostObjectCode, + LlvmHostIr, + LlvmShaderIr, +} + +impl SlangTarget { + /// Convert to the slangc command-line argument + pub fn as_arg(&self) -> &'static str { + match self { + Self::Unknown => "unknown", + Self::None => "none", + Self::Hlsl => "hlsl", + Self::Dxbc => "dxbc", + Self::DxbcAssembly => "dxbc-asm", + Self::Dxil => "dxil", + Self::DxilAssembly => "dxil-asm", + Self::Glsl => "glsl", + Self::SpirV => "spirv", + Self::SpirVAssembly => "spirv-asm", + Self::C => "c", + Self::Cpp => "cpp", + Self::CppHeader => "hpp", + Self::TorchBinding => "torch-binding", + Self::HostCpp => "host-cpp", + Self::Executable => "exe", + Self::ShaderSharedLibrary => "shader-dll", + Self::SharedLibrary => "dll", + Self::Cuda => "cuda", + Self::CudaHeader => "cuh", + Self::Ptx => "ptx", + Self::CuBin => "cubin", + Self::HostCallable => "host-callable", + Self::ShaderObjectCode => "shader-object-code", + Self::HostHostCallable => "host-host-callable", + Self::Metal => "metal", + Self::MetalLib => "metallib", + Self::MetalLibAssembly => "metallib-asm", + Self::Wgsl => "wgsl", + Self::WgslSpirVAssembly => "wgsl-spirv-asm", + Self::WgslSpirV => "wgsl-spirv", + Self::SlangVm => "slang-vm", + Self::HostObjectCode => "host-object-code", + Self::LlvmHostIr => "llvm-ir", + Self::LlvmShaderIr => "llvm-shader-ir", + } + } + + /// Parse from a string (supports all aliases) + pub fn from_str(s: &str) -> Option<Self> { + match s { + "unknown" => Some(Self::Unknown), + "none" => Some(Self::None), + "hlsl" => Some(Self::Hlsl), + "dxbc" => Some(Self::Dxbc), + "dxbc-asm" | "dxbc-assembly" => Some(Self::DxbcAssembly), + "dxil" => Some(Self::Dxil), + "dxil-asm" | "dxil-assembly" => Some(Self::DxilAssembly), + "glsl" => Some(Self::Glsl), + "spirv" => Some(Self::SpirV), + "spirv-asm" | "spirv-assembly" => Some(Self::SpirVAssembly), + "c" => Some(Self::C), + "cpp" | "c++" | "cxx" => Some(Self::Cpp), + "hpp" => Some(Self::CppHeader), + "torch" | "torch-binding" | "torch-cpp" | "torch-cpp-binding" => Some(Self::TorchBinding), + "host-cpp" | "host-c++" | "host-cxx" => Some(Self::HostCpp), + "exe" | "executable" => Some(Self::Executable), + "shader-sharedlib" | "shader-sharedlibrary" | "shader-dll" => Some(Self::ShaderSharedLibrary), + "sharedlib" | "sharedlibrary" | "dll" => Some(Self::SharedLibrary), + "cuda" | "cu" => Some(Self::Cuda), + "cuh" => Some(Self::CudaHeader), + "ptx" => Some(Self::Ptx), + "cuobj" | "cubin" => Some(Self::CuBin), + "host-callable" | "callable" => Some(Self::HostCallable), + "object-code" | "shader-object-code" => Some(Self::ShaderObjectCode), + "host-host-callable" => Some(Self::HostHostCallable), + "metal" => Some(Self::Metal), + "metallib" => Some(Self::MetalLib), + "metallib-asm" => Some(Self::MetalLibAssembly), + "wgsl" => Some(Self::Wgsl), + "wgsl-spirv-asm" | "wgsl-spirv-assembly" => Some(Self::WgslSpirVAssembly), + "wgsl-spirv" => Some(Self::WgslSpirV), + "slangvm" | "slang-vm" => Some(Self::SlangVm), + "host-object-code" => Some(Self::HostObjectCode), + "llvm-host-ir" | "llvm-ir" => Some(Self::LlvmHostIr), + "llvm-shader-ir" => Some(Self::LlvmShaderIr), + _ => None, + } + } + + /// Check if this target produces binary output + pub fn is_binary(&self) -> bool { + matches!( + self, + Self::Dxbc | Self::Dxil | Self::SpirV | Self::Executable + | Self::ShaderSharedLibrary | Self::SharedLibrary | Self::CuBin + | Self::MetalLib | Self::WgslSpirV | Self::SlangVm + | Self::HostObjectCode | Self::ShaderObjectCode + ) + } + + /// Check if this target is suitable for wgpu + pub fn is_wgpu_compatible(&self) -> bool { + matches!(self, Self::SpirV | Self::Wgsl) + } +} + +impl std::fmt::Display for SlangTarget { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_arg()) + } +} + +impl std::str::FromStr for SlangTarget { + type Err = String; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + Self::from_str(s).ok_or_else(|| format!("Unknown Slang target: {}", s)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_from_str() { + let shader = SlangShaderBuilder::new() + .add_source_str(include_str!("../../dropbear-engine/src/pipelines/shaders/light.slang")) + .entry_with_stage("vs_main", ShaderStage::Vertex) + .entry_with_stage("fs_main", ShaderStage::Fragment) + .build(SlangTarget::SpirV).unwrap(); + + shader.output("/home/tirbofish/shader.spv").unwrap(); + } +} @@ -0,0 +1,16 @@ +/// WGPU based traits that are useful for those using the [wgpu] crate. +#[cfg(feature = "use-wgpu")] +pub trait WgpuUtils { + /// Creates a new [`wgpu::ShaderModuleDescriptor`] for you to create your own shaders with. + fn create_wgpu_shader(&self, label: Option<&str>) -> wgpu::ShaderModuleDescriptor; +} + +#[cfg(feature = "use-wgpu")] +impl WgpuUtils for crate::CompiledSlangShader { + fn create_wgpu_shader(&self, label: Option<&str>) -> wgpu::ShaderModuleDescriptor { + wgpu::ShaderModuleDescriptor { + label, + source: wgpu::util::make_spirv(self.source.as_ref()), + } + } +} @@ -0,0 +1,57 @@ +struct Light { + float4 position; + float4 direction; // x, y, z, outer_cutoff_angle + float4 color; // r, g, b, light_type (0, 1, 2) + + float constant; + float lin; + float quadratic; + + float cutoff; +}; + + +struct CameraUniform { + float4 view_pos; + float4x4 view_proj; +}; + +[[vk::binding(0, 0)]] +ConstantBuffer<CameraUniform> camera; + +[[vk::binding(1, 0)]] +ConstantBuffer<Light> light; + +struct VertexInput { + float3 position : POSITION; + float4 model_matrix_0 : TEXCOORD5; + float4 model_matrix_1 : TEXCOORD6; + float4 model_matrix_2 : TEXCOORD7; + float4 model_matrix_3 : TEXCOORD8; +}; + +struct VertexOutput { + float4 clip_position : SV_Position; + float3 color : COLOR0; +}; + + +[shader("vertex")] +VertexOutput vs_main(VertexInput input) { + float4x4 model_matrix = float4x4( + input.model_matrix_0, + input.model_matrix_1, + input.model_matrix_2, + input.model_matrix_3 + ); + + VertexOutput output; + output.clip_position = mul(camera.view_proj, mul(model_matrix, float4(input.position, 1.0))); + output.color = light.color.xyz; + return output; +} + +[shader("fragment")] +float4 fs_main(VertexOutput input) : SV_Target0 { + return float4(input.color, 1.0); +}