mittens_engine/engine/ecs/system/
texture_system.rs1use 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 pending_attach: HashMap<ComponentId, ComponentId>,
30
31 filterings: HashMap<ComponentId, TextureFilteringRecord>,
32 pending_filtering_attach: HashMap<ComponentId, ComponentId>,
34}
35
36impl TextureSystem {
37 pub fn new() -> Self {
38 Self::default()
39 }
40
41 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 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 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 pub fn flush_pending(
123 &mut self,
124 world: &mut World,
125 visuals: &mut VisualWorld,
126 uploader: &mut dyn TextureUploader,
127 ) {
128 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 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 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 let _ = self.pending_attach.remove(&renderable_cid);
198 continue;
199 };
200
201 if is_virtual_texture_key(uri) {
205 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 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 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 !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 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}