Files
Oxide/engine/src/asset/handle.rs
T
Homer Simpson 9eead719b0 Import Oxide engine (Stages 0–10) under MIT license
Full project snapshot migrated to new Gitea remote without history:
engine, editor, physics, script, examples, tests, docs, and assets.
Relicensed from GPLv3 to MIT and updated repo URLs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 20:41:02 +02:00

200 lines
6.3 KiB
Rust

//! [`Handle`]: a typed, ref-counted reference to a loaded asset.
//!
//! A handle is the unit of *ownership* in the asset system. It is cheap to clone
//! (an `Arc` bump), and the asset behind it lives exactly as long as at least
//! one handle does — drop the last handle and the asset is freed. The
//! [`AssetServer`](super::AssetServer) keeps only a [`Weak`] reference in its
//! dedup cache, so it never keeps an otherwise-unused asset alive.
use std::fmt;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Condvar, Mutex};
/// A process-unique identifier assigned to every asset slot.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AssetId(pub(crate) u64);
impl AssetId {
/// The raw numeric id.
pub fn value(self) -> u64 {
self.0
}
}
/// The lifecycle state of an asset behind a [`Handle`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoadState {
/// A background load is in progress; the value is not ready yet.
Loading,
/// The asset loaded successfully and can be read with [`Handle::get`].
Loaded,
/// Loading failed; see [`Handle::error`] for why.
Failed,
}
/// The interior of an asset slot: its current state and (once ready) the value.
///
/// The value is stored as an `Arc<T>` so it can be cloned out cheaply and so a
/// live reload can swap in fresh contents without disturbing readers that
/// already hold the previous `Arc`.
pub(crate) enum CellState<T> {
Loading,
Loaded(Arc<T>),
Failed(Arc<str>),
}
/// The shared, reference-counted storage for one asset.
///
/// Handles hold an `Arc<AssetCell<T>>`; the server's cache holds a
/// `Weak<dyn Any>` to the same allocation for deduplication only.
pub(crate) struct AssetCell<T> {
id: AssetId,
source: Option<PathBuf>,
state: Mutex<CellState<T>>,
ready: Condvar,
}
impl<T> AssetCell<T> {
pub(crate) fn new_loading(id: AssetId, source: Option<PathBuf>) -> Arc<Self> {
Arc::new(Self {
id,
source,
state: Mutex::new(CellState::Loading),
ready: Condvar::new(),
})
}
pub(crate) fn new_loaded(id: AssetId, source: Option<PathBuf>, value: T) -> Arc<Self> {
Arc::new(Self {
id,
source,
state: Mutex::new(CellState::Loaded(Arc::new(value))),
ready: Condvar::new(),
})
}
pub(crate) fn new_failed(id: AssetId, source: Option<PathBuf>, message: String) -> Arc<Self> {
Arc::new(Self {
id,
source,
state: Mutex::new(CellState::Failed(Arc::from(message))),
ready: Condvar::new(),
})
}
pub(crate) fn set_loaded(&self, value: T) {
*self.state.lock().unwrap() = CellState::Loaded(Arc::new(value));
self.ready.notify_all();
}
pub(crate) fn set_failed(&self, message: String) {
*self.state.lock().unwrap() = CellState::Failed(Arc::from(message));
self.ready.notify_all();
}
}
/// A typed, reference-counted handle to an asset of type `T`.
///
/// Clone it freely to share ownership; the asset is freed when the last handle
/// is dropped. Read the value with [`get`](Self::get) (returns `None` until the
/// asset is loaded) or block for it with [`wait`](Self::wait).
pub struct Handle<T> {
cell: Arc<AssetCell<T>>,
}
impl<T> Handle<T> {
pub(crate) fn from_cell(cell: Arc<AssetCell<T>>) -> Self {
Self { cell }
}
/// This asset's process-unique id.
pub fn id(&self) -> AssetId {
self.cell.id
}
/// The source path the asset was loaded from, if any (in-memory assets added
/// with [`AssetServer::add`](super::AssetServer::add) have none).
pub fn source(&self) -> Option<&Path> {
self.cell.source.as_deref()
}
/// The current lifecycle state.
pub fn state(&self) -> LoadState {
match &*self.cell.state.lock().unwrap() {
CellState::Loading => LoadState::Loading,
CellState::Loaded(_) => LoadState::Loaded,
CellState::Failed(_) => LoadState::Failed,
}
}
/// Whether the asset has finished loading successfully.
pub fn is_loaded(&self) -> bool {
matches!(&*self.cell.state.lock().unwrap(), CellState::Loaded(_))
}
/// The loaded value as a cheap `Arc<T>` clone, or `None` if it is still
/// loading or failed.
pub fn get(&self) -> Option<Arc<T>> {
match &*self.cell.state.lock().unwrap() {
CellState::Loaded(value) => Some(value.clone()),
_ => None,
}
}
/// The error message if loading failed, else `None`.
pub fn error(&self) -> Option<String> {
match &*self.cell.state.lock().unwrap() {
CellState::Failed(message) => Some(message.to_string()),
_ => None,
}
}
/// Blocks until the asset is no longer [`Loading`](LoadState::Loading),
/// returning the value on success or `None` if it failed.
pub fn wait(&self) -> Option<Arc<T>> {
let mut guard = self.cell.state.lock().unwrap();
loop {
match &*guard {
CellState::Loading => guard = self.cell.ready.wait(guard).unwrap(),
CellState::Loaded(value) => return Some(value.clone()),
CellState::Failed(_) => return None,
}
}
}
/// The number of live handles to this asset (including this one). The
/// server holds only a weak reference, so this counts handles alone.
pub fn ref_count(&self) -> usize {
Arc::strong_count(&self.cell)
}
/// Replaces the asset's contents in place; every existing handle observes
/// the new value on its next [`get`](Self::get). Used by live reload.
pub(crate) fn set_loaded(&self, value: T) {
self.cell.set_loaded(value);
}
/// Marks the asset as failed in place.
pub(crate) fn set_failed(&self, message: String) {
self.cell.set_failed(message);
}
}
impl<T> Clone for Handle<T> {
fn clone(&self) -> Self {
Self {
cell: self.cell.clone(),
}
}
}
impl<T> fmt::Debug for Handle<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Handle")
.field("id", &self.cell.id.0)
.field("state", &self.state())
.field("source", &self.cell.source)
.finish()
}
}