Skip to main content

mittens_engine/engine/ecs/system/
texture_system.rs

1use crate::engine::ecs::component::texture::TextureSource;
2use crate::engine::ecs::component::{
3    CatEngineTextureFormat, RenderableComponent, TextureComponent, TextureFilteringComponent,
4};
5use crate::engine::ecs::{ComponentId, World};
6use crate::engine::graphics::{TextureFiltering, TextureHandle, TextureUploader, VisualWorld};
7use std::collections::HashMap;
8use std::io::Cursor;
9use std::path::{Path, PathBuf};
10
11#[derive(Debug, Clone)]
12struct TextureRecord {
13    uri: Option<String>,
14    render_image: Option<String>,
15    format: CatEngineTextureFormat,
16    gpu: Option<TextureHandle>,
17}
18
19#[derive(Debug, Clone, Copy)]
20struct TextureFilteringRecord {
21    filtering: TextureFiltering,
22}
23
24#[derive(Debug, Default)]
25pub struct TextureSystem {
26    textures: HashMap<ComponentId, TextureRecord>,
27    uri_cache: HashMap<String, TextureHandle>,
28    /// RenderableComponent cid -> TextureComponent cid
29    pending_attach: HashMap<ComponentId, ComponentId>,
30
31    filterings: HashMap<ComponentId, TextureFilteringRecord>,
32    /// RenderableComponent cid -> TextureFilteringComponent cid
33    pending_filtering_attach: HashMap<ComponentId, ComponentId>,
34}
35
36impl TextureSystem {
37    pub fn new() -> Self {
38        Self::default()
39    }
40
41    /// Register an already-uploaded texture handle for a URI-like key.
42    ///
43    /// This is intended for virtual keys (e.g. GLTF imported textures like
44    /// "{gltf_name}:{image_name_or_index}") where the data is not loaded from disk
45    /// by `TextureSystem`.
46    pub fn register_cached_texture(&mut self, uri: impl Into<String>, handle: TextureHandle) {
47        self.uri_cache.insert(uri.into(), handle);
48    }
49
50    pub fn register_texture(
51        &mut self,
52        world: &mut World,
53        _visuals: &mut VisualWorld,
54        component: ComponentId,
55    ) {
56        let Some(tex_comp) = world.get_component_by_id_as::<TextureComponent>(component) else {
57            return;
58        };
59
60        self.textures.insert(
61            component,
62            TextureRecord {
63                uri: tex_comp.uri().map(|s| s.to_string()),
64                render_image: tex_comp.render_image.clone(),
65                format: tex_comp.format,
66                gpu: match tex_comp.source {
67                    TextureSource::Handle(h) => Some(h),
68                    TextureSource::Uri(_) => None,
69                },
70            },
71        );
72
73        // If this texture is attached under a renderable, remember that relationship.
74        let mut cur = component;
75        while let Some(parent) = world.parent_of(cur) {
76            if world
77                .get_component_by_id_as::<RenderableComponent>(parent)
78                .is_some()
79            {
80                self.pending_attach.insert(parent, component);
81                break;
82            }
83            cur = parent;
84        }
85    }
86
87    pub fn register_texture_filtering(
88        &mut self,
89        world: &mut World,
90        _visuals: &mut VisualWorld,
91        component: ComponentId,
92    ) {
93        let Some(filter_comp) =
94            world.get_component_by_id_as::<TextureFilteringComponent>(component)
95        else {
96            return;
97        };
98
99        self.filterings
100            .entry(component)
101            .or_insert(TextureFilteringRecord {
102                filtering: filter_comp.filtering,
103            });
104
105        // If this filtering is attached under a renderable, remember that relationship.
106        let mut cur = component;
107        while let Some(parent) = world.parent_of(cur) {
108            if world
109                .get_component_by_id_as::<RenderableComponent>(parent)
110                .is_some()
111            {
112                self.pending_filtering_attach.insert(parent, component);
113                break;
114            }
115            cur = parent;
116        }
117    }
118
119    /// Decode+upload any textures that are now attachable to renderables.
120    ///
121    /// Must run after renderables are flushed into `VisualWorld` so we can update instance handles.
122    pub fn flush_pending(
123        &mut self,
124        world: &mut World,
125        visuals: &mut VisualWorld,
126        uploader: &mut dyn TextureUploader,
127    ) {
128        // Apply any pending filtering choices to renderable instances.
129        let filtering_pairs: Vec<(ComponentId, ComponentId)> = self
130            .pending_filtering_attach
131            .iter()
132            .map(|(&r, &f)| (r, f))
133            .collect();
134
135        for (renderable_cid, filtering_cid) in filtering_pairs {
136            let Some(renderable_comp) =
137                world.get_component_by_id_as::<RenderableComponent>(renderable_cid)
138            else {
139                let _ = self.pending_filtering_attach.remove(&renderable_cid);
140                continue;
141            };
142
143            let Some(instance_handle) = renderable_comp.get_handle() else {
144                // Renderable not in VisualWorld yet.
145                continue;
146            };
147
148            let Some(record) = self.filterings.get(&filtering_cid).copied() else {
149                let _ = self.pending_filtering_attach.remove(&renderable_cid);
150                continue;
151            };
152
153            let _ = visuals.update_texture_filtering(instance_handle, record.filtering);
154            let _ = self.pending_filtering_attach.remove(&renderable_cid);
155        }
156
157        let pairs: Vec<(ComponentId, ComponentId)> =
158            self.pending_attach.iter().map(|(&r, &t)| (r, t)).collect();
159
160        for (renderable_cid, texture_cid) in pairs {
161            let Some(renderable_comp) =
162                world.get_component_by_id_as::<RenderableComponent>(renderable_cid)
163            else {
164                let _ = self.pending_attach.remove(&renderable_cid);
165                continue;
166            };
167
168            let Some(instance_handle) = renderable_comp.get_handle() else {
169                // Renderable not in VisualWorld yet.
170                continue;
171            };
172
173            let Some(record) = self.textures.get_mut(&texture_cid) else {
174                let _ = self.pending_attach.remove(&renderable_cid);
175                continue;
176            };
177
178            if record.gpu.is_none() {
179                if let Some(render_image) = record.render_image.as_deref() {
180                    if let Some(existing) = visuals.runtime_texture_handle(render_image) {
181                        record.gpu = Some(existing);
182                    }
183                }
184
185                if let Some(uri) = record.uri.as_deref() {
186                    if let Some(cached) = self.uri_cache.get(uri).copied() {
187                        record.gpu = Some(cached);
188                    }
189                }
190            }
191
192            let tex_handle = match record.gpu {
193                Some(h) => h,
194                None => {
195                    let Some(uri) = record.uri.as_deref() else {
196                        // No URI and no pre-provided handle. Nothing we can do.
197                        let _ = self.pending_attach.remove(&renderable_cid);
198                        continue;
199                    };
200
201                    // Virtual URI keys (e.g. GLTF imported textures) are resolved purely via
202                    // `uri_cache`. If the handle isn't registered yet, keep the attachment
203                    // pending so we can retry later.
204                    if is_virtual_texture_key(uri) {
205                        // If the GLTFSystem hasn't registered this key yet, just wait.
206                        continue;
207                    }
208
209                    let raw_path_str = uri.strip_prefix("file://").unwrap_or(uri);
210                    let raw_path = Path::new(raw_path_str);
211
212                    let mut tried: Vec<PathBuf> = Vec::new();
213                    let resolved_path: Option<PathBuf> = if raw_path.is_absolute() {
214                        tried.push(raw_path.to_path_buf());
215                        if raw_path.exists() {
216                            Some(raw_path.to_path_buf())
217                        } else {
218                            None
219                        }
220                    } else {
221                        // 1) Current working directory
222                        if let Ok(cwd) = std::env::current_dir() {
223                            let p = cwd.join(raw_path);
224                            tried.push(p.clone());
225                            if p.exists() {
226                                Some(p)
227                            } else {
228                                // 2) Crate root (works even if CWD is target/...)
229                                let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
230                                let p2 = manifest_dir.join(raw_path);
231                                tried.push(p2.clone());
232                                if p2.exists() { Some(p2) } else { None }
233                            }
234                        } else {
235                            let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
236                            let p2 = manifest_dir.join(raw_path);
237                            tried.push(p2.clone());
238                            if p2.exists() { Some(p2) } else { None }
239                        }
240                    };
241
242                    let Some(path) = resolved_path else {
243                        let cwd = std::env::current_dir()
244                            .map(|p| p.display().to_string())
245                            .unwrap_or_else(|_| "<unknown>".to_string());
246                        println!("[TextureSystem] read failed for '{uri}'");
247                        println!("[TextureSystem]   cwd = {cwd}");
248                        for p in tried {
249                            println!("[TextureSystem]   tried: {}", p.display());
250                        }
251                        let _ = self.pending_attach.remove(&renderable_cid);
252                        continue;
253                    };
254
255                    let bytes = match std::fs::read(&path) {
256                        Ok(b) => b,
257                        Err(e) => {
258                            let cwd = std::env::current_dir()
259                                .map(|p| p.display().to_string())
260                                .unwrap_or_else(|_| "<unknown>".to_string());
261                            println!("[TextureSystem] read failed for '{uri}': {e}");
262                            println!("[TextureSystem]   cwd = {cwd}");
263                            println!("[TextureSystem]   resolved: {}", path.display());
264                            let _ = self.pending_attach.remove(&renderable_cid);
265                            continue;
266                        }
267                    };
268
269                    let handle = match record.format {
270                        CatEngineTextureFormat::DdsBc7 => match decode_dds_bc7(&bytes) {
271                            Ok(decoded) => match uploader.upload_texture_bc7(
272                                &decoded.bc7_blocks,
273                                decoded.width,
274                                decoded.height,
275                                decoded.srgb,
276                            ) {
277                                Ok(h) => h,
278                                Err(e) => {
279                                    println!(
280                                        "[TextureSystem] BC7 upload failed for '{uri}': {:?}",
281                                        e
282                                    );
283                                    let _ = self.pending_attach.remove(&renderable_cid);
284                                    continue;
285                                }
286                            },
287                            Err(e) => {
288                                println!("[TextureSystem] DDS/BC7 decode failed for '{uri}': {e}");
289                                let _ = self.pending_attach.remove(&renderable_cid);
290                                continue;
291                            }
292                        },
293                        CatEngineTextureFormat::Rgba8 => {
294                            let dyn_img = match image::load_from_memory(&bytes) {
295                                Ok(i) => i,
296                                Err(e) => {
297                                    println!("[TextureSystem] decode failed for '{uri}': {:?}", e);
298                                    let _ = self.pending_attach.remove(&renderable_cid);
299                                    continue;
300                                }
301                            };
302
303                            let rgba = dyn_img.to_rgba8();
304                            let (w, h) = rgba.dimensions();
305
306                            match uploader.upload_texture_rgba8(rgba.as_raw(), w, h) {
307                                Ok(h) => h,
308                                Err(e) => {
309                                    println!("[TextureSystem] upload failed for '{uri}': {:?}", e);
310                                    let _ = self.pending_attach.remove(&renderable_cid);
311                                    continue;
312                                }
313                            }
314                        }
315                    };
316
317                    record.gpu = Some(handle);
318                    self.uri_cache.insert(uri.to_string(), handle);
319                    handle
320                }
321            };
322
323            let _ = visuals.update_texture(instance_handle, Some(tex_handle));
324            let _ = self.pending_attach.remove(&renderable_cid);
325        }
326    }
327}
328
329fn is_virtual_texture_key(uri: &str) -> bool {
330    // Heuristic for v1: GLTF imported textures use the pattern "{gltf_name}:{image_name_or_index}".
331    // We treat these as non-filesystem keys that must be resolved via `uri_cache`.
332    //
333    // This avoids trying to read them from disk and allows components to be created before the
334    // GLTF's textures are uploaded.
335    !uri.starts_with("file://") && uri.contains(':')
336}
337
338struct Bc7Decoded {
339    width: u32,
340    height: u32,
341    srgb: bool,
342    bc7_blocks: Vec<u8>,
343}
344
345fn decode_dds_bc7(bytes: &[u8]) -> Result<Bc7Decoded, String> {
346    let mut cursor = Cursor::new(bytes);
347    let dds = ddsfile::Dds::read(&mut cursor).map_err(|e| format!("{e:?}"))?;
348
349    let width = dds.get_width();
350    let height = dds.get_height();
351    if width == 0 || height == 0 {
352        return Err("DDS has zero size".to_string());
353    }
354
355    let dxgi = dds
356        .get_dxgi_format()
357        .ok_or_else(|| "DDS missing DXGI format (need BC7 in DX10 header)".to_string())?;
358
359    let srgb = match dxgi {
360        ddsfile::DxgiFormat::BC7_UNorm => false,
361        ddsfile::DxgiFormat::BC7_UNorm_sRGB => true,
362        other => {
363            return Err(format!("DDS is not BC7 (got {other:?})"));
364        }
365    };
366
367    let data: &[u8] = dds.data.as_ref();
368    if data.is_empty() {
369        return Err("DDS contains no data".to_string());
370    }
371
372    // We only use the top mip for now.
373    let blocks_w = (width + 3) / 4;
374    let blocks_h = (height + 3) / 4;
375    let expected_len = blocks_w as usize * blocks_h as usize * 16;
376    if data.len() < expected_len {
377        return Err(format!(
378            "DDS data too small for BC7 level 0: got={}, need={}",
379            data.len(),
380            expected_len
381        ));
382    }
383
384    Ok(Bc7Decoded {
385        width,
386        height,
387        srgb,
388        bc7_blocks: data[..expected_len].to_vec(),
389    })
390}