tirbofish/dropbear · commit
98da2703dad458f6962009706a1af3be9e59e1fd
Merge pull request #86 from tirbofish/slang-shaders
feature: slang shaders
jarvis, run github actions
Signature present but could not be verified.
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", "use-wgpu"] } anyhow.workspace = true app_dirs2.workspace = true @@ -53,3 +54,6 @@ rfd.workspace = true version = "0.25" default-features = false features = ["png", "jpeg"] + +[build-dependencies] +slank = { path = "../slank", features = ["download-slang"] } @@ -0,0 +1,19 @@ +use slank::{SlangShaderBuilder, SlangTarget}; + +fn main() { + // to copy paste: + // let shader = Shader::from_slang(graphics.clone(), &slank::compiled::CompiledSlangShader::from_bytes("light cube", include_slang!("light_cube"))); + + SlangShaderBuilder::new("light_cube") + .add_source_path("src/pipelines/shaders/light.slang").unwrap() + .compile_to_out_dir(SlangTarget::SpirV).unwrap(); + + SlangShaderBuilder::new("blit_shader") + .add_source_path("src/pipelines/shaders/blit.slang").unwrap() + .compile_to_out_dir(SlangTarget::SpirV).unwrap(); + + // unable to do mipmap.slang because it required texture_storage_2d, one write and one read. + // just wasn't possible with slang :( + + +} @@ -1,4 +1,6 @@ -use crate::texture::Texture; +use slank::{include_slang, utils::WgpuUtils}; + +use crate::{texture::Texture}; pub struct MipMapper { blit_mipmap: wgpu::RenderPipeline, @@ -10,10 +12,10 @@ pub struct MipMapper { impl MipMapper { pub fn new(device: &wgpu::Device) -> Self { - let blit_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("mipmap blit shader"), - source: wgpu::ShaderSource::Wgsl(include_str!("pipelines/shaders/blit.wgsl").into()), - }); + let blit_shader = device.create_shader_module(slank::compiled::CompiledSlangShader::from_bytes( + "mipmap blit_shader", + include_slang!("blit_shader") + ).create_wgpu_shader()); // Keep this SRGB so we can render directly into the SRGB textures we create for materials. let blit_format = Texture::TEXTURE_FORMAT; @@ -88,10 +90,12 @@ impl MipMapper { bind_group_layouts: &[&storage_texture_layout], push_constant_ranges: &[], }); + let compute_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("mipmap compute shader"), source: wgpu::ShaderSource::Wgsl(include_str!("pipelines/shaders/mipmap.wgsl").into()), }); + let compute_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor { label: Some("Mipmapper"), layout: Some(&pipeline_layout), @@ -1,6 +1,7 @@ use std::sync::Arc; use std::mem::size_of; use glam::DMat4; +use slank::include_slang; use wgpu::{BufferAddress, VertexAttribute, VertexFormat}; use crate::buffer::{StorageBuffer, UniformBuffer}; use crate::entity::{EntityTransform, Transform}; @@ -26,11 +27,7 @@ pub struct LightCubePipeline { impl DropbearShaderPipeline for LightCubePipeline { fn new(graphics: Arc<SharedGraphicsContext>) -> Self { - let shader = Shader::new( - graphics.clone(), - include_str!("shaders/light.wgsl"), - Some("light cube shader"), - ); + let shader = Shader::from_slang(graphics.clone(), &slank::compiled::CompiledSlangShader::from_bytes("light cube", include_slang!("light_cube"))); let pipeline_layout = graphics.device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("light cube pipeline layout"), @@ -0,0 +1,35 @@ +#include "dropbear.slang" + +struct VertexOutput { + float4 clip_position : SV_Position; + + [[vk_location(0)]] + float2 uv; +}; + +[[vk_binding(0, 0)]] +Texture2D tex; + +[[vk_binding(1, 0)]] +SamplerState tex_sampler; + +[shader("vertex")] +VertexOutput vs_main( + uint in_vertex_index: SV_VulkanVertexID +) { + VertexOutput output; + + float x = float((in_vertex_index << 1u) & 2u); + float y = float(in_vertex_index & 2u); + + output.clip_position = float4(x * 2.0 - 1.0, y * 2.0 - 1.0, 0.0, 1.0); + output.uv = float2(x, 1.0 - y); + + return output; +} + +[shader("fragment")] +float4 fs_main(VertexOutput input) : SV_Target0 { + float4 s = tex.Sample(tex_sampler, input.uv); + return s; +} @@ -1,28 +0,0 @@ -struct VertexOutput { - @builtin(position) clip_position: vec4<f32>, - @location(0) uv: vec2<f32>, -} - -@group(0) @binding(0) -var tex: texture_2d<f32>; -@group(0) @binding(1) -var tex_sampler: sampler; - -@vertex -fn vs_main( - @builtin(vertex_index) in_vertex_index: u32, -) -> VertexOutput { - var out: VertexOutput; - // Create fullscreen triangle - let x = f32((in_vertex_index << 1u) & 2u); - let y = f32(in_vertex_index & 2u); - out.clip_position = vec4<f32>(x * 2.0 - 1.0, y * 2.0 - 1.0, 0.0, 1.0); - out.uv = vec2<f32>(x, 1.0 - y); - return out; -} - -@fragment -fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { - let s = textureSample(tex, tex_sampler, in.uv); - return s; -} @@ -0,0 +1,45 @@ +struct Globals { + uint num_lights; + float ambient_strength; +}; + +/// A standard light descriptor struct. +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; +}; + +struct MaterialUniform { + // for stuff like tinting + float4 colour; + + // scales incoming UVs before sampling + float2 uv_tiling; +}; + +/// Converts a wgpu matrix (in the form of 4 float4) into a matrix usable for Slang. +float4x4 vs_input_matrix( + float4 mat1, + float4 mat2, + float4 mat3, + float4 mat4 +) { + return transpose(float4x4( + mat1, + mat2, + mat3, + mat4 + )); +} @@ -0,0 +1,51 @@ +#include "dropbear.slang" + +[[vk::binding(0, 0)]] +ConstantBuffer<CameraUniform> camera; + +[[vk::binding(0, 1)]] +ConstantBuffer<Light> light; + +struct VertexInput { + [[vk::location(0)]] + float3 position : POSITION; + + [[vk::location(5)]] + float4 model_matrix_0 : TEXCOORD5; + + [[vk::location(6)]] + float4 model_matrix_1 : TEXCOORD6; + + [[vk::location(7)]] + float4 model_matrix_2 : TEXCOORD7; + + [[vk::location(8)]] + float4 model_matrix_3 : TEXCOORD8; +}; + +struct VertexOutput { + float4 clip_position : SV_Position; + + [[vk::location(0)]] + float3 color : COLOR0; +}; + +[shader("vertex")] +VertexOutput vs_main(VertexInput input) { + float4x4 model_matrix = vs_input_matrix( + 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); +} @@ -1,62 +0,0 @@ -// Shader for rendering the white cube for lighting (or diff depending on colour) - -struct Light { - position: vec4<f32>, - direction: vec4<f32>, // x, y, z, outer_cutoff_angle - color: vec4<f32>, // r, g, b, light_type (0, 1, 2) - constant: f32, - lin: f32, - quadratic: f32, - cutoff: f32, -} - -struct CameraUniform { - view_pos: vec4<f32>, - view_proj: mat4x4<f32>, -}; - -@group(0) @binding(0) -var<uniform> camera: CameraUniform; - -@group(1) @binding(0) -var<uniform> light: Light; - -struct InstanceInput { - @location(5) model_matrix_0: vec4<f32>, - @location(6) model_matrix_1: vec4<f32>, - @location(7) model_matrix_2: vec4<f32>, - @location(8) model_matrix_3: vec4<f32>, -} - -struct VertexInput { - @location(0) position: vec3<f32>, -}; - -struct VertexOutput { - @builtin(position) clip_position: vec4<f32>, - @location(0) color: vec3<f32>, -}; - -@vertex -fn vs_main( - model: VertexInput, - instance: InstanceInput, -) -> VertexOutput { - let model_matrix = mat4x4<f32>( - instance.model_matrix_0, - instance.model_matrix_1, - instance.model_matrix_2, - instance.model_matrix_3, - ); - var out: VertexOutput; - out.clip_position = camera.view_proj * model_matrix * vec4<f32>(model.position, 1.0); - out.color = light.color.xyz; - return out; -} - -// Fragment shaders - -@fragment -fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { - return vec4<f32>(in.color, 1.0); -} @@ -3,6 +3,7 @@ use std::ops::Deref; use crate::graphics::SharedGraphicsContext; use std::sync::Arc; +use slank::{compiled::CompiledSlangShader, utils::WgpuUtils}; use wgpu::ShaderModule; /// A nice little struct that stored basic information about a WGPU shaders. @@ -47,6 +48,8 @@ impl Shader { log::debug!("Created new shaders under the label: {:?}", label); + slank::compiled::CompiledSlangShader::from_bytes("light cube", slank::include_slang!("light_cube")); + Self { label: match label { Some(label) => label.into(), @@ -56,4 +59,18 @@ impl Shader { content: shader_file_contents.to_string(), } } + + pub fn from_slang(graphics: Arc<SharedGraphicsContext>, shader: &CompiledSlangShader) -> Self { + let module = graphics + .device + .create_shader_module(shader.create_wgpu_shader()); + + log::debug!("Created new shaders [slang] under the label: {}", shader.label()); + + Self { + label: shader.label().clone(), + module, + content: String::from_utf8(shader.source.clone()).unwrap_or_default(), + } + } } @@ -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,47 @@ +use std::path::Path; + +pub struct CompiledSlangShader { + pub(crate) label: String, + 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(label: String, args: String, source: Vec<u8>) -> Self { + Self { + args, + source, + label, + } + } + + /// Fetches the command line arguments provided into [crate::SlangShaderBuilder] + pub fn args(&self) -> String { + self.args.clone() + } + + /// Creates a [CompiledSlangShader] from raw bytes. + /// + /// This is useful when loading shaders compiled at build time using the + /// `include_slang!` macro. + pub fn from_bytes(label: &str, source: &[u8]) -> Self { + Self { + label: label.to_string(), + args: String::new(), + source: source.to_vec(), + } + } + + pub fn label(&self) -> String { + self.label.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,577 @@ +//! slank - slangc for rust. +//! +//! Compiles slang code during build and stores the shaders locally (or in the crate with [`include_slang`]) +//! +//! Check out [`SlangShaderBuilder`] to get started. + +/// Fetches the slang file (located in {OUT_DIR}/{label}.spv) (assuming it is compiled as .spv) +/// and includes the bytes of the file. +#[macro_export] +macro_rules! include_slang { + ($label:expr) => { + include_bytes!(concat!(env!("OUT_DIR"), "/", $label, ".spv")) + }; +} + +/// Fetches the path of the shader (with the same label) and returns it to you. +#[macro_export] +macro_rules! include_slang_path { + ($label:expr) => { + concat!(env!("OUT_DIR"), "/", $label, ".spv") + }; +} + +pub mod compiled; +pub mod utils; + +use std::{fmt::Display, 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 +/// +/// Add `slank` to your `[build-dependencies]`. +/// +/// In your `build.rs`: +/// ```rust,no_run +/// use slank::{ShaderStage, SlangShaderBuilder, SlangTarget}; +/// use std::path::Path; +/// +/// fn main() { +/// let out_dir = std::env::var("OUT_DIR").unwrap(); +/// let dest_path = Path::new(&out_dir).join("shader.spv"); +/// +/// SlangShaderBuilder::new("shader_label") +/// .add_source_path("src/shader.slang").unwrap() +/// .entry_with_stage("vs_main", ShaderStage::Vertex) +/// .entry_with_stage("fs_main", ShaderStage::Fragment) +/// .build(SlangTarget::SpirV).unwrap() +/// .output(&dest_path).unwrap(); +/// +/// println!("cargo:rerun-if-changed=src/shader.slang"); +/// } +/// ``` +/// +/// Then in your main code: +/// ```rust,ignore +/// let shader_bytes = include_bytes!(concat!(env!("OUT_DIR"), "/shader.spv")); +/// ``` +pub struct SlangShaderBuilder { + label: String, + sources: Vec<SourceFile>, + entries: Vec<EntryPoint>, + profile: Option<Profile>, + additional_args: Vec<String>, +} + +impl SlangShaderBuilder { + /// Creates a new instance of a [SlangShaderBuilder]. + pub fn new(label: &str) -> Self { + Self { + sources: Vec::new(), + entries: Vec::new(), + label: label.to_string(), + profile: None, + additional_args: 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 with_profile(mut self, profile: Profile) -> Self { + self.profile = Some(profile); + self + } + + /// In the case that there was an argument not available to this builder, you can + /// manually provide it here. + pub fn with_additional_args(mut self, args: &[&str]) -> Self { + self.additional_args = args.to_vec().iter().map(|v| v.to_string()).collect::<Vec<_>>(); + self + } + + pub fn compile_to_out_dir(self, target: SlangTarget) -> anyhow::Result<()> { + let label = self.label.clone(); + let compiled = self.build(target)?; + let out_dir = std::env::var("OUT_DIR") + .map_err(|_| anyhow::anyhow!("OUT_DIR not set. Are you running this from build.rs?"))?; + let dest = Path::new(&out_dir).join(format!("{}.spv", label)); + compiled.output(&dest).map_err(Into::into) + } + + 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())?; + + 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())?; + } + } + } + } + + cmd.args(&self.additional_args); + write!(&mut args_record, " {}", self.additional_args.join(" "))?; + + 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(self.label, args_record, binary_output)) + } +} + +impl Default for SlangShaderBuilder { + fn default() -> Self { + Self::new("no named shader") + } +} + +#[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, + /// Most optimal for WGPU and Vulkan/ + 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)) + } +} + +pub enum Profile { + Sm(String), + Glsl(String), + Vs(String), + Hs(String), + Ds(String), + Gs(String), + Ps(String), +} + +impl Display for Profile { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let string = match self { + Profile::Sm(v) => format!("sm_{v}"), + Profile::Glsl(v) => format!("glsl_{v}"), + Profile::Vs(v) => format!("vs_{v}"), + Profile::Hs(v) => format!("hs_{v}"), + Profile::Ds(v) => format!("ds_{v}"), + Profile::Gs(v) => format!("gs_{v}"), + Profile::Ps(v) => format!("ps_{v}"), + }; + write!(f, "{}", string) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_compile() { + let result = SlangShaderBuilder::new("light") + .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); + assert!(result.is_ok()); + } + + #[test] + fn test_from_bytes() { + let bytes = vec![1, 2, 3, 4]; + let shader = CompiledSlangShader::from_bytes("idk", &bytes); + assert_eq!(shader.source, bytes); + assert_eq!(shader.label(), "unknown"); + } +} @@ -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) -> wgpu::ShaderModuleDescriptor<'_>; +} + +#[cfg(feature = "use-wgpu")] +impl WgpuUtils for crate::CompiledSlangShader { + fn create_wgpu_shader(&self) -> wgpu::ShaderModuleDescriptor<'_> { + wgpu::ShaderModuleDescriptor { + label: Some(self.label.as_str()), + source: wgpu::util::make_spirv(self.source.as_ref()), + } + } +} @@ -0,0 +1,56 @@ +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); +}