use anyhow::{Context, Result};
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct ShaderLibrary {
name: String,
modules: HashMap<String, String>,
}
impl ShaderLibrary {
pub fn from_source(name: &str, source: &str) -> Self {
let mut modules = HashMap::new();
modules.insert(name.to_string(), source.to_string());
Self {
name: name.to_string(),
modules,
}
}
pub fn from_embedded(name: &str, modules: &[(&str, &str)]) -> Self {
let modules = modules.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect();
Self {
name: name.to_string(),
modules,
}
}
pub fn from_directory(name: &str, path: &Path) -> Result<Self> {
let mut modules = HashMap::new();
let primary_path = path.join(format!("{}.slang", name));
let primary_source = std::fs::read_to_string(&primary_path)
.with_context(|| format!("Failed to read primary module: {}", primary_path.display()))?;
modules.insert(name.to_string(), primary_source);
let subdir = path.join(name);
if subdir.is_dir() {
Self::read_submodules(&mut modules, name, &subdir)?;
}
Ok(Self {
name: name.to_string(),
modules,
})
}
fn read_submodules(modules: &mut HashMap<String, String>, prefix: &str, dir: &Path) -> Result<()> {
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() && path.extension().is_some_and(|e| e == "slang") {
let stem = path.file_stem().unwrap().to_string_lossy();
let module_name = format!("{}/{}", prefix, stem);
let source = std::fs::read_to_string(&path)
.with_context(|| format!("Failed to read module: {}", path.display()))?;
modules.insert(module_name, source);
} else if path.is_dir() {
let dir_name = path.file_name().unwrap().to_string_lossy();
let new_prefix = format!("{}/{}", prefix, dir_name);
Self::read_submodules(modules, &new_prefix, &path)?;
}
}
Ok(())
}
pub fn name(&self) -> &str {
&self.name
}
pub fn modules(&self) -> &HashMap<String, String> {
&self.modules
}
pub fn get_module(&self, path: &str) -> Option<&str> {
self.modules.get(path).map(|s| s.as_str())
}
pub fn has_module(&self, path: &str) -> bool {
self.modules.contains_key(path)
}
pub fn goldy_experimental() -> Self {
Self::from_embedded(
"goldy_exp",
&[
("goldy_exp", include_str!("../shaders/goldy_exp.slang")),
("goldy_exp/math", include_str!("../shaders/goldy_exp/math.slang")),
("goldy_exp/algebra", include_str!("../shaders/goldy_exp/algebra.slang")),
(
"goldy_exp/collectives",
include_str!("../shaders/goldy_exp/collectives.slang"),
),
("goldy_exp/atomics", include_str!("../shaders/goldy_exp/atomics.slang")),
("goldy_exp/color", include_str!("../shaders/goldy_exp/color.slang")),
("goldy_exp/vertex", include_str!("../shaders/goldy_exp/vertex.slang")),
("goldy_exp/types", include_str!("../shaders/goldy_exp/types.slang")),
(
"goldy_exp/primitives",
include_str!("../shaders/goldy_exp/primitives.slang"),
),
(
"goldy_exp/bindless",
include_str!("../shaders/goldy_exp/bindless.slang"),
),
(
"goldy_exp/descriptor_handle",
include_str!("../shaders/goldy_exp/descriptor_handle.slang"),
),
(
"goldy_exp/bindless_resources",
include_str!("../shaders/goldy_exp/bindless_resources.slang"),
),
("goldy_exp/access", include_str!("../shaders/goldy_exp/access.slang")),
],
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_source_creates_single_module() {
let lib = ShaderLibrary::from_source("test", "module test; void foo() {}");
assert_eq!(lib.name(), "test");
assert_eq!(lib.modules().len(), 1);
assert!(lib.has_module("test"));
assert!(!lib.has_module("other"));
}
#[test]
fn test_from_embedded_creates_multiple_modules() {
let lib = ShaderLibrary::from_embedded(
"mylib",
&[("mylib", "module mylib;"), ("mylib/sub", "implementing mylib;")],
);
assert_eq!(lib.name(), "mylib");
assert_eq!(lib.modules().len(), 2);
assert!(lib.has_module("mylib"));
assert!(lib.has_module("mylib/sub"));
}
#[test]
fn test_get_module_returns_source() {
let lib = ShaderLibrary::from_source("test", "module test; float x = 1.0;");
let source = lib.get_module("test").unwrap();
assert!(source.contains("float x = 1.0"));
}
#[test]
fn test_get_module_returns_none_for_missing() {
let lib = ShaderLibrary::from_source("test", "module test;");
assert!(lib.get_module("nonexistent").is_none());
}
#[test]
fn test_goldy_library_has_expected_modules() {
let lib = ShaderLibrary::goldy_experimental();
assert_eq!(lib.name(), "goldy_exp");
assert!(lib.has_module("goldy_exp"));
assert!(lib.has_module("goldy_exp/math"));
assert!(lib.has_module("goldy_exp/algebra"));
assert!(lib.has_module("goldy_exp/atomics"));
assert!(lib.has_module("goldy_exp/color"));
assert!(lib.has_module("goldy_exp/vertex"));
assert!(lib.has_module("goldy_exp/types"));
assert!(lib.has_module("goldy_exp/primitives"));
assert!(lib.has_module("goldy_exp/bindless"));
assert!(lib.has_module("goldy_exp/descriptor_handle"));
assert!(lib.has_module("goldy_exp/bindless_resources"));
assert!(lib.has_module("goldy_exp/access"));
}
#[test]
fn test_goldy_library_modules_are_valid() {
let lib = ShaderLibrary::goldy_experimental();
let primary = lib.get_module("goldy_exp").unwrap();
assert!(primary.contains("module goldy_exp;"));
let math = lib.get_module("goldy_exp/math").unwrap();
assert!(math.contains("implementing goldy_exp;"));
let color = lib.get_module("goldy_exp/color").unwrap();
assert!(color.contains("implementing goldy_exp;"));
let vertex = lib.get_module("goldy_exp/vertex").unwrap();
assert!(vertex.contains("implementing goldy_exp;"));
}
}