use molgfx_core::{ChunkFootprint, ChunkId};
use thiserror::Error;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ResidencyState {
#[default]
Absent,
Requested,
ReadyCpu,
Uploading,
Resident,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ResidencyTicket {
slot: u32,
generation: u64,
chunk: ChunkId,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ResidencyMachineMetrics {
pub resident_resources: u64,
pub resident_payload_bytes: u64,
pub transitions: u64,
pub capacity_stalls: u64,
}
#[derive(Clone, Copy, Debug, Error, PartialEq, Eq)]
pub enum ResidencyMachineError {
#[error("residency machine capacity is exhausted")]
Capacity,
#[error("invalid residency lifecycle transition")]
InvalidTransition,
}
#[derive(Clone, Copy, Debug)]
struct Entry {
chunk: ChunkId,
footprint: ChunkFootprint,
generation: u64,
state: ResidencyState,
}
impl Entry {
const fn vacant() -> Self {
Self {
chunk: ChunkId::new(0),
footprint: ChunkFootprint::new(0, 0, 0, 0),
generation: 0,
state: ResidencyState::Absent,
}
}
}
#[derive(Debug)]
pub struct ResidencyMachine {
entries: Vec<Entry>,
next_generation: u64,
metrics: ResidencyMachineMetrics,
}
impl ResidencyMachine {
#[must_use]
pub fn new(capacity: usize) -> Self {
Self {
entries: vec![Entry::vacant(); capacity],
next_generation: 1,
metrics: ResidencyMachineMetrics::default(),
}
}
pub fn request(
&mut self,
chunk: ChunkId,
footprint: ChunkFootprint,
) -> Result<ResidencyTicket, ResidencyMachineError> {
let Some((index, entry)) = self
.entries
.iter_mut()
.enumerate()
.find(|(_, entry)| entry.state == ResidencyState::Absent)
else {
self.metrics.capacity_stalls = self.metrics.capacity_stalls.saturating_add(1);
return Err(ResidencyMachineError::Capacity);
};
let generation = self.next_generation;
self.next_generation = self.next_generation.wrapping_add(1).max(1);
*entry = Entry {
chunk,
footprint,
generation,
state: ResidencyState::Requested,
};
self.metrics.transitions = self.metrics.transitions.saturating_add(1);
let slot = u32::try_from(index).map_err(|_| ResidencyMachineError::Capacity)?;
Ok(ResidencyTicket {
slot,
generation,
chunk,
})
}
pub fn ready_cpu(&mut self, ticket: ResidencyTicket) -> Result<(), ResidencyMachineError> {
self.transition(ticket, ResidencyState::Requested, ResidencyState::ReadyCpu)
}
pub fn uploading(&mut self, ticket: ResidencyTicket) -> Result<(), ResidencyMachineError> {
self.transition(ticket, ResidencyState::ReadyCpu, ResidencyState::Uploading)
}
pub fn resident(&mut self, ticket: ResidencyTicket) -> Result<(), ResidencyMachineError> {
self.transition(ticket, ResidencyState::Uploading, ResidencyState::Resident)?;
let resident_bytes = self.entry(ticket)?.footprint.gpu_bytes;
self.metrics.resident_resources = self.metrics.resident_resources.saturating_add(1);
self.metrics.resident_payload_bytes = self
.metrics
.resident_payload_bytes
.saturating_add(resident_bytes);
Ok(())
}
#[must_use]
pub fn state(&self, ticket: ResidencyTicket) -> Option<ResidencyState> {
self.entry(ticket).ok().map(|entry| entry.state)
}
#[must_use]
pub const fn metrics(&self) -> ResidencyMachineMetrics {
self.metrics
}
fn transition(
&mut self,
ticket: ResidencyTicket,
from: ResidencyState,
to: ResidencyState,
) -> Result<(), ResidencyMachineError> {
let entry = self.entry_mut(ticket)?;
if entry.state != from {
return Err(ResidencyMachineError::InvalidTransition);
}
entry.state = to;
self.metrics.transitions = self.metrics.transitions.saturating_add(1);
Ok(())
}
fn entry(&self, ticket: ResidencyTicket) -> Result<&Entry, ResidencyMachineError> {
let Some(entry) = self.entries.get(ticket.slot as usize) else {
return Err(ResidencyMachineError::InvalidTransition);
};
if entry.generation != ticket.generation || entry.chunk != ticket.chunk {
return Err(ResidencyMachineError::InvalidTransition);
}
Ok(entry)
}
fn entry_mut(&mut self, ticket: ResidencyTicket) -> Result<&mut Entry, ResidencyMachineError> {
let Some(entry) = self.entries.get_mut(ticket.slot as usize) else {
return Err(ResidencyMachineError::InvalidTransition);
};
if entry.generation != ticket.generation || entry.chunk != ticket.chunk {
return Err(ResidencyMachineError::InvalidTransition);
}
Ok(entry)
}
}
#[cfg(test)]
#[path = "residency_machine_tests.rs"]
mod tests;