use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::{Mutex, OnceLock, PoisonError};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ShaderId(u32);
impl ShaderId {
pub(crate) const fn raw(self) -> u32 {
self.0
}
pub(crate) const fn from_raw(index: u32) -> Self {
Self(index)
}
}
struct Entry {
name: String,
source: Mutex<Option<Cow<'static, str>>>,
module: OnceLock<wgpu::ShaderModule>,
}
impl std::fmt::Debug for Entry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Entry")
.field("name", &self.name)
.field("created", &self.module.get().is_some())
.finish()
}
}
impl Entry {
fn module(&self, device: &wgpu::Device) -> &wgpu::ShaderModule {
self.module.get_or_init(|| {
let mut source = self.source.lock().unwrap_or_else(PoisonError::into_inner);
let module = {
let wgsl: &str = source.as_deref().unwrap_or_else(|| {
log::error!(
"frust-gpu: shader `{}` has no source left to create its module from",
self.name
);
""
});
device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some(&format!("frust-gpu shader: {}", self.name)),
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(wgsl)),
})
};
*source = None;
module
})
}
}
#[derive(Debug, Default)]
pub struct ShaderLibrary {
entries: Vec<Entry>,
by_name: HashMap<String, ShaderId>,
}
impl ShaderLibrary {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn register_wgsl(
&mut self,
name: impl Into<String>,
src: impl Into<Cow<'static, str>>,
) -> ShaderId {
let name = name.into();
if let Some(existing) = self.by_name.get(&name) {
return *existing;
}
let id = ShaderId::from_raw(self.entries.len() as u32);
self.entries.push(Entry {
name: name.clone(),
source: Mutex::new(Some(src.into())),
module: OnceLock::new(),
});
self.by_name.insert(name, id);
id
}
pub fn insert_wgsl(
&mut self,
device: &wgpu::Device,
name: impl Into<String>,
src: &str,
) -> ShaderId {
let id = self.register_wgsl(name, src.to_owned());
let _module = self.module(device, id);
id
}
#[must_use]
pub fn module(&self, device: &wgpu::Device, id: ShaderId) -> Option<&wgpu::ShaderModule> {
self.entries
.get(id.raw() as usize)
.map(|entry| entry.module(device))
}
pub fn compile_all(&self, device: &wgpu::Device) {
for entry in &self.entries {
let _module = entry.module(device);
}
}
#[must_use]
pub fn get(&self, id: ShaderId) -> Option<&wgpu::ShaderModule> {
self.entries
.get(id.raw() as usize)
.and_then(|entry| entry.module.get())
}
#[must_use]
pub fn is_compiled(&self, id: ShaderId) -> bool {
self.get(id).is_some()
}
#[must_use]
pub fn id_of(&self, name: &str) -> Option<ShaderId> {
self.by_name.get(name).copied()
}
#[must_use]
pub fn name_of(&self, id: ShaderId) -> Option<&str> {
self.entries.get(id.raw() as usize).map(|e| e.name.as_str())
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn compiled_len(&self) -> usize {
self.entries
.iter()
.filter(|entry| entry.module.get().is_some())
.count()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_empty_library_resolves_nothing() {
let lib = ShaderLibrary::new();
assert!(lib.is_empty());
assert_eq!(lib.len(), 0);
assert_eq!(lib.compiled_len(), 0);
assert!(lib.get(ShaderId::from_raw(0)).is_none());
assert!(!lib.is_compiled(ShaderId::from_raw(0)));
assert!(lib.name_of(ShaderId::from_raw(0)).is_none());
assert!(lib.id_of("missing").is_none());
}
#[test]
fn ids_round_trip_through_their_raw_index() {
for index in [0u32, 1, 7, u32::MAX] {
assert_eq!(ShaderId::from_raw(index).raw(), index);
}
assert_ne!(ShaderId::from_raw(0), ShaderId::from_raw(1));
}
#[test]
fn registering_mints_ids_without_creating_a_module() {
let mut lib = ShaderLibrary::new();
let a = lib.register_wgsl("a", "@fragment fn fs_main() {}");
let b = lib.register_wgsl("b", String::from("@fragment fn fs_main() {}"));
assert_ne!(a, b);
assert_eq!(lib.register_wgsl("a", "something else"), a);
assert_eq!(lib.len(), 2);
assert!(!lib.is_empty());
assert_eq!(lib.compiled_len(), 0, "registration created a module");
assert!(lib.get(a).is_none());
assert!(!lib.is_compiled(b));
assert_eq!(lib.id_of("b"), Some(b));
assert_eq!(lib.name_of(a), Some("a"));
assert!(
format!("{lib:?}").contains("created: false"),
"a debug print names each entry's state, not its source"
);
}
}