Skip to main content

oxihuman_viewer/
texture_cache.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4#![allow(dead_code)]
5
6// ── Enumerations ──────────────────────────────────────────────────────────────
7
8/// Pixel format of a texture.
9pub enum TextureFormat {
10    Rgba8,
11    Rgba16Float,
12    R8,
13    Rg8,
14    Bc1,
15    Bc3,
16    Bc7,
17}
18
19/// Filtering mode.
20pub enum TextureFilter {
21    Nearest,
22    Linear,
23    Anisotropic(u8),
24}
25
26/// Texture address / wrap mode.
27pub enum TextureWrap {
28    Clamp,
29    Repeat,
30    Mirror,
31}
32
33// ── Descriptors ───────────────────────────────────────────────────────────────
34
35/// Describes a texture without holding pixel data.
36pub struct TextureDescriptor {
37    pub label: String,
38    pub width: u32,
39    pub height: u32,
40    pub mip_levels: u32,
41    pub format: TextureFormat,
42    pub filter: TextureFilter,
43    pub wrap_u: TextureWrap,
44    pub wrap_v: TextureWrap,
45}
46
47/// A texture entry held in the cache.
48pub struct TextureEntry {
49    pub id: u32,
50    pub descriptor: TextureDescriptor,
51    /// `None` = placeholder / streaming not yet loaded.
52    pub data: Option<Vec<u8>>,
53    pub loaded: bool,
54}
55
56// ── Cache ─────────────────────────────────────────────────────────────────────
57
58/// Manages a collection of GPU textures (CPU-side stubs for Phase 2).
59pub struct TextureCache {
60    entries: Vec<TextureEntry>,
61    next_id: u32,
62}
63
64impl TextureCache {
65    /// Create an empty texture cache.
66    pub fn new() -> Self {
67        Self {
68            entries: Vec::new(),
69            next_id: 0,
70        }
71    }
72
73    /// Insert a new texture, returning its unique id.
74    pub fn insert(&mut self, desc: TextureDescriptor, data: Option<Vec<u8>>) -> u32 {
75        let id = self.next_id;
76        self.next_id += 1;
77        let loaded = data.is_some();
78        self.entries.push(TextureEntry {
79            id,
80            descriptor: desc,
81            data,
82            loaded,
83        });
84        id
85    }
86
87    /// Retrieve a reference to a texture by id.
88    pub fn get(&self, id: u32) -> Option<&TextureEntry> {
89        self.entries.iter().find(|e| e.id == id)
90    }
91
92    /// Remove a texture by id. Returns `true` if it was present.
93    pub fn remove(&mut self, id: u32) -> bool {
94        if let Some(pos) = self.entries.iter().position(|e| e.id == id) {
95            self.entries.remove(pos);
96            true
97        } else {
98            false
99        }
100    }
101
102    /// Total number of entries (loaded or not).
103    pub fn count(&self) -> usize {
104        self.entries.len()
105    }
106
107    /// Number of entries where `loaded == true`.
108    pub fn loaded_count(&self) -> usize {
109        self.entries.iter().filter(|e| e.loaded).count()
110    }
111
112    /// Supply pixel data for a previously-inserted placeholder texture.
113    /// Returns `false` if the id is not found.
114    pub fn mark_loaded(&mut self, id: u32, data: Vec<u8>) -> bool {
115        if let Some(entry) = self.entries.iter_mut().find(|e| e.id == id) {
116            entry.data = Some(data);
117            entry.loaded = true;
118            true
119        } else {
120            false
121        }
122    }
123
124    /// Evict pixel data from all entries (streaming eviction), keeping descriptors.
125    pub fn evict_all(&mut self) {
126        for entry in &mut self.entries {
127            entry.data = None;
128            entry.loaded = false;
129        }
130    }
131
132    /// Sum of memory used by all loaded textures.
133    pub fn total_memory_bytes(&self) -> usize {
134        self.entries
135            .iter()
136            .filter(|e| e.loaded)
137            .map(|e| texture_memory_bytes(&e.descriptor))
138            .sum()
139    }
140}
141
142impl Default for TextureCache {
143    fn default() -> Self {
144        Self::new()
145    }
146}
147
148// ── Helper functions ──────────────────────────────────────────────────────────
149
150/// Bytes per pixel for a given format (BC formats use sub-pixel values rounded up).
151pub fn texture_format_bytes(fmt: &TextureFormat) -> u32 {
152    match fmt {
153        TextureFormat::R8 => 1,
154        TextureFormat::Rg8 => 2,
155        TextureFormat::Rgba8 => 4,
156        TextureFormat::Rgba16Float => 8,
157        // BC1: 0.5 bytes/pixel → represented as 0 for integer math; caller uses special path
158        TextureFormat::Bc1 => 0, // handled separately in texture_memory_bytes
159        // BC3/BC7: 1 byte/pixel
160        TextureFormat::Bc3 => 1,
161        TextureFormat::Bc7 => 1,
162    }
163}
164
165/// Approximate memory size in bytes for width×height texels (ignoring mips/compression exactly).
166pub fn texture_memory_bytes(desc: &TextureDescriptor) -> usize {
167    let pixels = desc.width as usize * desc.height as usize;
168    match &desc.format {
169        TextureFormat::Bc1 => {
170            // 0.5 bytes per pixel = pixels / 2
171            pixels / 2
172        }
173        other => pixels * texture_format_bytes(other) as usize,
174    }
175}
176
177/// Return a 1×1 pink RGBA8 placeholder texture.
178pub fn default_placeholder_texture() -> TextureEntry {
179    let desc = TextureDescriptor {
180        label: "placeholder".to_string(),
181        width: 1,
182        height: 1,
183        mip_levels: 1,
184        format: TextureFormat::Rgba8,
185        filter: TextureFilter::Nearest,
186        wrap_u: TextureWrap::Clamp,
187        wrap_v: TextureWrap::Clamp,
188    };
189    // Pink: R=255, G=105, B=180, A=255
190    let data = vec![255u8, 105, 180, 255];
191    TextureEntry {
192        id: u32::MAX,
193        descriptor: desc,
194        data: Some(data),
195        loaded: true,
196    }
197}
198
199// ── Tests ─────────────────────────────────────────────────────────────────────
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    fn rgba8_desc(label: &str, w: u32, h: u32) -> TextureDescriptor {
206        TextureDescriptor {
207            label: label.to_string(),
208            width: w,
209            height: h,
210            mip_levels: 1,
211            format: TextureFormat::Rgba8,
212            filter: TextureFilter::Linear,
213            wrap_u: TextureWrap::Repeat,
214            wrap_v: TextureWrap::Repeat,
215        }
216    }
217
218    #[test]
219    fn new_cache_is_empty() {
220        let cache = TextureCache::new();
221        assert_eq!(cache.count(), 0);
222        assert_eq!(cache.loaded_count(), 0);
223    }
224
225    #[test]
226    fn insert_returns_incrementing_ids() {
227        let mut cache = TextureCache::new();
228        let id0 = cache.insert(rgba8_desc("a", 4, 4), None);
229        let id1 = cache.insert(rgba8_desc("b", 4, 4), None);
230        assert_eq!(id0, 0);
231        assert_eq!(id1, 1);
232    }
233
234    #[test]
235    fn get_found() {
236        let mut cache = TextureCache::new();
237        let id = cache.insert(rgba8_desc("tex", 8, 8), None);
238        let entry = cache.get(id);
239        assert!(entry.is_some());
240        assert_eq!(entry.expect("should succeed").id, id);
241    }
242
243    #[test]
244    fn get_not_found() {
245        let cache = TextureCache::new();
246        assert!(cache.get(999).is_none());
247    }
248
249    #[test]
250    fn remove_true_when_present() {
251        let mut cache = TextureCache::new();
252        let id = cache.insert(rgba8_desc("x", 2, 2), None);
253        assert!(cache.remove(id));
254        assert_eq!(cache.count(), 0);
255    }
256
257    #[test]
258    fn remove_false_when_absent() {
259        let mut cache = TextureCache::new();
260        assert!(!cache.remove(42));
261    }
262
263    #[test]
264    fn count_and_loaded_count() {
265        let mut cache = TextureCache::new();
266        cache.insert(rgba8_desc("a", 4, 4), None);
267        cache.insert(rgba8_desc("b", 4, 4), Some(vec![0u8; 64]));
268        assert_eq!(cache.count(), 2);
269        assert_eq!(cache.loaded_count(), 1);
270    }
271
272    #[test]
273    fn mark_loaded_sets_data_and_flag() {
274        let mut cache = TextureCache::new();
275        let id = cache.insert(rgba8_desc("stream", 4, 4), None);
276        assert_eq!(cache.loaded_count(), 0);
277        let ok = cache.mark_loaded(id, vec![0u8; 64]);
278        assert!(ok);
279        assert_eq!(cache.loaded_count(), 1);
280        assert!(cache.get(id).expect("should succeed").data.is_some());
281    }
282
283    #[test]
284    fn mark_loaded_returns_false_for_missing_id() {
285        let mut cache = TextureCache::new();
286        assert!(!cache.mark_loaded(999, vec![0u8; 4]));
287    }
288
289    #[test]
290    fn evict_all_clears_data_and_loaded_flag() {
291        let mut cache = TextureCache::new();
292        cache.insert(rgba8_desc("t", 2, 2), Some(vec![0u8; 16]));
293        cache.insert(rgba8_desc("u", 2, 2), Some(vec![0u8; 16]));
294        assert_eq!(cache.loaded_count(), 2);
295        cache.evict_all();
296        assert_eq!(cache.loaded_count(), 0);
297        assert_eq!(cache.count(), 2); // descriptors remain
298        for entry in &cache.entries {
299            assert!(entry.data.is_none());
300        }
301    }
302
303    #[test]
304    fn texture_format_bytes_rgba8_is_4() {
305        assert_eq!(texture_format_bytes(&TextureFormat::Rgba8), 4);
306    }
307
308    #[test]
309    fn texture_format_bytes_r8_is_1() {
310        assert_eq!(texture_format_bytes(&TextureFormat::R8), 1);
311    }
312
313    #[test]
314    fn texture_format_bytes_rgba16float_is_8() {
315        assert_eq!(texture_format_bytes(&TextureFormat::Rgba16Float), 8);
316    }
317
318    #[test]
319    fn texture_memory_bytes_rgba8_formula() {
320        let desc = rgba8_desc("t", 4, 4);
321        // 4*4*4 = 64
322        assert_eq!(texture_memory_bytes(&desc), 64);
323    }
324
325    #[test]
326    fn texture_memory_bytes_bc1_half_pixel() {
327        let desc = TextureDescriptor {
328            label: "bc".to_string(),
329            width: 4,
330            height: 4,
331            mip_levels: 1,
332            format: TextureFormat::Bc1,
333            filter: TextureFilter::Linear,
334            wrap_u: TextureWrap::Clamp,
335            wrap_v: TextureWrap::Clamp,
336        };
337        // 4*4/2 = 8
338        assert_eq!(texture_memory_bytes(&desc), 8);
339    }
340
341    #[test]
342    fn total_memory_bytes_sum_of_loaded() {
343        let mut cache = TextureCache::new();
344        // Insert two 4×4 RGBA8 textures with data — each = 64 bytes
345        cache.insert(rgba8_desc("a", 4, 4), Some(vec![0u8; 64]));
346        cache.insert(rgba8_desc("b", 4, 4), Some(vec![0u8; 64]));
347        // One placeholder without data
348        cache.insert(rgba8_desc("c", 4, 4), None);
349        assert_eq!(cache.total_memory_bytes(), 128);
350    }
351
352    #[test]
353    fn default_placeholder_texture_non_null() {
354        let entry = default_placeholder_texture();
355        assert!(entry.data.is_some());
356        let data = entry.data.expect("should succeed");
357        assert_eq!(data.len(), 4);
358        // Pink: R=255, G=105, B=180, A=255
359        assert_eq!(data[0], 255);
360        assert_eq!(data[2], 180);
361    }
362}