Skip to main content

oximedia_gpu/
sampler.rs

1//! Texture sampler configuration and caching.
2//!
3//! Provides enumerations for texture filtering and wrapping modes, a
4//! configuration struct combining them, and a simple in-memory cache so
5//! identical configurations share the same sampler object.
6
7#![allow(dead_code)]
8
9use std::collections::HashMap;
10
11/// How texels are filtered when a texture is sampled at a non-integer
12/// coordinate.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
14pub enum FilterMode {
15    /// Return the nearest texel with no interpolation (pixelated).
16    Nearest,
17    /// Linearly interpolate between adjacent texels (smooth).
18    #[default]
19    Linear,
20    /// Use mipmaps with nearest selection between levels.
21    NearestMipmapNearest,
22    /// Use mipmaps with linear interpolation between levels.
23    LinearMipmapLinear,
24}
25
26impl FilterMode {
27    /// Return `true` if this filter mode uses mipmaps.
28    #[must_use]
29    pub const fn uses_mipmaps(self) -> bool {
30        matches!(self, Self::NearestMipmapNearest | Self::LinearMipmapLinear)
31    }
32}
33
34/// How texture coordinates outside [0, 1] are resolved.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
36pub enum WrapMode {
37    /// Texture repeats (tiles) indefinitely.
38    Repeat,
39    /// Texture tiles but every other repetition is mirrored.
40    MirrorRepeat,
41    /// Coordinates are clamped to [0, 1]; edge texels are stretched.
42    #[default]
43    ClampToEdge,
44    /// Coordinates outside [0, 1] sample a configured border colour.
45    ClampToBorder,
46}
47
48/// The level-of-detail bias applied when selecting a mipmap level.
49///
50/// Positive values blur the texture; negative values sharpen it.
51pub type LodBias = f32;
52
53/// Complete configuration for a texture sampler.
54///
55/// Combines filter and wrap modes together with an optional LOD bias and
56/// the maximum anisotropy level.
57#[derive(Debug, Clone, PartialEq)]
58pub struct SamplerConfig {
59    /// Filtering applied when the texture is minified.
60    pub min_filter: FilterMode,
61    /// Filtering applied when the texture is magnified.
62    pub mag_filter: FilterMode,
63    /// Wrapping applied along the U (horizontal) texture axis.
64    pub wrap_u: WrapMode,
65    /// Wrapping applied along the V (vertical) texture axis.
66    pub wrap_v: WrapMode,
67    /// Level-of-detail bias (applied after automatic mip selection).
68    pub lod_bias: LodBias,
69    /// Maximum anisotropy level (1 = isotropic, 16 = maximum).
70    pub max_anisotropy: u8,
71}
72
73impl SamplerConfig {
74    /// A minimal default: linear filter, clamp-to-edge, no anisotropy.
75    #[must_use]
76    pub fn linear_clamp() -> Self {
77        Self {
78            min_filter: FilterMode::Linear,
79            mag_filter: FilterMode::Linear,
80            wrap_u: WrapMode::ClampToEdge,
81            wrap_v: WrapMode::ClampToEdge,
82            lod_bias: 0.0,
83            max_anisotropy: 1,
84        }
85    }
86
87    /// Nearest-neighbour filter, repeat wrapping — good for tiling textures.
88    #[must_use]
89    pub fn nearest_repeat() -> Self {
90        Self {
91            min_filter: FilterMode::Nearest,
92            mag_filter: FilterMode::Nearest,
93            wrap_u: WrapMode::Repeat,
94            wrap_v: WrapMode::Repeat,
95            lod_bias: 0.0,
96            max_anisotropy: 1,
97        }
98    }
99
100    /// Trilinear mipmap filtering with 16× anisotropy — high quality.
101    #[must_use]
102    pub fn trilinear_anisotropic() -> Self {
103        Self {
104            min_filter: FilterMode::LinearMipmapLinear,
105            mag_filter: FilterMode::Linear,
106            wrap_u: WrapMode::Repeat,
107            wrap_v: WrapMode::Repeat,
108            lod_bias: 0.0,
109            max_anisotropy: 16,
110        }
111    }
112
113    /// Return `true` if this config requires mipmap generation.
114    #[must_use]
115    pub fn needs_mipmaps(&self) -> bool {
116        self.min_filter.uses_mipmaps()
117    }
118
119    /// Clamp `max_anisotropy` to the hardware-reported limit.
120    pub fn clamp_anisotropy(&mut self, hardware_max: u8) {
121        self.max_anisotropy = self.max_anisotropy.min(hardware_max);
122    }
123}
124
125impl Default for SamplerConfig {
126    fn default() -> Self {
127        Self::linear_clamp()
128    }
129}
130
131/// A handle to a cached sampler, returned by [`SamplerCache`].
132///
133/// The `u64` is an opaque identifier generated from the config hash.
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
135pub struct SamplerHandle(u64);
136
137impl SamplerHandle {
138    /// The underlying numeric identifier.
139    #[must_use]
140    pub fn id(self) -> u64 {
141        self.0
142    }
143}
144
145/// In-memory cache that de-duplicates identical sampler configurations.
146///
147/// When the same [`SamplerConfig`] is requested more than once the same
148/// [`SamplerHandle`] is returned, avoiding redundant GPU object creation.
149///
150/// # Example
151///
152/// ```
153/// use oximedia_gpu::sampler::{SamplerCache, SamplerConfig};
154///
155/// let mut cache = SamplerCache::new();
156/// let h1 = cache.get_or_insert(SamplerConfig::linear_clamp());
157/// let h2 = cache.get_or_insert(SamplerConfig::linear_clamp());
158/// assert_eq!(h1, h2); // same config → same handle
159/// assert_eq!(cache.len(), 1);
160/// ```
161#[derive(Debug, Default)]
162pub struct SamplerCache {
163    /// Maps a stable config hash to its handle.
164    entries: HashMap<u64, SamplerHandle>,
165    /// Counter used to generate monotonically increasing handles.
166    next_id: u64,
167}
168
169impl SamplerCache {
170    /// Create an empty cache.
171    #[must_use]
172    pub fn new() -> Self {
173        Self::default()
174    }
175
176    /// Compute a stable hash key for a [`SamplerConfig`].
177    fn config_key(cfg: &SamplerConfig) -> u64 {
178        use std::collections::hash_map::DefaultHasher;
179        use std::hash::{Hash, Hasher};
180        let mut h = DefaultHasher::new();
181        cfg.min_filter.hash(&mut h);
182        cfg.mag_filter.hash(&mut h);
183        cfg.wrap_u.hash(&mut h);
184        cfg.wrap_v.hash(&mut h);
185        // Treat lod_bias bits as a stable integer for hashing.
186        cfg.lod_bias.to_bits().hash(&mut h);
187        cfg.max_anisotropy.hash(&mut h);
188        h.finish()
189    }
190
191    /// Return an existing handle if `config` is already cached, or allocate a
192    /// new one and store it.
193    pub fn get_or_insert(&mut self, config: SamplerConfig) -> SamplerHandle {
194        let key = Self::config_key(&config);
195        if let Some(&handle) = self.entries.get(&key) {
196            return handle;
197        }
198        let handle = SamplerHandle(self.next_id);
199        self.next_id += 1;
200        self.entries.insert(key, handle);
201        handle
202    }
203
204    /// Number of unique sampler configurations in the cache.
205    #[must_use]
206    pub fn len(&self) -> usize {
207        self.entries.len()
208    }
209
210    /// Return `true` if the cache contains no entries.
211    #[must_use]
212    pub fn is_empty(&self) -> bool {
213        self.entries.is_empty()
214    }
215
216    /// Remove all cached entries and reset the handle counter.
217    pub fn clear(&mut self) {
218        self.entries.clear();
219        self.next_id = 0;
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn filter_mode_linear_no_mipmap() {
229        assert!(!FilterMode::Linear.uses_mipmaps());
230    }
231
232    #[test]
233    fn filter_mode_nearest_no_mipmap() {
234        assert!(!FilterMode::Nearest.uses_mipmaps());
235    }
236
237    #[test]
238    fn filter_mode_mipmap_variants_use_mipmaps() {
239        assert!(FilterMode::NearestMipmapNearest.uses_mipmaps());
240        assert!(FilterMode::LinearMipmapLinear.uses_mipmaps());
241    }
242
243    #[test]
244    fn filter_mode_default_is_linear() {
245        assert_eq!(FilterMode::default(), FilterMode::Linear);
246    }
247
248    #[test]
249    fn wrap_mode_default_is_clamp_to_edge() {
250        assert_eq!(WrapMode::default(), WrapMode::ClampToEdge);
251    }
252
253    #[test]
254    fn sampler_config_linear_clamp_no_mipmaps() {
255        let cfg = SamplerConfig::linear_clamp();
256        assert!(!cfg.needs_mipmaps());
257    }
258
259    #[test]
260    fn sampler_config_trilinear_needs_mipmaps() {
261        let cfg = SamplerConfig::trilinear_anisotropic();
262        assert!(cfg.needs_mipmaps());
263    }
264
265    #[test]
266    fn sampler_config_clamp_anisotropy() {
267        let mut cfg = SamplerConfig::trilinear_anisotropic();
268        cfg.clamp_anisotropy(4);
269        assert_eq!(cfg.max_anisotropy, 4);
270    }
271
272    #[test]
273    fn sampler_config_clamp_anisotropy_no_increase() {
274        let mut cfg = SamplerConfig::linear_clamp();
275        cfg.clamp_anisotropy(32);
276        assert_eq!(cfg.max_anisotropy, 1); // was 1, not increased
277    }
278
279    #[test]
280    fn sampler_cache_deduplicate() {
281        let mut cache = SamplerCache::new();
282        let h1 = cache.get_or_insert(SamplerConfig::linear_clamp());
283        let h2 = cache.get_or_insert(SamplerConfig::linear_clamp());
284        assert_eq!(h1, h2);
285        assert_eq!(cache.len(), 1);
286    }
287
288    #[test]
289    fn sampler_cache_different_configs_different_handles() {
290        let mut cache = SamplerCache::new();
291        let h1 = cache.get_or_insert(SamplerConfig::linear_clamp());
292        let h2 = cache.get_or_insert(SamplerConfig::nearest_repeat());
293        assert_ne!(h1, h2);
294        assert_eq!(cache.len(), 2);
295    }
296
297    #[test]
298    fn sampler_cache_is_empty_initially() {
299        let cache = SamplerCache::new();
300        assert!(cache.is_empty());
301    }
302
303    #[test]
304    fn sampler_cache_clear_resets() {
305        let mut cache = SamplerCache::new();
306        cache.get_or_insert(SamplerConfig::linear_clamp());
307        cache.clear();
308        assert!(cache.is_empty());
309        assert_eq!(cache.next_id, 0);
310    }
311
312    #[test]
313    fn sampler_handle_id() {
314        let mut cache = SamplerCache::new();
315        let h = cache.get_or_insert(SamplerConfig::linear_clamp());
316        assert_eq!(h.id(), 0);
317    }
318
319    #[test]
320    fn sampler_config_default_is_linear_clamp() {
321        let a = SamplerConfig::default();
322        let b = SamplerConfig::linear_clamp();
323        assert_eq!(a, b);
324    }
325
326    #[test]
327    fn nearest_repeat_config_values() {
328        let cfg = SamplerConfig::nearest_repeat();
329        assert_eq!(cfg.min_filter, FilterMode::Nearest);
330        assert_eq!(cfg.wrap_u, WrapMode::Repeat);
331    }
332}