chore(renderer): compile shaders at build time

This commit is contained in:
Serkyo 2026-08-04 02:41:33 +02:00
parent 857aada981
commit 0fd4378c3c
9 changed files with 155 additions and 10 deletions

2
.gitattributes vendored
View file

@ -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

View file

@ -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

39
Cargo.lock generated
View file

@ -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"

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f3f5c483dddf88cd90dbb62aa579c7f815b7ff304407d27c19a9a52cc450b4bb
size 572

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:17461a207a6e1d6ba3e2b050aaa83ad0fb880a29e54e42295a1d43cbdfbdc115
size 1888

View file

@ -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

100
crates/renderer/build.rs Normal file
View file

@ -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 `<out_dir>/<name>.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}"))
}

View file

@ -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)?;

View file

@ -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.