Skip to main content

myth_core/utils/
interner.rs

1//! Global String Interner
2//!
3//! Provides high-performance string interning service that converts strings into
4//! integer [`Symbol`]s for efficient comparison and hashing. This is the foundational
5//! infrastructure for the dynamic shader macro system.
6
7use lasso::Spur;
8use std::borrow::Cow;
9
10#[cfg(not(target_arch = "wasm32"))]
11use lasso::ThreadedRodeo;
12
13#[cfg(target_arch = "wasm32")]
14use lasso::Rodeo;
15#[cfg(target_arch = "wasm32")]
16use std::cell::UnsafeCell;
17
18/// Global string interner instance (Native - thread-safe)
19#[cfg(not(target_arch = "wasm32"))]
20static INTERNER: std::sync::LazyLock<ThreadedRodeo> = std::sync::LazyLock::new(ThreadedRodeo::new);
21
22#[cfg(target_arch = "wasm32")]
23// Global string interner instance (WASM - single-threaded)
24//
25// We use UnsafeCell here because WASM is single-threaded and RefCell can cause
26// double-borrow panics when async callbacks interleave with event handlers.
27// Since WASM guarantees single-threaded execution, this is safe.
28thread_local! {
29    static INTERNER: UnsafeCell<Rodeo> = UnsafeCell::new(Rodeo::new());
30}
31
32/// Symbol type alias
33///
34/// A Symbol is a compact integer identifier that can be efficiently compared
35/// and hashed, providing O(1) equality checks for interned strings.
36pub type Symbol = Spur;
37
38/// Interns a string and returns its Symbol.
39///
40/// If the string already exists in the intern pool, returns the existing Symbol.
41/// If it doesn't exist, adds it to the pool and returns a new Symbol.
42#[cfg(not(target_arch = "wasm32"))]
43#[inline]
44pub fn intern(s: &str) -> Symbol {
45    INTERNER.get_or_intern(s)
46}
47
48#[cfg(target_arch = "wasm32")]
49#[inline]
50pub fn intern(s: &str) -> Symbol {
51    // SAFETY: WASM is single-threaded, so there's no concurrent access.
52    // We use UnsafeCell to avoid RefCell double-borrow panics that occur
53    // when async callbacks (spawn_local) interleave with event handlers.
54    INTERNER.with(|interner| unsafe { (*interner.get()).get_or_intern(s) })
55}
56
57/// Attempts to get the Symbol for an existing string.
58///
59/// Returns `None` if the string doesn't exist in the intern pool.
60/// This method does not allocate new memory.
61#[cfg(not(target_arch = "wasm32"))]
62#[inline]
63pub fn get(s: &str) -> Option<Symbol> {
64    INTERNER.get(s)
65}
66
67#[cfg(target_arch = "wasm32")]
68#[inline]
69pub fn get(s: &str) -> Option<Symbol> {
70    // SAFETY: WASM is single-threaded
71    INTERNER.with(|interner| unsafe { (*interner.get()).get(s) })
72}
73
74/// Resolves a Symbol back to its string.
75///
76/// Returns a String containing the resolved value.
77///
78/// # Panics
79///
80/// Panics if the Symbol is invalid (this typically shouldn't happen).
81#[cfg(not(target_arch = "wasm32"))]
82#[inline]
83pub fn resolve(sym: Symbol) -> Cow<'static, str> {
84    Cow::Borrowed(INTERNER.resolve(&sym))
85}
86
87/// Resolves a Symbol back to its string (WASM version).
88#[cfg(target_arch = "wasm32")]
89#[inline]
90pub fn resolve(sym: Symbol) -> Cow<'static, str> {
91    INTERNER.with(|interner| unsafe { Cow::Owned((*interner.get()).resolve(&sym).to_string()) })
92}
93
94/// Pre-interns commonly used macro names.
95///
96/// Called during rendering engine initialization to ensure common macro names
97/// are already interned, reducing interning operations on hot paths.
98pub fn preload_common_macros() {
99    let common = [
100        // Physical-based Features
101        "USE_IBL",
102        "USE_IOR",
103        "USE_SPECULAR",
104        "USE_CLEARCOAT",
105        "USE_SHEEN",
106        "USE_IRIDESCENCE",
107        "USE_ANISOTROPY",
108        "USE_TRANSMISSION",
109        "USE_DISPERSION",
110        // Material-related
111        "HAS_MAP",
112        "HAS_NORMAL_MAP",
113        "HAS_ROUGHNESS_MAP",
114        "HAS_METALNESS_MAP",
115        "HAS_EMISSIVE_MAP",
116        "HAS_AO_MAP",
117        "HAS_SPECULAR_MAP",
118        "HAS_SPECULAR_INTENSITY_MAP",
119        "HAS_CLEARCOAT_MAP",
120        "HAS_CLEARCOAT_ROUGHNESS_MAP",
121        "HAS_CLEARCOAT_NORMAL_MAP",
122        "HAS_SHEEN_COLOR_MAP",
123        "HAS_SHEEN_ROUGHNESS_MAP",
124        "HAS_IRIDESCENCE_MAP",
125        "HAS_IRIDESCENCE_THICKNESS_MAP",
126        "HAS_ANISOTROPY_MAP",
127        "HAS_TRANSMISSION_MAP",
128        "HAS_THICKNESS_MAP",
129        // Geometry-related
130        "HAS_UV",
131        "HAS_NORMAL",
132        "HAS_COLOR",
133        "HAS_TANGENT",
134        "HAS_SKINNING",
135        "HAS_MORPH_TARGETS",
136        "HAS_MORPH_NORMALS",
137        "HAS_MORPH_TANGENTS",
138        "SUPPORT_SKINNING",
139        // Scene-related
140        "HAS_ENV_MAP",
141        "HAS_SHADOWS",
142        "USE_SSAO",
143        "USE_SCREEN_SPACE_FEATURES",
144        "USE_SSS",
145        "USE_SSR",
146        "HAS_MRT_SSSS",
147        // Pipeline-related
148        "ALPHA_MODE",
149        "OPAQUE",
150        "MASK",
151        "BLEND",
152        "BLEND_MASK",
153        "ALPHA_TO_COVERAGE",
154        "HDR",
155        "IN_TRANSPARENT_PASS",
156        // Post-processing effects
157        "TONE_MAPPING_MODE",
158        "NEUTRAL",
159        "LINEAR",
160        "REINHARD",
161        "CINEON",
162        "ACES_FILMIC",
163        "AGX",
164        "AGX_LOOK",
165        // Common values
166        "0",
167        "1",
168        "true",
169        "false",
170    ];
171
172    for name in common {
173        intern(name);
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn test_intern_and_resolve() {
183        let s1 = intern("hello");
184        let s2 = intern("hello");
185        let s3 = intern("world");
186
187        assert_eq!(s1, s2);
188        assert_ne!(s1, s3);
189
190        assert_eq!(resolve(s1), "hello");
191        assert_eq!(resolve(s3), "world");
192    }
193
194    #[test]
195    fn test_get() {
196        let _ = intern("existing");
197
198        assert!(get("existing").is_some());
199        assert!(get("non_existing").is_none());
200    }
201}