use crate::backend::{self, AdapterInfo, DeviceHandle, GpuBackend};
use crate::shader_library::ShaderLibrary;
use crate::types::*;
use anyhow::{Context, Result};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
static REGISTRY_COUNTER: AtomicU64 = AtomicU64::new(0);
pub struct Instance {
backend: Arc<Mutex<Box<dyn GpuBackend>>>,
}
impl Instance {
pub fn new() -> Result<Self> {
let backend = backend::create_default_backend()?;
Ok(Self {
backend: Arc::new(Mutex::new(backend)),
})
}
pub fn enumerate_adapters(&self) -> Vec<Adapter> {
let backend = self.backend.lock().unwrap();
backend
.enumerate_adapters()
.into_iter()
.map(|info| Adapter { info })
.collect()
}
pub fn create_device(&self, preferred_type: DeviceType) -> Result<Device> {
let adapters = self.enumerate_adapters();
let adapter = adapters
.iter()
.find(|a| a.info.device_type == preferred_type)
.or_else(|| adapters.first())
.context("No GPU adapters available")?;
self.create_device_for_adapter(adapter.info.id)
}
pub fn create_device_for_adapter(&self, adapter_id: u32) -> Result<Device> {
let mut backend = self.backend.lock().unwrap();
let handle = backend.create_device(adapter_id)?;
let mut registry = ShaderLibraryRegistry::new();
registry.register(ShaderLibrary::goldy_experimental())?;
Ok(Device {
backend: Arc::clone(&self.backend),
handle,
adapter_id,
library_registry: Arc::new(Mutex::new(registry)),
})
}
pub fn backend_type(&self) -> BackendType {
self.backend.lock().unwrap().backend_type()
}
}
#[derive(Debug, Clone)]
pub struct Adapter {
pub info: AdapterInfo,
}
impl Adapter {
pub fn id(&self) -> u32 {
self.info.id
}
pub fn name(&self) -> &str {
&self.info.name
}
pub fn device_type(&self) -> DeviceType {
self.info.device_type
}
pub fn vendor(&self) -> &str {
&self.info.vendor
}
}
#[derive(Debug, Clone)]
pub struct DeviceCapabilities {
pub preferred_surface_format: TextureFormat,
pub preferred_render_target_format: TextureFormat,
pub supported_surface_formats: Vec<TextureFormat>,
pub supported_render_target_formats: Vec<TextureFormat>,
}
impl Default for DeviceCapabilities {
fn default() -> Self {
Self {
preferred_surface_format: TextureFormat::Bgra8UnormSrgb,
preferred_render_target_format: TextureFormat::Rgba8Unorm,
supported_surface_formats: vec![
TextureFormat::Bgra8UnormSrgb,
TextureFormat::Bgra8Unorm,
],
supported_render_target_formats: vec![
TextureFormat::Rgba8Unorm,
TextureFormat::Rgba8UnormSrgb,
TextureFormat::Bgra8Unorm,
TextureFormat::Bgra8UnormSrgb,
TextureFormat::Rgba16Float,
TextureFormat::Rgba32Float,
],
}
}
}
pub struct Device {
pub(crate) backend: Arc<Mutex<Box<dyn GpuBackend>>>,
pub(crate) handle: DeviceHandle,
adapter_id: u32,
library_registry: Arc<Mutex<ShaderLibraryRegistry>>,
}
struct ShaderLibraryRegistry {
libraries: HashMap<String, ShaderLibrary>,
temp_dir: Option<PathBuf>,
dirty: bool,
}
impl ShaderLibraryRegistry {
fn new() -> Self {
Self {
libraries: HashMap::new(),
temp_dir: None,
dirty: true,
}
}
fn register(&mut self, library: ShaderLibrary) -> Result<()> {
let name = library.name().to_string();
if self.libraries.contains_key(&name) {
anyhow::bail!("Library '{}' is already registered", name);
}
self.libraries.insert(name, library);
self.dirty = true;
Ok(())
}
fn unregister(&mut self, name: &str) -> bool {
if self.libraries.remove(name).is_some() {
self.dirty = true;
true
} else {
false
}
}
fn has(&self, name: &str) -> bool {
self.libraries.contains_key(name)
}
fn list(&self) -> Vec<&str> {
self.libraries.keys().map(|s| s.as_str()).collect()
}
fn get_search_paths(&mut self) -> Result<Vec<PathBuf>> {
if self.libraries.is_empty() {
return Ok(vec![]);
}
if self.temp_dir.is_none() {
let unique_id = REGISTRY_COUNTER.fetch_add(1, Ordering::Relaxed);
let temp_dir = std::env::temp_dir().join(format!(
"goldy-shaders-{}-{}",
std::process::id(),
unique_id
));
std::fs::create_dir_all(&temp_dir)
.context("Failed to create shader library temp directory")?;
self.temp_dir = Some(temp_dir);
}
if self.dirty {
let temp_dir = self.temp_dir.as_ref().unwrap();
for library in self.libraries.values() {
for (module_path, source) in library.modules() {
let mut file_path = temp_dir.clone();
for component in module_path.split('/') {
file_path = file_path.join(component);
}
file_path.set_extension("slang");
if let Some(parent) = file_path.parent() {
std::fs::create_dir_all(parent)
.context("Failed to create module directory")?;
}
std::fs::write(&file_path, source)
.with_context(|| format!("Failed to write module: {}", module_path))?;
}
}
self.dirty = false;
}
Ok(vec![self.temp_dir.clone().unwrap()])
}
}
impl Drop for ShaderLibraryRegistry {
fn drop(&mut self) {
if let Some(temp_dir) = self.temp_dir.take() {
let _ = std::fs::remove_dir_all(temp_dir);
}
}
}
impl Device {
pub fn adapter_id(&self) -> u32 {
self.adapter_id
}
pub fn is_valid(&self) -> bool {
self.backend.lock().unwrap().is_device_valid(self.handle)
}
pub fn capabilities(&self) -> DeviceCapabilities {
DeviceCapabilities::default()
}
pub fn register_library(&self, library: ShaderLibrary) -> Result<()> {
self.library_registry.lock().unwrap().register(library)
}
pub fn unregister_library(&self, name: &str) -> bool {
self.library_registry.lock().unwrap().unregister(name)
}
pub fn has_library(&self, name: &str) -> bool {
self.library_registry.lock().unwrap().has(name)
}
pub fn list_libraries(&self) -> Vec<String> {
self.library_registry
.lock()
.unwrap()
.list()
.iter()
.map(|s| s.to_string())
.collect()
}
pub(crate) fn get_shader_search_paths(&self) -> Result<Vec<PathBuf>> {
self.library_registry.lock().unwrap().get_search_paths()
}
#[cfg(test)]
pub(crate) fn from_backend(backend: Box<dyn GpuBackend>) -> anyhow::Result<Self> {
let backend = Arc::new(Mutex::new(backend));
let handle = {
let mut b = backend.lock().unwrap();
b.create_device(0)?
};
let mut registry = ShaderLibraryRegistry::new();
registry.register(ShaderLibrary::goldy_experimental())?;
Ok(Self {
backend,
handle,
adapter_id: 0,
library_registry: Arc::new(Mutex::new(registry)),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::mock::MockBackend;
fn test_device() -> Device {
Device::from_backend(Box::new(MockBackend::new())).unwrap()
}
#[test]
fn test_goldy_library_registered_by_default() {
let device = test_device();
assert!(device.has_library("goldy_exp"));
}
#[test]
fn test_register_custom_library() {
let device = test_device();
let lib = ShaderLibrary::from_source("custom", "module custom;");
device.register_library(lib).unwrap();
assert!(device.has_library("custom"));
}
#[test]
fn test_register_duplicate_fails() {
let device = test_device();
let lib1 = ShaderLibrary::from_source("mylib", "module mylib;");
let lib2 = ShaderLibrary::from_source("mylib", "module mylib;");
device.register_library(lib1).unwrap();
let result = device.register_library(lib2);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("already registered"));
}
#[test]
fn test_unregister_library() {
let device = test_device();
let lib = ShaderLibrary::from_source("temp", "module temp;");
device.register_library(lib).unwrap();
assert!(device.has_library("temp"));
assert!(device.unregister_library("temp"));
assert!(!device.has_library("temp"));
}
#[test]
fn test_unregister_nonexistent_returns_false() {
let device = test_device();
assert!(!device.unregister_library("nonexistent"));
}
#[test]
fn test_list_libraries() {
let device = test_device();
let libs = device.list_libraries();
assert!(libs.contains(&"goldy_exp".to_string()));
device.register_library(ShaderLibrary::from_source("extra", "module extra;")).unwrap();
let libs = device.list_libraries();
assert!(libs.contains(&"goldy_exp".to_string()));
assert!(libs.contains(&"extra".to_string()));
}
#[test]
fn test_search_paths_writes_files() {
let device = test_device();
let paths = device.get_shader_search_paths().unwrap();
assert_eq!(paths.len(), 1);
let goldy_file = paths[0].join("goldy_exp.slang");
assert!(goldy_file.exists(), "goldy_exp.slang should exist at {:?}", goldy_file);
let math_file = paths[0].join("goldy_exp/math.slang");
assert!(math_file.exists(), "goldy_exp/math.slang should exist at {:?}", math_file);
}
}
impl Drop for Device {
fn drop(&mut self) {
let mut backend = self.backend.lock().unwrap();
backend.destroy_device(self.handle);
}
}