Skip to main content

molgfx_render/
residency.rs

1//! Reusable host workspace for future paged GPU scene residency.
2//!
3//! This composes the backend-neutral allocator, staging ring and command
4//! scratch without coupling them to current render passes. `begin_frame`
5//! resets only logical lengths and budgets; all backing storage remains live.
6
7use crate::ResidencyMachineMetrics;
8use molgfx_gpu::{
9    ArenaError, ArenaMetrics, CommandScratch, CommandScratchMetrics, PagedArena,
10    UploadBackpressure, UploadMetrics, UploadRing, UploadRingConfig,
11};
12use thiserror::Error;
13
14/// Fixed capacities allocated when a residency workspace is created.
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub struct ResidencyConfig {
17    /// GPU arena page size in bytes.
18    pub page_size: u64,
19    /// Number of pages represented by the arena.
20    pub page_count: u32,
21    /// Host upload staging and backpressure limits.
22    pub uploads: UploadRingConfig,
23    /// Maximum lowered commands retained per frame.
24    pub command_capacity: usize,
25    /// Maximum resources tracked by the lifecycle machine.
26    pub machine_capacity: usize,
27}
28
29impl Default for ResidencyConfig {
30    fn default() -> Self {
31        const KIB: usize = 1024;
32        Self {
33            page_size: 64 * 1024,
34            page_count: 256,
35            uploads: UploadRingConfig {
36                capacity_bytes: 256 * KIB,
37                ticket_capacity: 256,
38                epoch_budget_bytes: 256 * KIB,
39                in_flight_budget_bytes: 256 * KIB,
40                alignment: 256,
41            },
42            command_capacity: 1024,
43            machine_capacity: 1024,
44        }
45    }
46}
47
48/// Snapshot of all residency primitive counters.
49#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
50pub struct ResidencyMetrics {
51    /// Page allocation counters.
52    pub arena: ArenaMetrics,
53    /// Upload staging counters.
54    pub uploads: UploadMetrics,
55    /// Reusable command counters.
56    pub commands: CommandScratchMetrics,
57    /// Resource lifecycle gauges and transitions.
58    pub machine: ResidencyMachineMetrics,
59    /// Physical storage and traffic for immutable shared asset payloads.
60    pub immutable_assets: ImmutableArenaMetrics,
61}
62
63/// Physical counters for the immutable shared GPU arena.
64#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
65pub struct ImmutableArenaMetrics {
66    /// Number of live physical buffers backing all immutable assets.
67    pub physical_buffers: u32,
68    /// Bytes reserved by the current physical buffer.
69    pub physical_bytes: u64,
70    /// Page-rounded bytes occupied by live immutable payloads.
71    pub resident_bytes: u64,
72    /// Highest immutable resident footprint observed.
73    pub peak_resident_bytes: u64,
74    /// Payload bytes uploaded from the host.
75    pub uploaded_bytes: u64,
76    /// Host-to-GPU writes issued for immutable payloads.
77    pub writes: u64,
78    /// GPU-to-GPU relocations caused by physical growth.
79    pub relocations: u64,
80    /// Requests rejected at the physical device limit.
81    pub allocation_stalls: u64,
82}
83
84/// Flat counters consumed by profiling and benchmark reports.
85///
86/// Allocation, upload and stall values are cumulative for the engine
87/// lifetime. Resident bytes are the current gauge at snapshot time.
88#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
89pub struct ResidencyCounters {
90    /// Host backing-store allocations owned by residency primitives.
91    pub allocation_events: u64,
92    /// Payload bytes submitted through the upload ring.
93    pub upload_bytes: u64,
94    /// Page-rounded bytes currently reserved in the GPU arena.
95    pub resident_bytes: u64,
96    /// Capacity and budget rejections across every residency primitive.
97    pub stall_events: u64,
98}
99
100impl ResidencyMetrics {
101    /// Flattens the detailed metrics without estimating missing values.
102    #[must_use]
103    pub fn counters(self) -> ResidencyCounters {
104        ResidencyCounters {
105            allocation_events: self
106                .arena
107                .host_allocation_events
108                .saturating_add(self.uploads.host_allocation_events)
109                .saturating_add(self.commands.host_allocation_events),
110            upload_bytes: self.uploads.bytes_submitted,
111            resident_bytes: self.arena.resident_bytes,
112            stall_events: self
113                .arena
114                .allocation_stalls
115                .saturating_add(self.uploads.stall_events)
116                .saturating_add(self.commands.capacity_stalls)
117                .saturating_add(self.machine.capacity_stalls),
118        }
119    }
120}
121
122/// Failure while reserving fixed workspace resources.
123#[derive(Clone, Copy, Debug, Error, PartialEq, Eq)]
124pub enum ResidencyInitError {
125    /// Arena configuration is invalid.
126    #[error(transparent)]
127    Arena(#[from] ArenaError),
128    /// Upload ring configuration is invalid.
129    #[error(transparent)]
130    Uploads(#[from] UploadBackpressure),
131}
132
133/// Fixed-capacity reusable state for residency planning and command lowering.
134#[derive(Debug)]
135pub struct ResidencyWorkspace<C: Copy> {
136    arena: PagedArena,
137    uploads: UploadRing,
138    commands: CommandScratch<C>,
139}
140
141/// Upload storage whose configured capacity is committed only on first use.
142#[derive(Debug)]
143pub(crate) struct LazyUploadRing {
144    config: UploadRingConfig,
145    ring: Option<UploadRing>,
146}
147
148impl LazyUploadRing {
149    pub(crate) fn new(config: UploadRingConfig) -> Result<Self, UploadBackpressure> {
150        config.validate()?;
151        Ok(Self { config, ring: None })
152    }
153
154    pub(crate) fn begin_epoch(&mut self) {
155        if let Some(ring) = &mut self.ring {
156            ring.begin_epoch();
157        }
158    }
159
160    pub(crate) fn ensure(&mut self) -> Result<&mut UploadRing, UploadBackpressure> {
161        if self.ring.is_none() {
162            self.ring = Some(UploadRing::new(self.config)?);
163        }
164        self.ring
165            .as_mut()
166            .ok_or(UploadBackpressure::InvalidConfiguration)
167    }
168
169    pub(crate) const fn get_mut(&mut self) -> Option<&mut UploadRing> {
170        self.ring.as_mut()
171    }
172
173    pub(crate) fn metrics(&self) -> UploadMetrics {
174        self.ring
175            .as_ref()
176            .map_or_else(UploadMetrics::default, UploadRing::metrics)
177    }
178}
179
180impl<C: Copy> ResidencyWorkspace<C> {
181    /// Allocates every host-side backing store before frame processing starts.
182    ///
183    /// # Errors
184    ///
185    /// Returns a typed configuration error for invalid capacities.
186    pub fn new(config: ResidencyConfig) -> Result<Self, ResidencyInitError> {
187        Ok(Self {
188            arena: PagedArena::new(config.page_size, config.page_count)?,
189            uploads: UploadRing::new(config.uploads)?,
190            commands: CommandScratch::new(config.command_capacity),
191        })
192    }
193
194    /// Starts a frame without releasing or growing host storage.
195    pub fn begin_frame(&mut self) {
196        self.uploads.begin_epoch();
197        self.commands.clear();
198    }
199
200    /// Page allocator used to assign future GPU arena ranges.
201    #[must_use]
202    pub const fn arena(&self) -> &PagedArena {
203        &self.arena
204    }
205
206    /// Mutable page allocator used to assign and release ranges.
207    pub const fn arena_mut(&mut self) -> &mut PagedArena {
208        &mut self.arena
209    }
210
211    /// Upload staging ring and lifecycle state.
212    #[must_use]
213    pub const fn uploads(&self) -> &UploadRing {
214        &self.uploads
215    }
216
217    /// Mutable upload ring used during staging and retirement.
218    pub const fn uploads_mut(&mut self) -> &mut UploadRing {
219        &mut self.uploads
220    }
221
222    /// Commands lowered during the current frame.
223    #[must_use]
224    pub const fn commands(&self) -> &CommandScratch<C> {
225        &self.commands
226    }
227
228    /// Mutable reusable command storage.
229    pub const fn commands_mut(&mut self) -> &mut CommandScratch<C> {
230        &mut self.commands
231    }
232
233    /// Current real counters from all three storage primitives.
234    #[must_use]
235    pub fn metrics(&self) -> ResidencyMetrics {
236        ResidencyMetrics {
237            arena: self.arena.metrics(),
238            uploads: self.uploads.metrics(),
239            commands: self.commands.metrics(),
240            machine: ResidencyMachineMetrics::default(),
241            immutable_assets: ImmutableArenaMetrics::default(),
242        }
243    }
244}
245
246#[cfg(test)]
247#[path = "residency_tests.rs"]
248mod tests;