diff --git a/.gitattributes b/.gitattributes index cdb06f5..e3e4882 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,8 +33,6 @@ *.opus filter=lfs diff=lfs merge=lfs -text *.mp3 filter=lfs diff=lfs merge=lfs -text *.aiff filter=lfs diff=lfs merge=lfs -text -# Compiled shaders -*.spv filter=lfs diff=lfs merge=lfs -text # Fonts *.ttf filter=lfs diff=lfs merge=lfs -text *.otf filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 97800c9..a042f90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,9 @@ jobs: - name: Cache cargo artifacts uses: Swatinem/rust-cache@v2 + - name: Install shader compiler + run: sudo apt-get update && sudo apt-get install -y libshaderc-dev + - name: Check Rust Formatting run: cargo fmt --all -- --check diff --git a/Cargo.lock b/Cargo.lock index 0ad1db3..27409f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1285,6 +1285,15 @@ dependencies = [ "redox_syscall 0.9.0", ] +[[package]] +name = "link-cplusplus" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f78c730aaa7d0b9336a299029ea49f9ee53b0ed06e9202e8cb7db9bae7b8c82" +dependencies = [ + "cc", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -2153,6 +2162,7 @@ dependencies = [ "glam 0.33.2", "gpu-allocator", "raw-window-handle", + "shaderc", "shared", "thiserror 2.0.18", "tracing", @@ -2172,6 +2182,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + [[package]] name = "rustc-hash" version = "2.1.3" @@ -2441,6 +2457,29 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "shaderc" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ab2a6e36d1c1e2320c87e2b806a3e7b0dffaa67b82c14a39dad6cf7637208ae" +dependencies = [ + "libc", + "shaderc-sys", +] + +[[package]] +name = "shaderc-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdceb85b2c6d2c27b95ffe2d341063dfded0aca8046f7f60c544bbeaeaf8bcae" +dependencies = [ + "cmake", + "libc", + "link-cplusplus", + "pkg-config", + "roxmltree", +] + [[package]] name = "sharded-slab" version = "0.1.7" diff --git a/assets/shaders/cube.frag.spv b/assets/shaders/cube.frag.spv deleted file mode 100644 index 9a6a893..0000000 --- a/assets/shaders/cube.frag.spv +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f3f5c483dddf88cd90dbb62aa579c7f815b7ff304407d27c19a9a52cc450b4bb -size 572 diff --git a/assets/shaders/cube.vert.spv b/assets/shaders/cube.vert.spv deleted file mode 100644 index b9569a9..0000000 --- a/assets/shaders/cube.vert.spv +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:17461a207a6e1d6ba3e2b050aaa83ad0fb880a29e54e42295a1d43cbdfbdc115 -size 1888 diff --git a/crates/renderer/Cargo.toml b/crates/renderer/Cargo.toml index 9e31cf1..45a04b6 100644 --- a/crates/renderer/Cargo.toml +++ b/crates/renderer/Cargo.toml @@ -8,6 +8,9 @@ version.workspace = true [lints] workspace = true +[build-dependencies] +shaderc = "0.10.1" + [dependencies] ash = "0.38.0" ash-window.workspace = true diff --git a/crates/renderer/build.rs b/crates/renderer/build.rs new file mode 100644 index 0000000..8bdda63 --- /dev/null +++ b/crates/renderer/build.rs @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +//! Compiles the crate's GLSL shader sources to SPIR-V at build time. +//! +//! The resulting modules are written into `OUT_DIR` and embedded by `pipeline.rs` through `include_bytes!`, so no compiled artifact is committed to the repository and a source edit can never disagree with the binary shipped beside it. A compilation failure aborts the build, naming the offending shader and reproducing the compiler diagnostic verbatim. +//! +//! `#include` directives are deliberately not resolved. No shader uses one yet, and enabling them requires registering each included path for change tracking as well; a shared header added without that tracking would not retrigger compilation when edited, which is the exact staleness this script exists to prevent. + +use std::path::{Path, PathBuf}; + +/// The GLSL sources compiled into the crate, paired with the pipeline stage each one targets. +const SHADERS: [(&str, shaderc::ShaderKind); 2] = [ + ("cube.vert", shaderc::ShaderKind::Vertex), + ("cube.frag", shaderc::ShaderKind::Fragment), +]; + +/// Compiles every entry of [`SHADERS`] into `OUT_DIR`. +/// +/// # Panics +/// +/// Panics if a Cargo-provided environment variable is absent, if the shader compiler or its options cannot be constructed, or if any individual shader fails to read, compile, or write. +fn main() { + let manifest_dir = required_var("CARGO_MANIFEST_DIR"); + let out_dir = required_var("OUT_DIR"); + + // Shaders are shared repository assets rather than crate-local sources, so they resolve relative to the crate root instead of living under `src/`. + let shader_dir = Path::new(&manifest_dir).join("../../assets/shaders"); + + let compiler = shaderc::Compiler::new() + .unwrap_or_else(|error| panic!("failed to initialise the shader compiler: {error}")); + + let mut options = shaderc::CompileOptions::new() + .unwrap_or_else(|error| panic!("failed to create the shader compiler options: {error}")); + + // The target environment must match the API the modules are consumed by; the renderer uses Vulkan 1.3 dynamic rendering. + options.set_target_env( + shaderc::TargetEnv::Vulkan, + shaderc::EnvVersion::Vulkan1_3 as u32, + ); + options.set_optimization_level(shaderc::OptimizationLevel::Performance); + + for (name, kind) in SHADERS { + compile_shader( + &compiler, + &options, + &shader_dir, + Path::new(&out_dir), + name, + kind, + ); + } +} + +/// Compiles the shader `name` from `shader_dir` into `/.spv`. +/// +/// # Panics +/// +/// Panics if the source cannot be read, if the shader fails to compile, or if the resulting module cannot be written. +fn compile_shader( + compiler: &shaderc::Compiler, + options: &shaderc::CompileOptions, + shader_dir: &Path, + out_dir: &Path, + name: &str, + kind: shaderc::ShaderKind, +) { + let source_path = shader_dir.join(name); + + // Emitting any directive replaces Cargo's default of rerunning whenever the package changes, so every source consumed here must be registered explicitly or edits to it stop triggering a rebuild. + println!("cargo::rerun-if-changed={}", source_path.display()); + + let source = std::fs::read_to_string(&source_path).unwrap_or_else(|error| { + panic!( + "failed to read the shader source {}: {error}", + source_path.display() + ) + }); + + let artifact = compiler + .compile_into_spirv(&source, kind, name, "main", Some(options)) + .unwrap_or_else(|error| panic!("failed to compile the shader {name}:\n{error}")); + + let output_path: PathBuf = out_dir.join(format!("{name}.spv")); + std::fs::write(&output_path, artifact.as_binary_u8()).unwrap_or_else(|error| { + panic!( + "failed to write the compiled shader {}: {error}", + output_path.display() + ) + }); +} + +/// Returns the value of a Cargo-provided environment variable. +/// +/// # Panics +/// +/// Panics if `name` is not set, which indicates the script was not invoked by Cargo. +fn required_var(name: &str) -> String { + std::env::var(name) + .unwrap_or_else(|error| panic!("the environment variable {name} is not set: {error}")) +} diff --git a/crates/renderer/src/pipeline.rs b/crates/renderer/src/pipeline.rs index 7c5fdf6..9740815 100644 --- a/crates/renderer/src/pipeline.rs +++ b/crates/renderer/src/pipeline.rs @@ -166,14 +166,16 @@ pub fn create_graphics_pipeline( /// Loads the vertex and fragment shader modules from embedded bytes. /// +/// The SPIR-V is produced from the GLSL sources by the crate's build script and embedded from `OUT_DIR`, so the modules always correspond to the shader sources present at compile time. +/// /// # Errors /// /// Returns [`RendererError::IoError`] if an embedded shader is not valid SPIR-V, or [`RendererError::VulkanError`] if module creation fails on the device. fn load_shader_modules( device: &Device, ) -> Result<(vk::ShaderModule, vk::ShaderModule), RendererError> { - let vert_bytes = include_bytes!("../../../assets/shaders/cube.vert.spv"); - let frag_bytes = include_bytes!("../../../assets/shaders/cube.frag.spv"); + let vert_bytes = include_bytes!(concat!(env!("OUT_DIR"), "/cube.vert.spv")); + let frag_bytes = include_bytes!(concat!(env!("OUT_DIR"), "/cube.frag.spv")); let vert_module = create_shader_module(device, vert_bytes)?; let frag_module = create_shader_module(device, frag_bytes)?; diff --git a/docs/rendering.md b/docs/rendering.md index b444471..ba8ccb7 100644 --- a/docs/rendering.md +++ b/docs/rendering.md @@ -2,6 +2,12 @@ Implementation notes for the `renderer` crate and for code that imports geometry. The project-wide coordinate convention itself (+Y up, right-handed, 1 unit = 1 block) is stated in [`AGENTS.md`](../AGENTS.md#coordinate-system--units); this note collects the gotchas that arise because neighbouring systems use different conventions. These are not convention changes, only mismatches to handle in one agreed place. +## Shader compilation + +GLSL sources under `assets/shaders/` are compiled to SPIR-V by the `renderer` crate's build script and embedded from `OUT_DIR`; no compiled module is committed. Building the crate therefore requires `libshaderc`, either as a distribution package (`libshaderc-dev` on Debian and Ubuntu, `shaderc` on Arch, the Vulkan SDK on Windows) or, failing that, a C++ toolchain with cmake and ninja so `shaderc-sys` can build the library from source. + +A shader that fails to compile aborts the build, naming the source file and the offending line. + ## Vulkan clip space Vulkan clip space is **Y-down** by default, and its depth range is `[0, 1]` (not `[-1, 1]` as in OpenGL). The projection matrix must flip Y, or the viewport height is set negative, both are common idioms in `ash` examples. World and view space stay Y-up; only clip space differs.