use crate::{
core::{futures::executor::ThreadPool, instant, visitor::prelude::*},
renderer::TextureUploadSender,
resource::{
model::{Model, ModelData},
texture::{
CompressionOptions, Texture, TextureData, TextureError, TextureMagnificationFilter,
TextureMinificationFilter, TexturePixelKind, TextureState, TextureWrapMode,
},
Resource, ResourceData, ResourceLoadError, ResourceState,
},
sound::buffer::{DataSource, SoundBuffer},
utils::log::{Log, MessageKind},
};
use std::{
borrow::Cow,
ops::{Deref, DerefMut},
path::{Path, PathBuf},
sync::{Arc, Mutex, MutexGuard},
};
pub const DEFAULT_RESOURCE_LIFETIME: f32 = 60.0;
pub struct TimedEntry<T> {
pub value: T,
pub time_to_live: f32,
}
impl<T> Deref for TimedEntry<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.value
}
}
impl<T> DerefMut for TimedEntry<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.value
}
}
impl<T> Default for TimedEntry<T>
where
T: Default,
{
fn default() -> Self {
Self {
value: Default::default(),
time_to_live: DEFAULT_RESOURCE_LIFETIME,
}
}
}
impl<T> Clone for TimedEntry<T>
where
T: Clone,
{
fn clone(&self) -> Self {
Self {
value: self.value.clone(),
time_to_live: self.time_to_live,
}
}
}
impl<T> Visit for TimedEntry<T>
where
T: Default + Visit,
{
fn visit(&mut self, name: &str, visitor: &mut Visitor) -> VisitResult {
visitor.enter_region(name)?;
self.value.visit("Value", visitor)?;
self.time_to_live.visit("TimeToLive", visitor)?;
visitor.leave_region()
}
}
impl ResourceData for Arc<Mutex<SoundBuffer>> {
fn path(&self) -> Cow<Path> {
self.lock()
.unwrap()
.external_data_path()
.map(|p| Cow::Owned(p.to_owned()))
.unwrap_or_else(|| Cow::Owned(Path::new("").to_owned()))
}
}
pub type SharedSoundBuffer = Resource<Arc<Mutex<SoundBuffer>>, ()>;
impl Into<Arc<Mutex<SoundBuffer>>> for SharedSoundBuffer {
fn into(self) -> Arc<Mutex<SoundBuffer>> {
self.data_ref().clone()
}
}
pub struct ResourceManagerState {
textures: Vec<TimedEntry<Texture>>,
models: Vec<TimedEntry<Model>>,
sound_buffers: Vec<TimedEntry<SharedSoundBuffer>>,
textures_import_options: TextureImportOptions,
#[cfg(not(target_arch = "wasm32"))]
thread_pool: ThreadPool,
pub(in crate) upload_sender: Option<TextureUploadSender>,
}
impl Default for ResourceManagerState {
fn default() -> Self {
Self {
textures: Default::default(),
models: Default::default(),
sound_buffers: Default::default(),
textures_import_options: Default::default(),
#[cfg(not(target_arch = "wasm32"))]
thread_pool: ThreadPool::new().unwrap(),
upload_sender: None,
}
}
}
#[derive(Clone)]
pub struct ResourceManager {
state: Option<Arc<Mutex<ResourceManagerState>>>,
}
impl Visit for ResourceManager {
fn visit(&mut self, name: &str, visitor: &mut Visitor) -> VisitResult {
visitor.enter_region(name)?;
self.state.visit("State", visitor)?;
visitor.leave_region()
}
}
#[derive(Clone)]
pub struct TextureImportOptions {
minification_filter: TextureMinificationFilter,
magnification_filter: TextureMagnificationFilter,
s_wrap_mode: TextureWrapMode,
t_wrap_mode: TextureWrapMode,
anisotropy: f32,
compression: CompressionOptions,
}
impl Default for TextureImportOptions {
fn default() -> Self {
Self {
minification_filter: TextureMinificationFilter::LinearMipMapLinear,
magnification_filter: TextureMagnificationFilter::Linear,
s_wrap_mode: TextureWrapMode::Repeat,
t_wrap_mode: TextureWrapMode::Repeat,
anisotropy: 16.0,
compression: CompressionOptions::Quality,
}
}
}
impl TextureImportOptions {
pub fn with_minification_filter(
mut self,
minification_filter: TextureMinificationFilter,
) -> Self {
self.minification_filter = minification_filter;
self
}
pub fn with_magnification_filter(
mut self,
magnification_filter: TextureMagnificationFilter,
) -> Self {
self.magnification_filter = magnification_filter;
self
}
pub fn with_s_wrap_mode(mut self, s_wrap_mode: TextureWrapMode) -> Self {
self.s_wrap_mode = s_wrap_mode;
self
}
pub fn with_t_wrap_mode(mut self, t_wrap_mode: TextureWrapMode) -> Self {
self.t_wrap_mode = t_wrap_mode;
self
}
pub fn with_anisotropy(mut self, anisotropy: f32) -> Self {
self.anisotropy = anisotropy.min(1.0);
self
}
pub fn with_compression(mut self, compression: CompressionOptions) -> Self {
self.compression = compression;
self
}
}
#[derive(Debug)]
pub enum TextureRegistrationError {
Texture(TextureError),
InvalidState,
AlreadyRegistered,
}
impl From<TextureError> for TextureRegistrationError {
fn from(e: TextureError) -> Self {
Self::Texture(e)
}
}
async fn load_texture(
texture: Texture,
path: PathBuf,
options: TextureImportOptions,
upload_sender: TextureUploadSender,
) {
let time = instant::Instant::now();
match TextureData::load_from_file(&path, options.compression).await {
Ok(mut raw_texture) => {
Log::writeln(
MessageKind::Information,
format!("Texture {:?} is loaded in {:?}!", path, time.elapsed()),
);
raw_texture.set_magnification_filter(options.magnification_filter);
raw_texture.set_minification_filter(options.minification_filter);
raw_texture.set_anisotropy_level(options.anisotropy);
raw_texture.set_s_wrap_mode(options.s_wrap_mode);
raw_texture.set_t_wrap_mode(options.t_wrap_mode);
texture.state().commit(ResourceState::Ok(raw_texture));
upload_sender.request_upload(texture);
}
Err(error) => {
Log::writeln(
MessageKind::Error,
format!("Unable to load texture {:?}! Reason {:?}", &path, &error),
);
texture.state().commit(ResourceState::LoadError {
path,
error: Some(Arc::new(error)),
});
}
}
}
async fn load_model(
model: Model,
path: PathBuf,
resource_manager: ResourceManager,
material_search_options: MaterialSearchOptions,
) {
match ModelData::load(&path, resource_manager, material_search_options).await {
Ok(raw_model) => {
Log::writeln(
MessageKind::Information,
format!("Model {:?} is loaded!", path),
);
model.state().commit(ResourceState::Ok(raw_model));
}
Err(error) => {
Log::writeln(
MessageKind::Error,
format!("Unable to load model from {:?}! Reason {:?}", path, error),
);
model.state().commit(ResourceState::LoadError {
path,
error: Some(Arc::new(error)),
});
}
}
}
async fn load_sound_buffer(resource: SharedSoundBuffer, path: PathBuf, stream: bool) {
match DataSource::from_file(&path).await {
Ok(source) => {
let buffer = if stream {
SoundBuffer::new_streaming(source)
} else {
SoundBuffer::new_generic(source)
};
match buffer {
Ok(sound_buffer) => {
Log::writeln(
MessageKind::Information,
format!("Sound buffer {:?} is loaded!", path),
);
resource.state().commit(ResourceState::Ok(sound_buffer));
}
Err(_) => {
Log::writeln(
MessageKind::Error,
format!("Unable to load sound buffer from {:?}!", path),
);
resource.state().commit(ResourceState::LoadError {
path: path.clone(),
error: Some(Arc::new(())),
})
}
}
}
Err(e) => {
Log::writeln(MessageKind::Error, format!("Invalid data source: {:?}", e));
resource.state().commit(ResourceState::LoadError {
path: path.clone(),
error: Some(Arc::new(())),
})
}
}
}
async fn reload_texture(texture: Texture, path: PathBuf, compression: CompressionOptions) {
match TextureData::load_from_file(&path, compression).await {
Ok(data) => {
Log::writeln(
MessageKind::Information,
format!("Texture {:?} successfully reloaded!", path,),
);
texture.state().commit(ResourceState::Ok(data));
}
Err(e) => {
Log::writeln(
MessageKind::Error,
format!("Unable to reload {:?} texture! Reason: {:?}", path, e),
);
texture.state().commit(ResourceState::LoadError {
path,
error: Some(Arc::new(e)),
});
}
};
}
async fn reload_model(
model: Model,
path: PathBuf,
resource_manager: ResourceManager,
material_search_options: MaterialSearchOptions,
) {
match ModelData::load(&path, resource_manager, material_search_options).await {
Ok(data) => {
Log::writeln(
MessageKind::Information,
format!("Model {:?} successfully reloaded!", path,),
);
model.state().commit(ResourceState::Ok(data));
}
Err(e) => {
Log::writeln(
MessageKind::Error,
format!("Unable to reload {:?} model! Reason: {:?}", path, e),
);
model.state().commit(ResourceState::LoadError {
path,
error: Some(Arc::new(e)),
})
}
};
}
async fn reload_sound_buffer(
resource: SharedSoundBuffer,
path: PathBuf,
stream: bool,
inner_buffer: Arc<Mutex<SoundBuffer>>,
) {
if let Ok(data_source) = DataSource::from_file(&path).await {
let new_sound_buffer = match stream {
false => SoundBuffer::raw_generic(data_source),
true => SoundBuffer::raw_streaming(data_source),
};
match new_sound_buffer {
Ok(new_sound_buffer) => {
Log::writeln(
MessageKind::Information,
format!("Sound buffer {:?} successfully reloaded!", path,),
);
*inner_buffer.lock().unwrap() = new_sound_buffer;
resource.state().commit(ResourceState::Ok(inner_buffer));
}
Err(_) => {
Log::writeln(
MessageKind::Error,
format!("Unable to reload {:?} sound buffer!", path),
);
resource.state().commit(ResourceState::LoadError {
path,
error: Some(Arc::new(())),
})
}
}
}
}
impl ResourceManager {
pub(in crate) fn new(upload_sender: TextureUploadSender) -> Self {
Self {
state: Some(Arc::new(Mutex::new(ResourceManagerState::new(
upload_sender,
)))),
}
}
pub fn state(&self) -> MutexGuard<'_, ResourceManagerState> {
self.state.as_ref().unwrap().lock().unwrap()
}
pub fn request_texture<P: AsRef<Path>>(&self, path: P) -> Texture {
let mut state = self.state();
if let Some(texture) = state.find_texture(path.as_ref()) {
return texture;
}
let texture = Texture::new(ResourceState::new_pending(path.as_ref().to_owned()));
state.textures.push(TimedEntry {
value: texture.clone(),
time_to_live: DEFAULT_RESOURCE_LIFETIME,
});
let result = texture.clone();
let options = state.textures_import_options.clone();
let path = path.as_ref().to_owned();
let upload_sender = state
.upload_sender
.as_ref()
.expect("Upload sender must be set!")
.clone();
#[cfg(target_arch = "wasm32")]
crate::core::wasm_bindgen_futures::spawn_local(async move {
load_texture(texture, path, options, upload_sender).await;
});
#[cfg(not(target_arch = "wasm32"))]
state.thread_pool.spawn_ok(async move {
load_texture(texture, path, options, upload_sender).await;
});
result
}
pub fn register_texture<P: AsRef<Path>>(
&self,
texture: Texture,
path: P,
) -> Result<(), TextureRegistrationError> {
let mut state = self.state();
if state.find_texture(path.as_ref()).is_some() {
Err(TextureRegistrationError::AlreadyRegistered)
} else {
let mut texture_state = texture.state();
match &mut *texture_state {
TextureState::Ok(texture_data) => {
texture_data.set_path(path);
if let Err(e) = texture_data.save() {
Err(TextureRegistrationError::Texture(e))
} else {
std::mem::drop(texture_state);
state.textures.push(TimedEntry {
value: texture,
time_to_live: DEFAULT_RESOURCE_LIFETIME,
});
Ok(())
}
}
_ => Err(TextureRegistrationError::InvalidState),
}
}
}
pub fn request_model<P: AsRef<Path>>(
&self,
path: P,
material_search_options: MaterialSearchOptions,
) -> Model {
let mut state = self.state();
if let Some(model) = state.find_model(path.as_ref()) {
return model;
}
let model = Model::new(ResourceState::new_pending(path.as_ref().to_owned()));
state.models.push(TimedEntry {
value: model.clone(),
time_to_live: DEFAULT_RESOURCE_LIFETIME,
});
let result = model.clone();
let path = path.as_ref().to_owned();
let resource_manager = self.clone();
#[cfg(target_arch = "wasm32")]
crate::core::wasm_bindgen_futures::spawn_local(async move {
load_model(model, path, resource_manager, material_search_options).await;
});
#[cfg(not(target_arch = "wasm32"))]
state.thread_pool.spawn_ok(async move {
load_model(model, path, resource_manager, material_search_options).await;
});
result
}
pub fn request_sound_buffer<P: AsRef<Path>>(&self, path: P, stream: bool) -> SharedSoundBuffer {
let mut state = self.state();
if let Some(sound_buffer) = state.find_sound_buffer(path.as_ref()) {
return sound_buffer;
}
let resource = SharedSoundBuffer::new(ResourceState::new_pending(path.as_ref().to_owned()));
state.sound_buffers.push(TimedEntry {
value: resource.clone(),
time_to_live: DEFAULT_RESOURCE_LIFETIME,
});
let result = resource.clone();
let path = path.as_ref().to_owned();
#[cfg(target_arch = "wasm32")]
crate::core::wasm_bindgen_futures::spawn_local(async move {
load_sound_buffer(resource, path, stream).await;
});
#[cfg(not(target_arch = "wasm32"))]
state.thread_pool.spawn_ok(async move {
load_sound_buffer(resource, path, stream).await;
});
result
}
pub async fn reload_textures(&self) {
let textures = {
let state = self.state();
let textures = state
.textures
.iter()
.map(|e| e.value.clone())
.collect::<Vec<Texture>>();
for resource in textures.iter().cloned() {
let path = resource.state().path().to_path_buf();
let compression = if let ResourceState::Ok(ref data) = *resource.state() {
match data.pixel_kind() {
TexturePixelKind::DXT1RGB => CompressionOptions::Speed,
TexturePixelKind::DXT1RGBA => CompressionOptions::Speed,
TexturePixelKind::DXT3RGBA => CompressionOptions::NoCompression, TexturePixelKind::DXT5RGBA => CompressionOptions::Quality,
_ => CompressionOptions::NoCompression,
}
} else {
CompressionOptions::NoCompression
};
*resource.state() = ResourceState::new_pending(path.clone());
#[cfg(target_arch = "wasm32")]
crate::core::wasm_bindgen_futures::spawn_local(async move {
reload_texture(resource, path, compression).await;
});
#[cfg(not(target_arch = "wasm32"))]
state.thread_pool.spawn_ok(async move {
reload_texture(resource, path, compression).await;
});
}
textures
};
crate::core::futures::future::join_all(textures).await;
}
pub async fn reload_models(&self) {
let models = {
let this = self.clone();
let state = self.state();
let models = state
.models
.iter()
.map(|m| m.value.clone())
.collect::<Vec<Model>>();
for model in models.iter().cloned() {
let this = this.clone();
let path = model.state().path().to_path_buf();
let material_search_options = model.data_ref().material_search_options().clone();
*model.state() = ResourceState::new_pending(path.clone());
#[cfg(target_arch = "wasm32")]
crate::core::wasm_bindgen_futures::spawn_local(async move {
reload_model(model, path, this, material_search_options).await;
});
#[cfg(not(target_arch = "wasm32"))]
state.thread_pool.spawn_ok(async move {
reload_model(model, path, this, material_search_options).await;
})
}
models
};
crate::core::futures::future::join_all(models).await;
Log::writeln(
MessageKind::Information,
"All model resources reloaded!".to_owned(),
);
}
pub async fn reload_sound_buffers(&self) {
let buffers = {
let state = self.state();
let sound_buffers = state
.sound_buffers
.iter()
.map(|b| b.value.clone())
.collect::<Vec<SharedSoundBuffer>>();
for resource in sound_buffers.iter().cloned() {
let (stream, path, inner_buffer) = {
let inner_buffer_ref = resource.data_ref();
let inner_buffer = inner_buffer_ref.lock().unwrap();
let stream = match *inner_buffer {
SoundBuffer::Generic(_) => false,
SoundBuffer::Streaming(_) => true,
};
(
stream,
inner_buffer.external_data_path().map(|p| p.to_owned()),
inner_buffer_ref.clone(),
)
};
if let Some(ext_path) = path {
*resource.state() = ResourceState::new_pending(ext_path.clone());
#[cfg(target_arch = "wasm32")]
crate::core::wasm_bindgen_futures::spawn_local(async move {
reload_sound_buffer(resource, ext_path, stream, inner_buffer).await;
});
#[cfg(not(target_arch = "wasm32"))]
state.thread_pool.spawn_ok(async move {
reload_sound_buffer(resource, ext_path, stream, inner_buffer).await;
});
}
}
sound_buffers
};
crate::core::futures::future::join_all(buffers).await;
}
pub async fn reload_resources(&self) {
crate::core::futures::join!(
self.reload_textures(),
self.reload_models(),
self.reload_sound_buffers()
);
}
}
fn count_pending_resources<T, E>(resources: &[TimedEntry<Resource<T, E>>]) -> usize
where
T: ResourceData,
E: ResourceLoadError,
{
let mut count = 0;
for entry in resources.iter() {
if let ResourceState::Pending { .. } = *entry.value.state() {
count += 1;
}
}
count
}
fn count_loaded_resources<T, E>(resources: &[TimedEntry<Resource<T, E>>]) -> usize
where
T: ResourceData,
E: ResourceLoadError,
{
let mut count = 0;
for entry in resources.iter() {
match *entry.value.state() {
ResourceState::LoadError { .. } | ResourceState::Ok(_) => {
count += 1;
}
_ => {}
}
}
count
}
#[derive(Clone, Debug, Visit, PartialEq)]
pub enum MaterialSearchOptions {
MaterialsDirectory(PathBuf),
RecursiveUp,
WorkingDirectory,
UsePathDirectly,
}
impl Default for MaterialSearchOptions {
fn default() -> Self {
Self::RecursiveUp
}
}
impl MaterialSearchOptions {
pub fn materials_directory<P: AsRef<Path>>(path: P) -> Self {
Self::MaterialsDirectory(path.as_ref().to_path_buf())
}
}
impl ResourceManagerState {
pub(in crate::engine) fn new(upload_sender: TextureUploadSender) -> Self {
Self {
textures: Vec::new(),
models: Vec::new(),
sound_buffers: Vec::new(),
textures_import_options: Default::default(),
#[cfg(not(target_arch = "wasm32"))]
thread_pool: ThreadPool::new().unwrap(),
upload_sender: Some(upload_sender),
}
}
pub fn set_textures_import_options(&mut self, options: TextureImportOptions) {
self.textures_import_options = options;
}
#[inline]
pub fn textures(&self) -> &[TimedEntry<Texture>] {
&self.textures
}
pub fn find_texture<P: AsRef<Path>>(&self, path: P) -> Option<Texture> {
for texture_entry in self.textures.iter() {
if texture_entry.state().path() == path.as_ref() {
return Some(texture_entry.value.clone());
}
}
None
}
#[inline]
pub fn models(&self) -> &[TimedEntry<Model>] {
&self.models
}
pub fn find_model<P: AsRef<Path>>(&self, path: P) -> Option<Model> {
for model in self.models.iter() {
if model.state().path() == path.as_ref() {
return Some(model.value.clone());
}
}
None
}
#[inline]
pub fn sound_buffers(&self) -> &[TimedEntry<SharedSoundBuffer>] {
&self.sound_buffers
}
pub fn find_sound_buffer<P: AsRef<Path>>(&self, path: P) -> Option<SharedSoundBuffer> {
for sound_buffer in self.sound_buffers.iter() {
if sound_buffer.state().path() == path.as_ref() {
return Some(sound_buffer.value.clone());
}
}
None
}
pub fn count_pending_textures(&self) -> usize {
count_pending_resources(&self.textures)
}
pub fn count_loaded_textures(&self) -> usize {
count_loaded_resources(&self.textures)
}
pub fn count_pending_sound_buffers(&self) -> usize {
count_pending_resources(&self.sound_buffers)
}
pub fn count_loaded_sound_buffers(&self) -> usize {
count_loaded_resources(&self.sound_buffers)
}
pub fn count_pending_models(&self) -> usize {
count_pending_resources(&self.models)
}
pub fn count_loaded_models(&self) -> usize {
count_loaded_resources(&self.models)
}
pub fn count_pending_resources(&self) -> usize {
self.count_pending_textures()
+ self.count_pending_sound_buffers()
+ self.count_pending_models()
}
pub fn count_loaded_resources(&self) -> usize {
self.count_loaded_textures()
+ self.count_loaded_sound_buffers()
+ self.count_loaded_models()
}
pub fn count_registered_resources(&self) -> usize {
self.textures.len() + self.sound_buffers.len() + self.models.len()
}
pub fn loading_progress(&self) -> usize {
let registered = self.count_registered_resources();
if registered > 0 {
self.count_loaded_resources() * 100 / registered
} else {
100
}
}
pub fn purge_unused_resources(&mut self) {
self.sound_buffers
.retain(|buffer| buffer.value.use_count() > 1);
self.models.retain(|buffer| buffer.value.use_count() > 1);
self.textures.retain(|buffer| buffer.value.use_count() > 1);
}
fn update_textures(&mut self, dt: f32) {
for texture in self.textures.iter_mut() {
if matches!(*texture.state(), ResourceState::Ok(_)) {
texture.time_to_live -= dt;
if texture.use_count() > 1 {
texture.time_to_live = DEFAULT_RESOURCE_LIFETIME;
}
}
}
self.textures.retain(|texture| {
let retain = texture.time_to_live > 0.0;
if !retain && texture.state().path().exists() {
Log::writeln(
MessageKind::Information,
format!(
"Texture resource {:?} destroyed because it not used anymore!",
texture.state().path()
),
);
}
retain
});
}
fn update_model(&mut self, dt: f32) {
for model in self.models.iter_mut() {
model.time_to_live -= dt;
if model.use_count() > 1 {
model.time_to_live = DEFAULT_RESOURCE_LIFETIME;
}
}
self.models.retain(|model| {
let retain = model.time_to_live > 0.0;
if !retain && model.state().path().exists() {
Log::writeln(
MessageKind::Information,
format!(
"Model resource {:?} destroyed because it not used anymore!",
model.state().path()
),
);
}
retain
});
}
fn update_sound_buffers(&mut self, dt: f32) {
for buffer in self.sound_buffers.iter_mut() {
buffer.time_to_live -= dt;
if buffer.use_count() > 1 {
buffer.time_to_live = DEFAULT_RESOURCE_LIFETIME;
}
}
self.sound_buffers.retain(|buffer| {
let retain = buffer.time_to_live > 0.0;
if !retain {
Log::writeln(
MessageKind::Information,
format!(
"Sound resource {:?} destroyed because it not used anymore!",
buffer.state().path()
),
);
}
retain
});
}
pub(in crate) fn update(&mut self, dt: f32) {
self.update_textures(dt);
self.update_model(dt);
self.update_sound_buffers(dt);
}
}
impl Visit for ResourceManagerState {
fn visit(&mut self, name: &str, visitor: &mut Visitor) -> VisitResult {
visitor.enter_region(name)?;
crate::core::futures::executor::block_on(crate::core::futures::future::join_all(
self.textures.iter().map(|t| t.value.clone()),
));
crate::core::futures::executor::block_on(crate::core::futures::future::join_all(
self.models.iter().map(|m| m.value.clone()),
));
crate::core::futures::executor::block_on(crate::core::futures::future::join_all(
self.sound_buffers.iter().map(|m| m.value.clone()),
));
self.textures.visit("Textures", visitor)?;
self.models.visit("Models", visitor)?;
self.sound_buffers.visit("SoundBuffers", visitor)?;
visitor.leave_region()
}
}