use std::any::TypeId;
use std::collections::VecDeque;
use std::path::Path;
use std::sync::Arc;
use astrelis_core::profiling::profile_function;
use crate::Asset;
use crate::error::AssetError;
use crate::event::{AssetEvent, AssetEventBuffer};
use crate::handle::{Handle, UntypedHandle};
use crate::io::{BytesReader, MemoryReader};
use crate::loader::{AssetLoader, LoaderRegistry};
use crate::source::AssetSource;
use crate::state::AssetState;
use crate::storage::{AssetStorages, Assets};
#[cfg(not(target_arch = "wasm32"))]
use crate::io::FileReader;
struct PendingLoad {
handle: UntypedHandle,
source: AssetSource,
bytes: Option<Vec<u8>>,
extension: Option<String>,
}
pub struct AssetServer {
storages: AssetStorages,
loaders: LoaderRegistry,
events: AssetEventBuffer,
pending: VecDeque<PendingLoad>,
#[cfg(not(target_arch = "wasm32"))]
file_reader: FileReader,
memory_reader: MemoryReader,
#[cfg(feature = "hot-reload")]
watcher: Option<crate::hot_reload::AssetWatcher>,
}
impl AssetServer {
#[cfg(not(target_arch = "wasm32"))]
pub fn new() -> Self {
Self::with_base_path(".")
}
#[cfg(not(target_arch = "wasm32"))]
pub fn with_base_path(base_path: impl AsRef<Path>) -> Self {
Self {
storages: AssetStorages::new(),
loaders: LoaderRegistry::new(),
events: AssetEventBuffer::new(),
pending: VecDeque::new(),
file_reader: FileReader::new(base_path),
memory_reader: MemoryReader::new(),
#[cfg(feature = "hot-reload")]
watcher: None,
}
}
#[cfg(target_arch = "wasm32")]
pub fn new() -> Self {
Self {
storages: AssetStorages::new(),
loaders: LoaderRegistry::new(),
events: AssetEventBuffer::new(),
pending: VecDeque::new(),
memory_reader: MemoryReader::new(),
}
}
pub fn register_loader<L: AssetLoader>(&mut self, loader: L)
where
L::Asset: Asset,
{
self.loaders.register(loader);
}
pub fn add_embedded(&mut self, path: impl AsRef<str>, bytes: &'static [u8]) {
self.memory_reader.insert_static(path, bytes);
}
pub fn load<T: Asset>(&mut self, path: impl AsRef<Path>) -> Handle<T> {
profile_function!();
let source = AssetSource::disk(path.as_ref());
self.load_from_source::<T>(source)
}
pub fn load_from_source<T: Asset>(&mut self, source: AssetSource) -> Handle<T> {
let storage = self.storages.get_or_create::<T>();
if let Some(existing) = storage.find_by_source(&source) {
return existing;
}
let handle = storage.reserve(source.clone());
storage.set_loading(&handle);
let extension = source.extension().map(|s| s.to_string()).or_else(|| {
if let AssetSource::Disk { path, .. } = &source {
path.extension().and_then(|e| e.to_str()).map(String::from)
} else {
None
}
});
self.pending.push_back(PendingLoad {
handle: handle.untyped(),
source,
bytes: None,
extension,
});
handle
}
#[cfg(not(target_arch = "wasm32"))]
pub fn load_sync<T: Asset>(&mut self, path: impl AsRef<Path>) -> Result<Handle<T>, AssetError> {
profile_function!();
let source = AssetSource::disk(path.as_ref());
self.load_from_source_sync::<T>(source)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn load_from_source_sync<T: Asset>(
&mut self,
source: AssetSource,
) -> Result<Handle<T>, AssetError> {
profile_function!();
let storage = self.storages.get_or_create::<T>();
if let Some(existing) = storage.find_by_source(&source)
&& storage.is_ready(&existing)
{
return Ok(existing);
}
let handle = storage.reserve(source.clone());
let bytes = match &source {
AssetSource::Disk { path, .. } => self.file_reader.read_bytes_sync(path)?,
AssetSource::Memory { key } => {
let path = Path::new(key);
futures_lite::future::block_on(self.memory_reader.read_bytes(path))?
}
AssetSource::Bytes { data, .. } => data.to_vec(),
};
let extension = source.extension().map(String::from);
let asset: T = self
.loaders
.load_typed::<T>(&source, &bytes, extension.as_deref())?;
let storage = self.storages.get_or_create::<T>();
storage.set_loaded(&handle, asset);
let version = storage.version(&handle).unwrap_or(1);
self.events.push(AssetEvent::Created {
handle: handle.untyped(),
type_id: TypeId::of::<T>(),
version,
});
Ok(handle)
}
pub fn insert<T: Asset>(&mut self, source: AssetSource, asset: T) -> Handle<T> {
profile_function!();
let storage = self.storages.get_or_create::<T>();
let handle = storage.insert(source, asset);
let version = storage.version(&handle).unwrap_or(1);
self.events.push(AssetEvent::Created {
handle: handle.untyped(),
type_id: TypeId::of::<T>(),
version,
});
handle
}
pub fn get<T: Asset>(&self, handle: &Handle<T>) -> Option<&Arc<T>> {
profile_function!();
self.storages.get::<T>().and_then(|s| s.get(handle))
}
pub fn is_ready<T: Asset>(&self, handle: &Handle<T>) -> bool {
self.storages
.get::<T>()
.map(|s| s.is_ready(handle))
.unwrap_or(false)
}
pub fn is_loading<T: Asset>(&self, handle: &Handle<T>) -> bool {
self.storages
.get::<T>()
.map(|s| s.is_loading(handle))
.unwrap_or(false)
}
pub fn version<T: Asset>(&self, handle: &Handle<T>) -> Option<u32> {
self.storages.get::<T>().and_then(|s| s.version(handle))
}
pub fn remove<T: Asset>(&mut self, handle: &Handle<T>) {
if let Some(storage) = self.storages.get_mut::<T>()
&& storage.remove(handle).is_some()
{
self.events.push(AssetEvent::Removed {
handle_id: handle.id(),
type_id: TypeId::of::<T>(),
});
}
}
pub fn drain_events(&mut self) -> impl Iterator<Item = AssetEvent> + '_ {
self.events.drain()
}
pub fn iter_events(&self) -> impl Iterator<Item = &AssetEvent> {
self.events.iter()
}
#[cfg(not(target_arch = "wasm32"))]
pub fn process_pending(&mut self, max_loads: usize) -> usize {
profile_function!();
let mut processed = 0;
while processed < max_loads {
let Some(pending) = self.pending.pop_front() else {
break;
};
processed += 1;
self.process_single_load(pending);
}
processed
}
#[cfg(not(target_arch = "wasm32"))]
fn process_single_load(&mut self, pending: PendingLoad) {
let bytes = match pending.bytes {
Some(b) => b,
None => {
let result = match &pending.source {
AssetSource::Disk { path, .. } => self.file_reader.read_bytes_sync(path),
AssetSource::Memory { key } => {
let path = Path::new(key);
futures_lite::future::block_on(self.memory_reader.read_bytes(path))
}
AssetSource::Bytes { data, .. } => Ok(data.to_vec()),
};
match result {
Ok(bytes) => bytes,
Err(err) => {
self.events.push(AssetEvent::LoadFailed {
handle: pending.handle,
type_id: pending.handle.type_id(),
error: err.to_string(),
});
return;
}
}
}
};
let result = self
.loaders
.load(&pending.source, &bytes, pending.extension.as_deref());
match result {
Ok(asset) => {
if self.storages.set_loaded_erased(&pending.handle, asset) {
let version = self.storages.version_erased(&pending.handle).unwrap_or(1);
self.events.push(AssetEvent::Created {
handle: pending.handle,
type_id: pending.handle.type_id(),
version,
});
} else {
tracing::error!(
"Failed to store loaded asset for handle {:?}",
pending.handle.id()
);
self.events.push(AssetEvent::LoadFailed {
handle: pending.handle,
type_id: pending.handle.type_id(),
error: "Failed to store loaded asset".to_string(),
});
}
}
Err(err) => {
self.storages.set_failed_erased(
&pending.handle,
AssetError::LoaderError {
path: pending.source.display_path(),
message: err.to_string(),
},
);
self.events.push(AssetEvent::LoadFailed {
handle: pending.handle,
type_id: pending.handle.type_id(),
error: err.to_string(),
});
}
}
}
pub fn pending_count(&self) -> usize {
self.pending.len()
}
pub fn has_pending(&self) -> bool {
!self.pending.is_empty()
}
pub fn assets<T: Asset>(&self) -> Option<&Assets<T>> {
self.storages.get::<T>()
}
pub fn assets_mut<T: Asset>(&mut self) -> &mut Assets<T> {
self.storages.get_or_create::<T>()
}
#[cfg(not(target_arch = "wasm32"))]
pub fn ensure_loaded<T: Asset>(&mut self, handle: &Handle<T>) -> Option<&Arc<T>> {
profile_function!();
if self.is_ready(handle) {
return self.get(handle);
}
let max_iterations = 1000;
for _ in 0..max_iterations {
if self.is_ready(handle) || !self.is_loading(handle) {
break;
}
self.process_pending(1);
}
self.get(handle)
}
pub fn find_by_path<T: Asset>(&self, path: impl AsRef<Path>) -> Option<Handle<T>> {
let source = AssetSource::disk(path);
self.storages.get::<T>()?.find_by_source(&source)
}
pub fn drain_events_for<T: Asset>(&mut self) -> impl Iterator<Item = AssetEvent> + '_ {
let target_type = TypeId::of::<T>();
self.events
.drain()
.filter(move |e| e.type_id() == target_type)
}
pub fn state<T: Asset>(&self, handle: &Handle<T>) -> Option<&AssetState<T>> {
self.storages.get::<T>()?.state(handle)
}
pub fn has_loader_for<T: 'static>(&self, extension: &str) -> bool {
self.loaders.has_loader_for::<T>(extension)
}
pub fn has_loader_for_type<T: 'static>(&self) -> bool {
self.loaders.has_loader_for_type::<T>()
}
#[cfg(feature = "hot-reload")]
pub fn enable_hot_reload(&mut self, watch_dir: impl AsRef<Path>) -> Result<(), String> {
use crate::hot_reload::AssetWatcher;
if self.watcher.is_none() {
self.watcher = Some(AssetWatcher::new().map_err(|e| e.to_string())?);
}
if let Some(watcher) = &mut self.watcher {
watcher
.watch_directory(&watch_dir)
.map_err(|e| e.to_string())?;
tracing::info!(
"Hot reload enabled for directory: {}",
watch_dir.as_ref().display()
);
}
Ok(())
}
#[cfg(feature = "hot-reload")]
pub fn process_hot_reload(&mut self) -> usize {
profile_function!();
let Some(watcher) = &mut self.watcher else {
return 0;
};
let changed_handles = watcher.poll_changes();
if changed_handles.is_empty() {
return 0;
}
tracing::debug!("Hot reload: {} assets changed", changed_handles.len());
let mut reloaded = 0;
for handle in changed_handles {
if let Some(source) = self.storages.find_source(&handle) {
tracing::debug!("Reloading asset from: {:?}", source);
self.pending.push_back(PendingLoad {
handle,
source: source.clone(),
bytes: None,
extension: source.extension().map(String::from),
});
reloaded += 1;
}
}
reloaded
}
#[cfg(feature = "hot-reload")]
pub fn register_hot_reload_path(&mut self, path: impl AsRef<Path>, handle: UntypedHandle) {
if let Some(watcher) = &mut self.watcher {
watcher.register_file(path, handle);
}
}
}
#[cfg(not(target_arch = "wasm32"))]
impl Default for AssetServer {
fn default() -> Self {
Self::new()
}
}
pub struct GpuTaskQueue {
tasks: VecDeque<Box<dyn GpuTask>>,
}
impl Default for GpuTaskQueue {
fn default() -> Self {
Self::new()
}
}
impl GpuTaskQueue {
pub fn new() -> Self {
Self {
tasks: VecDeque::new(),
}
}
pub fn queue(&mut self, task: impl GpuTask + 'static) {
self.tasks.push_back(Box::new(task));
}
pub fn process_all<Ctx>(&mut self, ctx: &Ctx)
where
Ctx: GpuContext,
{
while let Some(task) = self.tasks.pop_front() {
task.execute(ctx);
}
}
pub fn len(&self) -> usize {
self.tasks.len()
}
pub fn is_empty(&self) -> bool {
self.tasks.is_empty()
}
}
pub trait GpuTask: Send + Sync {
fn execute(&self, ctx: &dyn GpuContext);
}
pub trait GpuContext: Send + Sync {
fn create_texture(&self, data: &[u8], width: u32, height: u32, format: TextureFormat) -> u64;
fn create_buffer(&self, data: &[u8], usage: BufferUsage) -> u64;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextureFormat {
Rgba8Unorm,
Rgba8Srgb,
Bgra8Unorm,
Bgra8Srgb,
R8Unorm,
Rg8Unorm,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BufferUsage {
pub vertex: bool,
pub index: bool,
pub uniform: bool,
pub storage: bool,
pub copy_src: bool,
pub copy_dst: bool,
}
impl Default for BufferUsage {
fn default() -> Self {
Self {
vertex: false,
index: false,
uniform: false,
storage: false,
copy_src: false,
copy_dst: true,
}
}
}
impl BufferUsage {
pub fn vertex() -> Self {
Self {
vertex: true,
..Default::default()
}
}
pub fn index() -> Self {
Self {
index: true,
..Default::default()
}
}
pub fn uniform() -> Self {
Self {
uniform: true,
..Default::default()
}
}
pub fn storage() -> Self {
Self {
storage: true,
..Default::default()
}
}
}