Skip to main content

ursus_core/assets/registry/
material.rs

1use crate::assets::upload::GpuUploadRequest;
2use std::collections::{HashMap, HashSet};
3use std::sync::mpsc::Sender;
4use ursus_materials::pack::aos::pack;
5use ursus_materials::{Material, MaterialHandle, ShadingStrategy, StrategyRegistry};
6
7/// The only source of `MaterialHandle` in the system.
8#[derive(Default)]
9pub(crate) struct MaterialHandleAllocator {
10    next: u32,
11}
12
13impl MaterialHandleAllocator {
14    pub(crate) fn alloc(&mut self) -> MaterialHandle {
15        let id = self.next;
16        self.next += 1;
17        MaterialHandle(id)
18    }
19}
20
21/// CPU-side storage for materials, with dirty tracking so unchanged
22/// materials aren't re-resolved/re-packed every frame.
23///
24/// Game thread only. Holds `Material` values (data + declared strategy
25/// name); actually resolving a material against a `StrategyRegistry` and
26/// packing it into GPU-visible bytes happens later, in the material extract
27/// system - `MaterialRegistry` itself doesn't know about strategies or GPU
28/// layouts.
29#[derive(Default)]
30pub struct MaterialRegistry {
31    handles: MaterialHandleAllocator,
32    materials: HashMap<MaterialHandle, Material>,
33    dirty: HashSet<MaterialHandle>,
34}
35
36impl MaterialRegistry {
37    /// Registers a new material and marks it dirty so it gets packed and
38    /// uploaded on the next extract.
39    pub fn insert(&mut self, material: Material) -> MaterialHandle {
40        let handle = self.handles.alloc();
41        self.materials.insert(handle, material);
42        self.dirty.insert(handle);
43        handle
44    }
45
46    /// Mutates a material in place via `f`, then marks it dirty. Returns
47    /// `false` if `handle` doesn't exist.
48    pub fn modify(&mut self, handle: MaterialHandle, f: impl FnOnce(&mut Material)) -> bool {
49        let Some(material) = self.materials.get_mut(&handle) else {
50            return false;
51        };
52        f(material);
53        self.dirty.insert(handle);
54        true
55    }
56
57    pub fn get(&self, handle: MaterialHandle) -> Option<&Material> {
58        self.materials.get(&handle)
59    }
60
61    /// Drains the set of materials that changed since the last call,
62    /// pairing each with its current data. Used by the material extract
63    /// system once per frame.
64    pub(crate) fn drain_dirty(&mut self) -> Vec<(MaterialHandle, Material)> {
65        self.dirty.drain().filter_map(|handle| self.materials.get(&handle).map(|m| (handle, m.clone()))).collect()
66    }
67}
68
69/// CPU-side material registration + strategy resolution + GPU packing.
70#[derive(Default)]
71pub struct MaterialAssetRegistry {
72    materials: MaterialRegistry,
73    strategies: StrategyRegistry,
74}
75
76impl MaterialAssetRegistry {
77    pub fn insert(&mut self, material: Material) -> MaterialHandle {
78        if self.strategies.by_name(material.strategy).is_none() {
79            panic!(
80                "MaterialAssetRegistry::insert: material '{}' references unknown strategy '{}' - \
81             register it via register_strategy() before creating materials that use it",
82                material.name, material.strategy
83            );
84        }
85        self.materials.insert(material)
86    }
87
88    pub fn modify(&mut self, handle: MaterialHandle, f: impl FnOnce(&mut Material)) -> bool {
89        self.materials.modify(handle, f)
90    }
91
92    pub fn get(&self, handle: MaterialHandle) -> Option<&Material> {
93        self.materials.get(handle)
94    }
95
96    pub fn register_strategy(&mut self, strategy: impl ShadingStrategy + 'static) {
97        self.strategies.register(strategy);
98    }
99
100    /// Resolves and packs materials changed since the last call, queuing
101    /// the bytes for GPU upload. Called once per frame.
102    pub(crate) fn flush_dirty(&mut self, upload_tx: &Sender<GpuUploadRequest>) {
103        for (handle, material) in self.materials.drain_dirty() {
104            let Some(strategy) = self.strategies.by_name(material.strategy) else {
105                log::error!(
106                    "MaterialAssetRegistry: material '{}' references unknown strategy '{}'",
107                    material.name,
108                    material.strategy
109                );
110                continue;
111            };
112
113            if let Err(e) = strategy.validate(&material) {
114                log::error!("MaterialAssetRegistry: material '{}' failed validation: {e}", material.name);
115                continue;
116            }
117
118            let values = match strategy.resolve(&material) {
119                Ok(values) => values,
120                Err(e) => {
121                    log::error!("MaterialAssetRegistry: failed to resolve material '{}': {e}", material.name);
122                    continue;
123                }
124            };
125
126            let Some(bytes) = pack(strategy.fields(), &values) else {
127                log::error!(
128                    "MaterialAssetRegistry: failed to pack material '{}' - value/field mismatch",
129                    material.name
130                );
131                continue;
132            };
133
134            upload_tx.send(GpuUploadRequest::Material { handle, bytes }).ok();
135        }
136    }
137}