use crate::engine::ecs::component::texture::TextureSource;
use crate::engine::ecs::component::{
CatEngineTextureFormat, RenderableComponent, TextureComponent, TextureFilteringComponent,
};
use crate::engine::ecs::{ComponentId, World};
use crate::engine::graphics::{TextureFiltering, TextureHandle, TextureUploader, VisualWorld};
use std::collections::HashMap;
use std::io::Cursor;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
struct TextureRecord {
uri: Option<String>,
render_image: Option<String>,
format: CatEngineTextureFormat,
gpu: Option<TextureHandle>,
}
#[derive(Debug, Clone, Copy)]
struct TextureFilteringRecord {
filtering: TextureFiltering,
}
#[derive(Debug, Default)]
pub struct TextureSystem {
textures: HashMap<ComponentId, TextureRecord>,
uri_cache: HashMap<String, TextureHandle>,
pending_attach: HashMap<ComponentId, ComponentId>,
filterings: HashMap<ComponentId, TextureFilteringRecord>,
pending_filtering_attach: HashMap<ComponentId, ComponentId>,
}
impl TextureSystem {
pub fn new() -> Self {
Self::default()
}
pub fn register_cached_texture(&mut self, uri: impl Into<String>, handle: TextureHandle) {
self.uri_cache.insert(uri.into(), handle);
}
pub fn register_texture(
&mut self,
world: &mut World,
_visuals: &mut VisualWorld,
component: ComponentId,
) {
let Some(tex_comp) = world.get_component_by_id_as::<TextureComponent>(component) else {
return;
};
self.textures.insert(
component,
TextureRecord {
uri: tex_comp.uri().map(|s| s.to_string()),
render_image: tex_comp.render_image.clone(),
format: tex_comp.format,
gpu: match tex_comp.source {
TextureSource::Handle(h) => Some(h),
TextureSource::Uri(_) => None,
},
},
);
let mut cur = component;
while let Some(parent) = world.parent_of(cur) {
if world
.get_component_by_id_as::<RenderableComponent>(parent)
.is_some()
{
self.pending_attach.insert(parent, component);
break;
}
cur = parent;
}
}
pub fn register_texture_filtering(
&mut self,
world: &mut World,
_visuals: &mut VisualWorld,
component: ComponentId,
) {
let Some(filter_comp) =
world.get_component_by_id_as::<TextureFilteringComponent>(component)
else {
return;
};
self.filterings
.entry(component)
.or_insert(TextureFilteringRecord {
filtering: filter_comp.filtering,
});
let mut cur = component;
while let Some(parent) = world.parent_of(cur) {
if world
.get_component_by_id_as::<RenderableComponent>(parent)
.is_some()
{
self.pending_filtering_attach.insert(parent, component);
break;
}
cur = parent;
}
}
pub fn flush_pending(
&mut self,
world: &mut World,
visuals: &mut VisualWorld,
uploader: &mut dyn TextureUploader,
) {
let filtering_pairs: Vec<(ComponentId, ComponentId)> = self
.pending_filtering_attach
.iter()
.map(|(&r, &f)| (r, f))
.collect();
for (renderable_cid, filtering_cid) in filtering_pairs {
let Some(renderable_comp) =
world.get_component_by_id_as::<RenderableComponent>(renderable_cid)
else {
let _ = self.pending_filtering_attach.remove(&renderable_cid);
continue;
};
let Some(instance_handle) = renderable_comp.get_handle() else {
continue;
};
let Some(record) = self.filterings.get(&filtering_cid).copied() else {
let _ = self.pending_filtering_attach.remove(&renderable_cid);
continue;
};
let _ = visuals.update_texture_filtering(instance_handle, record.filtering);
let _ = self.pending_filtering_attach.remove(&renderable_cid);
}
let pairs: Vec<(ComponentId, ComponentId)> =
self.pending_attach.iter().map(|(&r, &t)| (r, t)).collect();
for (renderable_cid, texture_cid) in pairs {
let Some(renderable_comp) =
world.get_component_by_id_as::<RenderableComponent>(renderable_cid)
else {
let _ = self.pending_attach.remove(&renderable_cid);
continue;
};
let Some(instance_handle) = renderable_comp.get_handle() else {
continue;
};
let Some(record) = self.textures.get_mut(&texture_cid) else {
let _ = self.pending_attach.remove(&renderable_cid);
continue;
};
if record.gpu.is_none() {
if let Some(render_image) = record.render_image.as_deref() {
if let Some(existing) = visuals.runtime_texture_handle(render_image) {
record.gpu = Some(existing);
}
}
if let Some(uri) = record.uri.as_deref() {
if let Some(cached) = self.uri_cache.get(uri).copied() {
record.gpu = Some(cached);
}
}
}
let tex_handle = match record.gpu {
Some(h) => h,
None => {
let Some(uri) = record.uri.as_deref() else {
let _ = self.pending_attach.remove(&renderable_cid);
continue;
};
if is_virtual_texture_key(uri) {
continue;
}
let raw_path_str = uri.strip_prefix("file://").unwrap_or(uri);
let raw_path = Path::new(raw_path_str);
let mut tried: Vec<PathBuf> = Vec::new();
let resolved_path: Option<PathBuf> = if raw_path.is_absolute() {
tried.push(raw_path.to_path_buf());
if raw_path.exists() {
Some(raw_path.to_path_buf())
} else {
None
}
} else {
if let Ok(cwd) = std::env::current_dir() {
let p = cwd.join(raw_path);
tried.push(p.clone());
if p.exists() {
Some(p)
} else {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let p2 = manifest_dir.join(raw_path);
tried.push(p2.clone());
if p2.exists() { Some(p2) } else { None }
}
} else {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let p2 = manifest_dir.join(raw_path);
tried.push(p2.clone());
if p2.exists() { Some(p2) } else { None }
}
};
let Some(path) = resolved_path else {
let cwd = std::env::current_dir()
.map(|p| p.display().to_string())
.unwrap_or_else(|_| "<unknown>".to_string());
println!("[TextureSystem] read failed for '{uri}'");
println!("[TextureSystem] cwd = {cwd}");
for p in tried {
println!("[TextureSystem] tried: {}", p.display());
}
let _ = self.pending_attach.remove(&renderable_cid);
continue;
};
let bytes = match std::fs::read(&path) {
Ok(b) => b,
Err(e) => {
let cwd = std::env::current_dir()
.map(|p| p.display().to_string())
.unwrap_or_else(|_| "<unknown>".to_string());
println!("[TextureSystem] read failed for '{uri}': {e}");
println!("[TextureSystem] cwd = {cwd}");
println!("[TextureSystem] resolved: {}", path.display());
let _ = self.pending_attach.remove(&renderable_cid);
continue;
}
};
let handle = match record.format {
CatEngineTextureFormat::DdsBc7 => match decode_dds_bc7(&bytes) {
Ok(decoded) => match uploader.upload_texture_bc7(
&decoded.bc7_blocks,
decoded.width,
decoded.height,
decoded.srgb,
) {
Ok(h) => h,
Err(e) => {
println!(
"[TextureSystem] BC7 upload failed for '{uri}': {:?}",
e
);
let _ = self.pending_attach.remove(&renderable_cid);
continue;
}
},
Err(e) => {
println!("[TextureSystem] DDS/BC7 decode failed for '{uri}': {e}");
let _ = self.pending_attach.remove(&renderable_cid);
continue;
}
},
CatEngineTextureFormat::Rgba8 => {
let dyn_img = match image::load_from_memory(&bytes) {
Ok(i) => i,
Err(e) => {
println!("[TextureSystem] decode failed for '{uri}': {:?}", e);
let _ = self.pending_attach.remove(&renderable_cid);
continue;
}
};
let rgba = dyn_img.to_rgba8();
let (w, h) = rgba.dimensions();
match uploader.upload_texture_rgba8(rgba.as_raw(), w, h) {
Ok(h) => h,
Err(e) => {
println!("[TextureSystem] upload failed for '{uri}': {:?}", e);
let _ = self.pending_attach.remove(&renderable_cid);
continue;
}
}
}
};
record.gpu = Some(handle);
self.uri_cache.insert(uri.to_string(), handle);
handle
}
};
let _ = visuals.update_texture(instance_handle, Some(tex_handle));
let _ = self.pending_attach.remove(&renderable_cid);
}
}
}
fn is_virtual_texture_key(uri: &str) -> bool {
!uri.starts_with("file://") && uri.contains(':')
}
struct Bc7Decoded {
width: u32,
height: u32,
srgb: bool,
bc7_blocks: Vec<u8>,
}
fn decode_dds_bc7(bytes: &[u8]) -> Result<Bc7Decoded, String> {
let mut cursor = Cursor::new(bytes);
let dds = ddsfile::Dds::read(&mut cursor).map_err(|e| format!("{e:?}"))?;
let width = dds.get_width();
let height = dds.get_height();
if width == 0 || height == 0 {
return Err("DDS has zero size".to_string());
}
let dxgi = dds
.get_dxgi_format()
.ok_or_else(|| "DDS missing DXGI format (need BC7 in DX10 header)".to_string())?;
let srgb = match dxgi {
ddsfile::DxgiFormat::BC7_UNorm => false,
ddsfile::DxgiFormat::BC7_UNorm_sRGB => true,
other => {
return Err(format!("DDS is not BC7 (got {other:?})"));
}
};
let data: &[u8] = dds.data.as_ref();
if data.is_empty() {
return Err("DDS contains no data".to_string());
}
let blocks_w = (width + 3) / 4;
let blocks_h = (height + 3) / 4;
let expected_len = blocks_w as usize * blocks_h as usize * 16;
if data.len() < expected_len {
return Err(format!(
"DDS data too small for BC7 level 0: got={}, need={}",
data.len(),
expected_len
));
}
Ok(Bc7Decoded {
width,
height,
srgb,
bc7_blocks: data[..expected_len].to_vec(),
})
}