concinnity_core/ecs/
resolver.rs1use core::sync::atomic::{AtomicUsize, Ordering};
22
23pub(crate) type ResolveFn = fn(&str) -> u32;
25
26pub(crate) type HandleResolveFn = fn(&str) -> Option<u32>;
33
34struct NameResolverSlot(AtomicUsize);
37
38impl NameResolverSlot {
39 const fn new() -> Self {
40 Self(AtomicUsize::new(0))
41 }
42
43 fn set(&self, f: ResolveFn) {
44 self.0.store(f as usize, Ordering::Release);
45 }
46
47 fn resolve(&self, name: &str) -> Option<u32> {
48 let v = self.0.load(Ordering::Acquire);
49 if v == 0 {
50 return None;
51 }
52 let f: ResolveFn = unsafe { core::mem::transmute::<usize, ResolveFn>(v) };
55 Some(f(name))
56 }
57}
58
59struct HandleResolverSlot(AtomicUsize);
63
64impl HandleResolverSlot {
65 const fn new() -> Self {
66 Self(AtomicUsize::new(0))
67 }
68
69 fn set(&self, f: HandleResolveFn) {
70 self.0.store(f as usize, Ordering::Release);
71 }
72
73 fn resolve(&self, name: &str) -> Option<u32> {
74 let v = self.0.load(Ordering::Acquire);
75 if v == 0 {
76 return None;
77 }
78 let f: HandleResolveFn = unsafe { core::mem::transmute::<usize, HandleResolveFn>(v) };
81 f(name)
82 }
83}
84
85#[cfg(not(test))]
86static RESOLVER: NameResolverSlot = NameResolverSlot::new();
87
88#[cfg(not(test))]
91pub fn set_name_resolver(f: ResolveFn) {
92 RESOLVER.set(f);
93}
94
95#[cfg(not(test))]
98pub(crate) fn resolve_name(name: &str) -> Option<u32> {
99 RESOLVER.resolve(name)
100}
101
102#[cfg(test)]
107std::thread_local! {
108 static RESOLVER: core::cell::Cell<Option<ResolveFn>> =
109 const { core::cell::Cell::new(None) };
110}
111
112#[cfg(test)]
114pub fn set_name_resolver(f: ResolveFn) {
115 RESOLVER.with(|slot| slot.set(Some(f)));
116}
117
118#[cfg(test)]
120pub(crate) fn resolve_name(name: &str) -> Option<u32> {
121 RESOLVER.with(|slot| slot.get()).map(|f| f(name))
122}
123
124macro_rules! handle_resolver {
127 (
128 $(#[$extra:meta])*
129 $slot:ident, $noun:literal, $set_fn:ident, $resolve_fn:ident $(,)?
130 ) => {
131 #[cfg(not(test))]
132 static $slot: HandleResolverSlot = HandleResolverSlot::new();
133
134 #[cfg(test)]
138 std::thread_local! {
139 static $slot: core::cell::Cell<Option<HandleResolveFn>> =
140 const { core::cell::Cell::new(None) };
141 }
142
143 #[doc = concat!(
144 "Install the name -> ", $noun,
145 "-handle resolver. Called by concinnity-cook, backed by the current ",
146 "build's declaration-ordered ", $noun,
147 " handle map. Idempotent; the last writer wins."
148 )]
149 $(#[$extra])*
150 #[cfg(not(test))]
151 pub fn $set_fn(f: HandleResolveFn) {
152 $slot.set(f);
153 }
154
155 #[cfg(test)]
156 #[doc = concat!("Install the name -> ", $noun, "-handle resolver.")]
157 pub fn $set_fn(f: HandleResolveFn) {
158 $slot.with(|slot| slot.set(Some(f)));
159 }
160
161 #[doc = concat!(
162 "Resolve a ", $noun,
163 " reference name to its dense handle value via the installed ",
164 "resolver. `None` means either no resolver is installed or the name ",
165 "is not a declared ", $noun,
166 "; the caller decides whether to fall back (a validation context) ",
167 "or to fail (a real build)."
168 )]
169 #[cfg(not(test))]
170 pub(crate) fn $resolve_fn(name: &str) -> Option<u32> {
171 $slot.resolve(name)
172 }
173
174 #[cfg(test)]
175 pub(crate) fn $resolve_fn(name: &str) -> Option<u32> {
176 $slot.with(|slot| slot.get()).and_then(|f| f(name))
177 }
178 };
179}
180
181handle_resolver! {
182 TEXTURE_HANDLE_RESOLVER, "texture",
183 set_texture_handle_resolver, resolve_texture_handle,
184}
185handle_resolver! {
186 AUDIO_CLIP_HANDLE_RESOLVER, "audio-clip",
187 set_audio_clip_handle_resolver, resolve_audio_clip_handle,
188}
189handle_resolver! {
190 FONT_HANDLE_RESOLVER, "font",
191 set_font_handle_resolver, resolve_font_handle,
192}
193handle_resolver! {
194 MESH_HANDLE_RESOLVER, "mesh",
198 set_mesh_handle_resolver, resolve_mesh_handle,
199}
200handle_resolver! {
201 MATERIAL_HANDLE_RESOLVER, "material",
202 set_material_handle_resolver, resolve_material_handle,
203}
204handle_resolver! {
205 SKINNED_MESH_HANDLE_RESOLVER, "skinned-mesh",
209 set_skinned_mesh_handle_resolver, resolve_skinned_mesh_handle,
210}
211handle_resolver! {
212 SHADER_HANDLE_RESOLVER, "shader",
215 set_shader_handle_resolver, resolve_shader_handle,
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
224 use crate::ecs::asset_id::{AssetId, AssetRef, de_opt_asset_ref, de_opt_asset_ref_typed};
225 use crate::test_support::{install_resolvers, len_handle_resolver, len_name_resolver};
226
227 struct Clip;
228
229 #[test]
230 fn a_slot_reads_back_the_function_pointer_it_was_given() {
231 let name_slot = NameResolverSlot::new();
236 assert_eq!(name_slot.resolve("floor"), None);
237 name_slot.set(len_name_resolver);
238 assert_eq!(name_slot.resolve("floor"), Some(5));
239
240 let handle_slot = HandleResolverSlot::new();
241 assert_eq!(handle_slot.resolve("floor"), None);
242 handle_slot.set(len_handle_resolver);
243 assert_eq!(handle_slot.resolve("floor"), Some(5));
244 assert_eq!(handle_slot.resolve("unknown_x"), None);
247 }
248
249 #[test]
250 fn installed_resolver_is_used() {
251 set_name_resolver(len_name_resolver);
252 assert_eq!(resolve_name("abcd"), Some(4));
253 }
254
255 #[test]
256 fn asset_id_resolves_a_name_through_the_seam() {
257 set_name_resolver(len_name_resolver);
258 let id: AssetId = serde_json::from_str("\"floor\"").unwrap();
259 assert_eq!(id, AssetId(5));
260 }
261
262 #[test]
263 fn asset_ref_resolves_a_name_through_the_seam() {
264 set_name_resolver(len_name_resolver);
265 let r: AssetRef<Clip> = serde_json::from_str("\"wall\"").unwrap();
266 assert_eq!(r.id(), Some(AssetId(4)));
267 assert!(r.is_resolved());
268 }
269
270 #[test]
271 fn every_handle_seam_resolves_through_its_own_slot() {
272 install_resolvers();
275 set_texture_handle_resolver(len_handle_resolver);
276 set_audio_clip_handle_resolver(len_handle_resolver);
277 set_font_handle_resolver(len_handle_resolver);
278 set_mesh_handle_resolver(len_handle_resolver);
279 set_material_handle_resolver(len_handle_resolver);
280 set_skinned_mesh_handle_resolver(len_handle_resolver);
281 set_shader_handle_resolver(len_handle_resolver);
282
283 assert_eq!(resolve_texture_handle("floor"), Some(5));
284 assert_eq!(resolve_audio_clip_handle("floor"), Some(5));
285 assert_eq!(resolve_font_handle("floor"), Some(5));
286 assert_eq!(resolve_mesh_handle("floor"), Some(5));
287 assert_eq!(resolve_material_handle("floor"), Some(5));
288 assert_eq!(resolve_skinned_mesh_handle("floor"), Some(5));
289 assert_eq!(resolve_shader_handle("floor"), Some(5));
290
291 assert_eq!(resolve_texture_handle("unknown_x"), None);
294 assert_eq!(resolve_shader_handle("unknown_x"), None);
295 }
296
297 #[test]
298 fn opt_helpers_resolve_a_name_and_pass_through_an_id() {
299 set_name_resolver(len_name_resolver);
300
301 #[derive(serde::Deserialize)]
302 struct Bare {
303 #[serde(default, deserialize_with = "de_opt_asset_ref")]
304 r: Option<AssetId>,
305 }
306 #[derive(serde::Deserialize)]
307 struct Typed {
308 #[serde(default, deserialize_with = "de_opt_asset_ref_typed")]
309 r: Option<AssetRef<Clip>>,
310 }
311
312 assert_eq!(
313 serde_json::from_str::<Bare>("{\"r\":\"mesh_a\"}")
314 .unwrap()
315 .r,
316 Some(AssetId(6))
317 );
318 assert_eq!(
319 serde_json::from_str::<Typed>("{\"r\":\"abc\"}")
320 .unwrap()
321 .r
322 .unwrap()
323 .id(),
324 Some(AssetId(3))
325 );
326 }
327}