use super::{
storage_backend::{Query, QueryError, QueryPage},
StorageBackend,
};
#[cfg(debug_assertions)]
use crate::asset::{
asset_handler::LoadErrors,
asset_manager::{AssetManager, LoadMessages},
ResourceTrace,
};
use crate::{asset::ResourceId, online_docs_url, Model, Reference, ReferenceModel, Resource, SerializableResource, Solver};
#[cfg(debug_assertions)]
const BAKING_APP_RESOURCES_DOCS_PATH: &str = "develop/design/resource-management/baking-app-resources";
#[cfg(debug_assertions)]
fn asset_lookup_error(message: &str, id: &str, error: &LoadMessages, asset_manager: &AssetManager) -> String {
let byte_engine_root_inaccessible = matches!(
error,
LoadMessages::FailedToBake {
error: LoadErrors::AssetCouldNotBeLoaded,
..
}
) && (id == "byte-engine" || id.starts_with("byte-engine/"))
&& asset_manager.source_directory_accessible(std::path::Path::new("byte-engine")) == Some(false);
if byte_engine_root_inaccessible {
format!(
"{message} The 'byte-engine' path in the assets directory is inaccessible, so its symlink was probably not configured. See {}.",
online_docs_url(BAKING_APP_RESOURCES_DOCS_PATH)
)
} else {
message.to_string()
}
}
pub struct ResourceManager {
#[cfg(debug_assertions)]
asset_manager: std::sync::OnceLock<AssetManager>,
storage_backend: Box<dyn StorageBackend>,
}
impl ResourceManager {
pub fn new<SB: StorageBackend>(storage_backend: SB) -> Self {
ResourceManager {
#[cfg(debug_assertions)]
asset_manager: std::sync::OnceLock::new(),
storage_backend: Box::new(storage_backend),
}
}
#[cfg(debug_assertions)]
pub fn set_asset_manager(&self, asset_manager: AssetManager) {
assert!(
self.try_set_asset_manager(asset_manager).is_ok(),
"Failed to set up resource manager. The most likely cause is that asset management was installed more than once."
);
}
#[cfg(debug_assertions)]
pub fn try_set_asset_manager(&self, asset_manager: AssetManager) -> Result<(), AssetManager> {
self.asset_manager.set(asset_manager)
}
#[cfg(debug_assertions)]
pub fn resource_trace(&self) -> Option<&ResourceTrace> {
self.asset_manager.get().map(AssetManager::resource_trace)
}
fn get_storage_backend(&self) -> &dyn StorageBackend {
self.storage_backend.as_ref()
}
pub fn request<'s, 'a, 'b, T: Resource + 'a>(&'s self, id: &'b str) -> Result<Reference<T>, String>
where
ReferenceModel<T::Model>: Solver<'a, Reference<T>>,
SerializableResource: TryInto<ReferenceModel<T::Model>>,
{
let storage_backend = self.get_storage_backend();
let reference_model: ReferenceModel<T::Model> = if let Some(result) = storage_backend.read(ResourceId::new(id)) {
let (resource, _) = result;
resource.into()
} else {
#[cfg(debug_assertions)]
{
let Some(asset_manager) = self.asset_manager.get() else {
return Err("Resource does not exist and an asset manager is not available.".to_string());
};
let runtime = compio::runtime::Runtime::new().unwrap();
runtime
.block_on(asset_manager.bake_if_not_exists(id, storage_backend))
.map_err(|error| {
asset_lookup_error(
"Failed to load asset. The asset manager could not bake the resource.",
id,
&error,
asset_manager,
)
})?
}
#[cfg(not(debug_assertions))]
{
return Err("Resource does not exist in the baked release resource store.".to_string());
}
};
let reference: Reference<T> = reference_model
.solve(self.get_storage_backend())
.map_err(|error| Into::<&'static str>::into(error).to_string())?;
Ok(reference)
}
pub fn query<'a, T: Resource + 'a>(&'a self, query: Query) -> Result<QueryPage<Reference<T>>, QueryError>
where
ReferenceModel<T::Model>: Solver<'a, Reference<T>>,
SerializableResource: Into<ReferenceModel<T::Model>>,
{
let page = self.get_storage_backend().query(Query {
class: T::Model::get_class().to_string(),
..query
})?;
Ok(QueryPage {
items: page
.items
.into_iter()
.map(|e| {
let r: ReferenceModel<T::Model> = e.0.into();
let r: Reference<T> = r.solve(self.get_storage_backend()).unwrap();
r
})
.collect(),
cursor: page.cursor,
})
}
}
#[cfg(all(test, debug_assertions))]
mod debug_tests {
use std::{
fs,
sync::Arc,
time::{SystemTime, UNIX_EPOCH},
};
use super::ResourceManager;
use crate::{
asset::{
asset_handler::{AssetHandler, BakeContext, LoadErrors},
asset_manager::AssetManager,
storage_backend::{tests::TestStorageBackend as AssetTestStorageBackend, FileStorageBackend},
ResourceId, ResourceTraceLevel,
},
resource::storage_backend::tests::TestStorageBackend as ResourceTestStorageBackend,
resources::material::Shader,
};
struct ResolvingAssetHandler;
impl AssetHandler for ResolvingAssetHandler {
fn can_handle(&self, extension: &str) -> bool {
extension == "test"
}
async fn bake<'a>(&'a self, context: BakeContext<'a>, id: ResourceId<'a>) -> Result<(), LoadErrors> {
context.resolve(id).await.map(|_| ())
}
}
fn temporary_asset_directory(name: &str) -> std::path::PathBuf {
std::env::temp_dir().join(format!(
"byte-engine-resource-manager-{name}-{}-{}",
std::process::id(),
SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos()
))
}
fn resource_manager_with_file_assets(path: std::path::PathBuf) -> ResourceManager {
let mut asset_manager = AssetManager::new(FileStorageBackend::new(path));
asset_manager.add_asset_handler(ResolvingAssetHandler);
let resource_manager = ResourceManager::new(ResourceTestStorageBackend::new());
resource_manager.set_asset_manager(asset_manager);
resource_manager
}
#[test]
fn asset_management_can_be_installed_after_the_resource_manager_is_shared() {
let resource_manager = Arc::new(ResourceManager::new(ResourceTestStorageBackend::new()));
let renderer_reference = Arc::downgrade(&resource_manager);
resource_manager.set_asset_manager(AssetManager::new(AssetTestStorageBackend::new()));
assert!(renderer_reference.upgrade().is_some());
assert!(resource_manager.resource_trace().is_some());
assert!(resource_manager
.try_set_asset_manager(AssetManager::new(AssetTestStorageBackend::new()))
.is_err());
}
#[test]
fn inaccessible_engine_asset_root_suggests_configuring_the_symlink() {
let assets = temporary_asset_directory("missing-root");
let resource_manager = resource_manager_with_file_assets(assets.clone());
let error = resource_manager.request::<Shader>("byte-engine/missing.test").unwrap_err();
assert!(error.contains("The 'byte-engine' path in the assets directory is inaccessible"));
assert!(error.contains(&super::online_docs_url(super::BAKING_APP_RESOURCES_DOCS_PATH)));
fs::remove_dir_all(assets).unwrap();
}
#[test]
fn individual_asset_failure_omits_engine_symlink_hint_when_root_is_accessible() {
let assets = temporary_asset_directory("accessible-root");
fs::create_dir_all(assets.join("byte-engine")).unwrap();
let resource_manager = resource_manager_with_file_assets(assets.clone());
let error = resource_manager.request::<Shader>("byte-engine/missing.test").unwrap_err();
assert_eq!(error, "Failed to load asset. The asset manager could not bake the resource.");
let trace = resource_manager
.resource_trace()
.expect("installed asset management should expose its trace");
let items = trace.items("byte-engine/missing.test");
assert_eq!(items.len(), 1);
assert_eq!(items[0].level(), ResourceTraceLevel::Error);
fs::remove_dir_all(assets).unwrap();
}
}
#[cfg(all(test, not(debug_assertions)))]
mod release_tests {
use super::ResourceManager;
use crate::{resource::storage_backend::tests::TestStorageBackend, resources::material::Shader};
#[test]
fn missing_release_resource_fails_without_running_asset_processors() {
let resource_manager = ResourceManager::new(TestStorageBackend::new());
let result = resource_manager.request::<Shader>("missing/render-pass.besl");
assert!(
matches!(result, Err(error) if error.starts_with("Resource does not exist in the baked release resource store."))
);
}
}