use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, RwLock};
use std::time::{SystemTime, UNIX_EPOCH};
use std::fs;
use crate::{math::Color, EngineResult};
#[derive(Debug)]
pub struct AssetManager {
texture_cache: Arc<RwLock<HashMap<AssetId, TextureAsset>>>,
audio_cache: Arc<RwLock<HashMap<AssetId, AudioAsset>>>,
font_cache: Arc<RwLock<HashMap<AssetId, FontAsset>>>,
shader_cache: Arc<RwLock<HashMap<AssetId, ShaderAsset>>>,
data_cache: Arc<RwLock<HashMap<AssetId, DataAsset>>>,
asset_paths: HashMap<AssetId, PathBuf>,
asset_metadata: HashMap<AssetId, AssetMetadata>,
next_id: AssetId,
root_path: PathBuf,
hot_reload_enabled: bool,
file_watcher: Option<FileWatcher>,
loaders: HashMap<String, Box<dyn AssetLoader>>,
}
impl AssetManager {
pub fn new<P: AsRef<Path>>(root_path: P) -> EngineResult<Self> {
let mut manager = Self {
texture_cache: Arc::new(RwLock::new(HashMap::new())),
audio_cache: Arc::new(RwLock::new(HashMap::new())),
font_cache: Arc::new(RwLock::new(HashMap::new())),
shader_cache: Arc::new(RwLock::new(HashMap::new())),
data_cache: Arc::new(RwLock::new(HashMap::new())),
asset_paths: HashMap::new(),
asset_metadata: HashMap::new(),
next_id: AssetId(1),
root_path: root_path.as_ref().to_path_buf(),
hot_reload_enabled: false,
file_watcher: None,
loaders: HashMap::new(),
};
manager.register_default_loaders();
Ok(manager)
}
pub fn enable_hot_reload(&mut self) -> EngineResult<()> {
if !self.hot_reload_enabled {
let watcher = FileWatcher::new(&self.root_path)?;
self.file_watcher = Some(watcher);
self.hot_reload_enabled = true;
}
Ok(())
}
pub fn disable_hot_reload(&mut self) {
self.hot_reload_enabled = false;
self.file_watcher = None;
}
pub fn load<P: AsRef<Path>>(&mut self, path: P) -> EngineResult<AssetId> {
let full_path = self.root_path.join(path.as_ref());
let extension = full_path.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("")
.to_lowercase();
let id = self.next_id;
self.next_id.0 += 1;
let metadata = fs::metadata(&full_path)?;
let modified_time = metadata.modified()?
.duration_since(UNIX_EPOCH)?
.as_secs();
let asset_metadata = AssetMetadata {
file_size: metadata.len(),
modified_time,
asset_type: AssetType::from_extension(&extension),
dependencies: Vec::new(),
};
match extension.as_str() {
"png" | "jpg" | "jpeg" | "bmp" | "tga" => {
self.load_texture(id, &full_path)?;
}
"wav" | "mp3" | "ogg" | "flac" => {
self.load_audio(id, &full_path)?;
}
"ttf" | "otf" => {
self.load_font(id, &full_path)?;
}
"wgsl" | "glsl" | "hlsl" => {
self.load_shader(id, &full_path)?;
}
"json" | "toml" | "yaml" | "yml" | "xml" => {
self.load_data(id, &full_path)?;
}
_ => {
return Err(format!("Unsupported asset type: {}", extension).into());
}
}
self.asset_paths.insert(id, full_path);
self.asset_metadata.insert(id, asset_metadata);
if self.hot_reload_enabled {
if let Some(watcher) = &mut self.file_watcher {
watcher.watch_file(&self.asset_paths[&id], id)?;
}
}
Ok(id)
}
pub fn load_with_loader<P: AsRef<Path>>(&mut self, path: P, loader_name: &str) -> EngineResult<AssetId> {
let full_path = self.root_path.join(path.as_ref());
if let Some(loader) = self.loaders.get(loader_name) {
let id = self.next_id;
self.next_id.0 += 1;
let asset_data = loader.load(&full_path)?;
self.store_custom_asset(id, asset_data);
self.asset_paths.insert(id, full_path);
Ok(id)
} else {
Err(format!("Unknown asset loader: {}", loader_name).into())
}
}
pub fn get_texture(&self, id: AssetId) -> Option<TextureAsset> {
self.texture_cache.read().ok()?.get(&id).cloned()
}
pub fn get_audio(&self, id: AssetId) -> Option<AudioAsset> {
self.audio_cache.read().ok()?.get(&id).cloned()
}
pub fn get_font(&self, id: AssetId) -> Option<FontAsset> {
self.font_cache.read().ok()?.get(&id).cloned()
}
pub fn get_shader(&self, id: AssetId) -> Option<ShaderAsset> {
self.shader_cache.read().ok()?.get(&id).cloned()
}
pub fn get_data(&self, id: AssetId) -> Option<DataAsset> {
self.data_cache.read().ok()?.get(&id).cloned()
}
pub fn unload(&mut self, id: AssetId) {
if let Ok(mut cache) = self.texture_cache.write() {
cache.remove(&id);
}
if let Ok(mut cache) = self.audio_cache.write() {
cache.remove(&id);
}
if let Ok(mut cache) = self.font_cache.write() {
cache.remove(&id);
}
if let Ok(mut cache) = self.shader_cache.write() {
cache.remove(&id);
}
if let Ok(mut cache) = self.data_cache.write() {
cache.remove(&id);
}
self.asset_paths.remove(&id);
self.asset_metadata.remove(&id);
if let Some(watcher) = &mut self.file_watcher {
watcher.unwatch_file(id);
}
}
pub fn reload(&mut self, id: AssetId) -> EngineResult<()> {
if let Some(path) = self.asset_paths.get(&id).cloned() {
let extension = path.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("")
.to_lowercase();
match extension.as_str() {
"png" | "jpg" | "jpeg" | "bmp" | "tga" => {
self.load_texture(id, &path)?;
}
"wav" | "mp3" | "ogg" | "flac" => {
self.load_audio(id, &path)?;
}
"ttf" | "otf" => {
self.load_font(id, &path)?;
}
"wgsl" | "glsl" | "hlsl" => {
self.load_shader(id, &path)?;
}
"json" | "toml" | "yaml" | "yml" | "xml" => {
self.load_data(id, &path)?;
}
_ => {}
}
if let Ok(metadata) = fs::metadata(&path) {
if let Ok(modified_time) = metadata.modified() {
if let Some(asset_meta) = self.asset_metadata.get_mut(&id) {
asset_meta.modified_time = modified_time
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
asset_meta.file_size = metadata.len();
}
}
}
}
Ok(())
}
pub fn update(&mut self) -> EngineResult<Vec<AssetId>> {
let mut reloaded_assets = Vec::new();
if self.hot_reload_enabled {
if let Some(watcher) = &mut self.file_watcher {
let changed_files = watcher.get_changed_files();
for asset_id in changed_files {
if let Err(e) = self.reload(asset_id) {
eprintln!("Failed to reload asset {:?}: {}", asset_id, e);
} else {
reloaded_assets.push(asset_id);
}
}
}
}
Ok(reloaded_assets)
}
pub fn get_metadata(&self, id: AssetId) -> Option<&AssetMetadata> {
self.asset_metadata.get(&id)
}
pub fn get_all_assets(&self) -> Vec<AssetId> {
self.asset_paths.keys().cloned().collect()
}
pub fn register_loader(&mut self, extension: String, loader: Box<dyn AssetLoader>) {
self.loaders.insert(extension, loader);
}
pub fn create_bundle(&mut self, assets: Vec<AssetId>) -> AssetBundle {
let total_size = self.calculate_bundle_size(&assets);
AssetBundle {
assets,
metadata: AssetBundleMetadata {
created_time: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
total_size,
},
}
}
pub fn load_bundle(&mut self, bundle: &AssetBundle) -> EngineResult<()> {
for &asset_id in &bundle.assets {
if !self.asset_paths.contains_key(&asset_id) {
return Err(format!("Asset {:?} not found in bundle", asset_id).into());
}
}
Ok(())
}
fn load_texture(&mut self, id: AssetId, path: &Path) -> EngineResult<()> {
let data = fs::read(path)?;
let texture = TextureAsset::from_bytes(&data)?;
if let Ok(mut cache) = self.texture_cache.write() {
cache.insert(id, texture);
}
Ok(())
}
fn load_audio(&mut self, id: AssetId, path: &Path) -> EngineResult<()> {
let data = fs::read(path)?;
let audio = AudioAsset::from_bytes(&data, path)?;
if let Ok(mut cache) = self.audio_cache.write() {
cache.insert(id, audio);
}
Ok(())
}
fn load_font(&mut self, id: AssetId, path: &Path) -> EngineResult<()> {
let data = fs::read(path)?;
let font = FontAsset::from_bytes(&data)?;
if let Ok(mut cache) = self.font_cache.write() {
cache.insert(id, font);
}
Ok(())
}
fn load_shader(&mut self, id: AssetId, path: &Path) -> EngineResult<()> {
let source = fs::read_to_string(path)?;
let shader = ShaderAsset::from_source(&source, path)?;
if let Ok(mut cache) = self.shader_cache.write() {
cache.insert(id, shader);
}
Ok(())
}
fn load_data(&mut self, id: AssetId, path: &Path) -> EngineResult<()> {
let content = fs::read_to_string(path)?;
let data = DataAsset::from_string(&content, path)?;
if let Ok(mut cache) = self.data_cache.write() {
cache.insert(id, data);
}
Ok(())
}
fn store_custom_asset(&mut self, _id: AssetId, _data: Box<dyn std::any::Any>) {
}
fn register_default_loaders(&mut self) {
self.register_loader("png".to_string(), Box::new(ImageLoader));
self.register_loader("jpg".to_string(), Box::new(ImageLoader));
self.register_loader("jpeg".to_string(), Box::new(ImageLoader));
self.register_loader("wav".to_string(), Box::new(AudioLoader));
self.register_loader("mp3".to_string(), Box::new(AudioLoader));
self.register_loader("ogg".to_string(), Box::new(AudioLoader));
self.register_loader("ttf".to_string(), Box::new(FontLoader));
self.register_loader("json".to_string(), Box::new(DataLoader));
}
fn calculate_bundle_size(&self, assets: &[AssetId]) -> u64 {
assets.iter()
.filter_map(|id| self.asset_metadata.get(id))
.map(|meta| meta.file_size)
.sum()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AssetId(pub u32);
#[derive(Debug, Clone)]
pub struct AssetMetadata {
pub file_size: u64,
pub modified_time: u64,
pub asset_type: AssetType,
pub dependencies: Vec<AssetId>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AssetType {
Texture,
Audio,
Font,
Shader,
Data,
Custom(u32),
}
impl AssetType {
pub fn from_extension(ext: &str) -> Self {
match ext {
"png" | "jpg" | "jpeg" | "bmp" | "tga" => Self::Texture,
"wav" | "mp3" | "ogg" | "flac" => Self::Audio,
"ttf" | "otf" => Self::Font,
"wgsl" | "glsl" | "hlsl" => Self::Shader,
"json" | "toml" | "yaml" | "yml" | "xml" => Self::Data,
_ => Self::Custom(0),
}
}
}
#[derive(Debug, Clone)]
pub struct TextureAsset {
pub data: Arc<[u8]>,
pub width: u32,
pub height: u32,
pub format: TextureFormat,
pub mip_levels: u32,
pub sample_count: u32,
}
impl TextureAsset {
pub fn from_bytes(data: &[u8]) -> EngineResult<Self> {
Ok(Self {
data: Arc::from(data),
width: 256,
height: 256,
format: TextureFormat::Rgba8,
mip_levels: 1,
sample_count: 1,
})
}
pub fn create_solid_color(width: u32, height: u32, color: Color) -> Self {
let pixel_count = (width * height) as usize;
let mut data = Vec::with_capacity(pixel_count * 4);
let r = (color.r * 255.0) as u8;
let g = (color.g * 255.0) as u8;
let b = (color.b * 255.0) as u8;
let a = (color.a * 255.0) as u8;
for _ in 0..pixel_count {
data.extend_from_slice(&[r, g, b, a]);
}
Self {
data: Arc::from(data),
width,
height,
format: TextureFormat::Rgba8,
mip_levels: 1,
sample_count: 1,
}
}
pub fn create_checker_pattern(width: u32, height: u32, color1: Color, color2: Color, checker_size: u32) -> Self {
let pixel_count = (width * height) as usize;
let mut data = Vec::with_capacity(pixel_count * 4);
let c1 = [
(color1.r * 255.0) as u8,
(color1.g * 255.0) as u8,
(color1.b * 255.0) as u8,
(color1.a * 255.0) as u8,
];
let c2 = [
(color2.r * 255.0) as u8,
(color2.g * 255.0) as u8,
(color2.b * 255.0) as u8,
(color2.a * 255.0) as u8,
];
for y in 0..height {
for x in 0..width {
let checker_x = x / checker_size;
let checker_y = y / checker_size;
let is_even = (checker_x + checker_y) % 2 == 0;
let color = if is_even { c1 } else { c2 };
data.extend_from_slice(&color);
}
}
Self {
data: Arc::from(data),
width,
height,
format: TextureFormat::Rgba8,
mip_levels: 1,
sample_count: 1,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextureFormat {
R8,
Rg8,
Rgb8,
Rgba8,
R16F,
Rg16F,
Rgb16F,
Rgba16F,
R32F,
Rg32F,
Rgb32F,
Rgba32F,
Depth16,
Depth24,
Depth32F,
}
#[derive(Debug, Clone)]
pub struct AudioAsset {
pub data: Arc<[u8]>,
pub format: AudioFormat,
pub sample_rate: u32,
pub channels: u16,
pub duration: f32,
pub loop_start: Option<u32>,
pub loop_end: Option<u32>,
}
impl AudioAsset {
pub fn from_bytes(data: &[u8], path: &Path) -> EngineResult<Self> {
let extension = path.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("")
.to_lowercase();
let format = match extension.as_str() {
"wav" => AudioFormat::Wav,
"mp3" => AudioFormat::Mp3,
"ogg" => AudioFormat::Ogg,
"flac" => AudioFormat::Flac,
_ => return Err("Unsupported audio format".into()),
};
Ok(Self {
data: Arc::from(data),
format,
sample_rate: 44100,
channels: 2,
duration: 0.0,
loop_start: None,
loop_end: None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AudioFormat {
Wav,
Mp3,
Ogg,
Flac,
}
#[derive(Debug, Clone)]
pub struct FontAsset {
pub data: Arc<[u8]>,
pub family_name: String,
pub style: FontStyle,
pub weight: FontWeight,
pub glyph_cache: Arc<Mutex<HashMap<char, GlyphMetrics>>>,
}
impl FontAsset {
pub fn from_bytes(data: &[u8]) -> EngineResult<Self> {
Ok(Self {
data: Arc::from(data),
family_name: "Unknown".to_string(),
style: FontStyle::Normal,
weight: FontWeight::Normal,
glyph_cache: Arc::new(Mutex::new(HashMap::new())),
})
}
pub fn get_glyph_metrics(&self, character: char, size: f32) -> Option<GlyphMetrics> {
if let Ok(cache) = self.glyph_cache.lock() {
cache.get(&character).cloned()
} else {
None
}
}
pub fn measure_text(&self, text: &str, size: f32) -> TextMetrics {
let mut width = 0.0;
let mut height = size;
for ch in text.chars() {
if let Some(metrics) = self.get_glyph_metrics(ch, size) {
width += metrics.advance_width;
height = height.max(metrics.height);
} else {
width += size * 0.5;
}
}
TextMetrics { width, height }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FontStyle {
Normal,
Italic,
Oblique,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FontWeight {
Thin,
Light,
Normal,
Medium,
Bold,
ExtraBold,
Black,
}
#[derive(Debug, Clone)]
pub struct GlyphMetrics {
pub advance_width: f32,
pub advance_height: f32,
pub width: f32,
pub height: f32,
pub bearing_x: f32,
pub bearing_y: f32,
}
#[derive(Debug, Clone)]
pub struct TextMetrics {
pub width: f32,
pub height: f32,
}
#[derive(Debug, Clone)]
pub struct ShaderAsset {
pub source: String,
pub stage: ShaderStage,
pub entry_point: String,
pub uniforms: Vec<ShaderUniform>,
pub compiled_data: Option<Arc<[u8]>>,
}
impl ShaderAsset {
pub fn from_source(source: &str, path: &Path) -> EngineResult<Self> {
let extension = path.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("")
.to_lowercase();
let stage = match extension.as_str() {
"vert" => ShaderStage::Vertex,
"frag" => ShaderStage::Fragment,
"comp" => ShaderStage::Compute,
_ => ShaderStage::Fragment,
};
let uniforms = Self::parse_uniforms(source);
Ok(Self {
source: source.to_string(),
stage,
entry_point: "main".to_string(),
uniforms,
compiled_data: None,
})
}
fn parse_uniforms(source: &str) -> Vec<ShaderUniform> {
let mut uniforms = Vec::new();
for line in source.lines() {
let trimmed = line.trim();
if trimmed.starts_with("uniform") {
if let Some(uniform) = Self::parse_uniform_line(trimmed) {
uniforms.push(uniform);
}
}
}
uniforms
}
fn parse_uniform_line(line: &str) -> Option<ShaderUniform> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 3 {
let type_name = parts[1];
let var_name = parts[2].trim_end_matches(';');
let uniform_type = match type_name {
"float" => UniformType::Float,
"vec2" => UniformType::Vec2,
"vec3" => UniformType::Vec3,
"vec4" => UniformType::Vec4,
"mat3" => UniformType::Mat3,
"mat4" => UniformType::Mat4,
"sampler2D" => UniformType::Texture2D,
_ => UniformType::Float,
};
Some(ShaderUniform {
name: var_name.to_string(),
uniform_type,
binding: 0,
location: 0,
})
} else {
None
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShaderStage {
Vertex,
Fragment,
Compute,
Geometry,
TessellationControl,
TessellationEvaluation,
}
#[derive(Debug, Clone)]
pub struct ShaderUniform {
pub name: String,
pub uniform_type: UniformType,
pub binding: u32,
pub location: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UniformType {
Float,
Vec2,
Vec3,
Vec4,
Mat3,
Mat4,
Int,
IVec2,
IVec3,
IVec4,
Bool,
Texture2D,
TextureCube,
}
#[derive(Debug, Clone)]
pub struct DataAsset {
pub content: String,
pub format: DataFormat,
pub parsed_data: Option<Arc<dyn std::any::Any + Send + Sync>>,
}
impl DataAsset {
pub fn from_string(content: &str, path: &Path) -> EngineResult<Self> {
let extension = path.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("")
.to_lowercase();
let format = match extension.as_str() {
"json" => DataFormat::Json,
"toml" => DataFormat::Toml,
"yaml" | "yml" => DataFormat::Yaml,
"xml" => DataFormat::Xml,
_ => DataFormat::Text,
};
Ok(Self {
content: content.to_string(),
format,
parsed_data: None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DataFormat {
Json,
Toml,
Yaml,
Xml,
Text,
}
#[derive(Debug, Clone)]
pub struct AssetBundle {
pub assets: Vec<AssetId>,
pub metadata: AssetBundleMetadata,
}
#[derive(Debug, Clone)]
pub struct AssetBundleMetadata {
pub created_time: u64,
pub total_size: u64,
}
#[derive(Debug)]
pub struct FileWatcher {
watched_files: HashMap<AssetId, PathBuf>,
changed_files: Vec<AssetId>,
last_check: SystemTime,
}
impl FileWatcher {
pub fn new(_root_path: &Path) -> EngineResult<Self> {
Ok(Self {
watched_files: HashMap::new(),
changed_files: Vec::new(),
last_check: SystemTime::now(),
})
}
pub fn watch_file(&mut self, path: &Path, asset_id: AssetId) -> EngineResult<()> {
self.watched_files.insert(asset_id, path.to_path_buf());
Ok(())
}
pub fn unwatch_file(&mut self, asset_id: AssetId) {
self.watched_files.remove(&asset_id);
}
pub fn get_changed_files(&mut self) -> Vec<AssetId> {
let now = SystemTime::now();
let mut changed = Vec::new();
for (&asset_id, path) in &self.watched_files {
if let Ok(metadata) = fs::metadata(path) {
if let Ok(modified) = metadata.modified() {
if modified > self.last_check {
changed.push(asset_id);
}
}
}
}
self.last_check = now;
changed
}
}
pub trait AssetLoader: std::fmt::Debug + Send + Sync {
fn load(&self, path: &Path) -> EngineResult<Box<dyn std::any::Any>>;
fn get_supported_extensions(&self) -> Vec<String>;
}
#[derive(Debug)]
pub struct ImageLoader;
impl AssetLoader for ImageLoader {
fn load(&self, path: &Path) -> EngineResult<Box<dyn std::any::Any>> {
let data = fs::read(path)?;
let texture = TextureAsset::from_bytes(&data)?;
Ok(Box::new(texture))
}
fn get_supported_extensions(&self) -> Vec<String> {
vec!["png".to_string(), "jpg".to_string(), "jpeg".to_string(), "bmp".to_string(), "tga".to_string()]
}
}
#[derive(Debug)]
pub struct AudioLoader;
impl AssetLoader for AudioLoader {
fn load(&self, path: &Path) -> EngineResult<Box<dyn std::any::Any>> {
let data = fs::read(path)?;
let audio = AudioAsset::from_bytes(&data, path)?;
Ok(Box::new(audio))
}
fn get_supported_extensions(&self) -> Vec<String> {
vec!["wav".to_string(), "mp3".to_string(), "ogg".to_string(), "flac".to_string()]
}
}
#[derive(Debug)]
pub struct FontLoader;
impl AssetLoader for FontLoader {
fn load(&self, path: &Path) -> EngineResult<Box<dyn std::any::Any>> {
let data = fs::read(path)?;
let font = FontAsset::from_bytes(&data)?;
Ok(Box::new(font))
}
fn get_supported_extensions(&self) -> Vec<String> {
vec!["ttf".to_string(), "otf".to_string()]
}
}
#[derive(Debug)]
pub struct DataLoader;
impl AssetLoader for DataLoader {
fn load(&self, path: &Path) -> EngineResult<Box<dyn std::any::Any>> {
let content = fs::read_to_string(path)?;
let data = DataAsset::from_string(&content, path)?;
Ok(Box::new(data))
}
fn get_supported_extensions(&self) -> Vec<String> {
vec!["json".to_string(), "toml".to_string(), "yaml".to_string(), "yml".to_string(), "xml".to_string()]
}
}
impl Default for AssetManager {
fn default() -> Self {
Self::new("assets").unwrap_or_else(|_| Self {
texture_cache: Arc::new(RwLock::new(HashMap::new())),
audio_cache: Arc::new(RwLock::new(HashMap::new())),
font_cache: Arc::new(RwLock::new(HashMap::new())),
shader_cache: Arc::new(RwLock::new(HashMap::new())),
data_cache: Arc::new(RwLock::new(HashMap::new())),
asset_paths: HashMap::new(),
asset_metadata: HashMap::new(),
next_id: AssetId(1),
root_path: PathBuf::from("assets"),
hot_reload_enabled: false,
file_watcher: None,
loaders: HashMap::new(),
})
}
}