1use molgfx_core::{ChunkFootprint, ChunkId};
4use thiserror::Error;
5
6#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
8pub enum ResidencyState {
9 #[default]
11 Absent,
12 Requested,
14 ReadyCpu,
16 Uploading,
18 Resident,
20}
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub struct ResidencyTicket {
25 slot: u32,
26 generation: u64,
27 chunk: ChunkId,
28}
29
30#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
32pub struct ResidencyMachineMetrics {
33 pub resident_resources: u64,
35 pub resident_payload_bytes: u64,
37 pub transitions: u64,
39 pub capacity_stalls: u64,
41}
42
43#[derive(Clone, Copy, Debug, Error, PartialEq, Eq)]
45pub enum ResidencyMachineError {
46 #[error("residency machine capacity is exhausted")]
48 Capacity,
49 #[error("invalid residency lifecycle transition")]
51 InvalidTransition,
52}
53
54#[derive(Clone, Copy, Debug)]
55struct Entry {
56 chunk: ChunkId,
57 footprint: ChunkFootprint,
58 generation: u64,
59 state: ResidencyState,
60}
61
62impl Entry {
63 const fn vacant() -> Self {
64 Self {
65 chunk: ChunkId::new(0),
66 footprint: ChunkFootprint::new(0, 0, 0, 0),
67 generation: 0,
68 state: ResidencyState::Absent,
69 }
70 }
71}
72
73#[derive(Debug)]
75pub struct ResidencyMachine {
76 entries: Vec<Entry>,
77 next_generation: u64,
78 metrics: ResidencyMachineMetrics,
79}
80
81impl ResidencyMachine {
82 #[must_use]
84 pub fn new(capacity: usize) -> Self {
85 Self {
86 entries: vec![Entry::vacant(); capacity],
87 next_generation: 1,
88 metrics: ResidencyMachineMetrics::default(),
89 }
90 }
91
92 pub fn request(
98 &mut self,
99 chunk: ChunkId,
100 footprint: ChunkFootprint,
101 ) -> Result<ResidencyTicket, ResidencyMachineError> {
102 let Some((index, entry)) = self
103 .entries
104 .iter_mut()
105 .enumerate()
106 .find(|(_, entry)| entry.state == ResidencyState::Absent)
107 else {
108 self.metrics.capacity_stalls = self.metrics.capacity_stalls.saturating_add(1);
109 return Err(ResidencyMachineError::Capacity);
110 };
111 let generation = self.next_generation;
112 self.next_generation = self.next_generation.wrapping_add(1).max(1);
113 *entry = Entry {
114 chunk,
115 footprint,
116 generation,
117 state: ResidencyState::Requested,
118 };
119 self.metrics.transitions = self.metrics.transitions.saturating_add(1);
120 let slot = u32::try_from(index).map_err(|_| ResidencyMachineError::Capacity)?;
121 Ok(ResidencyTicket {
122 slot,
123 generation,
124 chunk,
125 })
126 }
127
128 pub fn ready_cpu(&mut self, ticket: ResidencyTicket) -> Result<(), ResidencyMachineError> {
134 self.transition(ticket, ResidencyState::Requested, ResidencyState::ReadyCpu)
135 }
136
137 pub fn uploading(&mut self, ticket: ResidencyTicket) -> Result<(), ResidencyMachineError> {
143 self.transition(ticket, ResidencyState::ReadyCpu, ResidencyState::Uploading)
144 }
145
146 pub fn resident(&mut self, ticket: ResidencyTicket) -> Result<(), ResidencyMachineError> {
152 self.transition(ticket, ResidencyState::Uploading, ResidencyState::Resident)?;
153 let resident_bytes = self.entry(ticket)?.footprint.gpu_bytes;
154 self.metrics.resident_resources = self.metrics.resident_resources.saturating_add(1);
155 self.metrics.resident_payload_bytes = self
156 .metrics
157 .resident_payload_bytes
158 .saturating_add(resident_bytes);
159 Ok(())
160 }
161
162 #[must_use]
164 pub fn state(&self, ticket: ResidencyTicket) -> Option<ResidencyState> {
165 self.entry(ticket).ok().map(|entry| entry.state)
166 }
167
168 #[must_use]
170 pub const fn metrics(&self) -> ResidencyMachineMetrics {
171 self.metrics
172 }
173
174 fn transition(
175 &mut self,
176 ticket: ResidencyTicket,
177 from: ResidencyState,
178 to: ResidencyState,
179 ) -> Result<(), ResidencyMachineError> {
180 let entry = self.entry_mut(ticket)?;
181 if entry.state != from {
182 return Err(ResidencyMachineError::InvalidTransition);
183 }
184 entry.state = to;
185 self.metrics.transitions = self.metrics.transitions.saturating_add(1);
186 Ok(())
187 }
188
189 fn entry(&self, ticket: ResidencyTicket) -> Result<&Entry, ResidencyMachineError> {
190 let Some(entry) = self.entries.get(ticket.slot as usize) else {
191 return Err(ResidencyMachineError::InvalidTransition);
192 };
193 if entry.generation != ticket.generation || entry.chunk != ticket.chunk {
194 return Err(ResidencyMachineError::InvalidTransition);
195 }
196 Ok(entry)
197 }
198
199 fn entry_mut(&mut self, ticket: ResidencyTicket) -> Result<&mut Entry, ResidencyMachineError> {
200 let Some(entry) = self.entries.get_mut(ticket.slot as usize) else {
201 return Err(ResidencyMachineError::InvalidTransition);
202 };
203 if entry.generation != ticket.generation || entry.chunk != ticket.chunk {
204 return Err(ResidencyMachineError::InvalidTransition);
205 }
206 Ok(entry)
207 }
208}
209
210#[cfg(test)]
211#[path = "residency_machine_tests.rs"]
212mod tests;