Skip to main content

ferrum_interfaces/vnext/resource/
static_lease.rs

1use super::{
2    expected_lease_transition, invalid_resource, AdmissionFitPolicy, AdmissionPressureAction, Arc,
3    BTreeSet, BufferDescriptor, CoreOwnedAllocation, DeviceRuntime, DynamicResourceShape,
4    ResourceAllocation, ResourceId, ResourceLeaseAction, ResourceLeaseEntry, ResourceLeaseState,
5    ResourceOwnedBuffer, ResourceReservation, ResourceReservationBatch,
6    ResourceTransactionIdentity, ResourceWorkShape, StaticProvisioningBinding, VNextError,
7};
8
9pub(super) struct OwnedLeaseSlot<B> {
10    pub(super) entry: ResourceLeaseEntry,
11    pub(super) actual_resource_id: Option<ResourceId>,
12    pub(super) actual_generation: Option<u64>,
13    pub(super) descriptor: Option<BufferDescriptor>,
14    pub(super) buffer: Option<B>,
15}
16
17impl<B> OwnedLeaseSlot<B> {
18    pub(super) fn new(reservation: &ResourceReservation) -> Self {
19        Self {
20            entry: ResourceLeaseEntry::from_reservation(reservation, ResourceLeaseState::Active),
21            actual_resource_id: None,
22            actual_generation: None,
23            descriptor: None,
24            buffer: None,
25        }
26    }
27
28    pub(super) fn install(&mut self, allocation: CoreOwnedAllocation<B>) {
29        self.actual_resource_id = Some(allocation.resource_id);
30        self.actual_generation = Some(allocation.generation);
31        self.descriptor = Some(allocation.descriptor);
32        self.buffer = Some(allocation.buffer);
33    }
34
35    pub(super) fn clear(&mut self) {
36        drop(self.buffer.take());
37        self.descriptor.take();
38        self.actual_resource_id.take();
39        self.actual_generation.take();
40    }
41
42    pub(super) fn take_allocation(&mut self) -> Option<CoreOwnedAllocation<B>> {
43        Some(CoreOwnedAllocation {
44            resource_id: self.actual_resource_id.take()?,
45            generation: self.actual_generation.take()?,
46            descriptor: self.descriptor.take()?,
47            buffer: self.buffer.take()?,
48        })
49    }
50
51    pub(super) fn restore_allocation(&mut self, allocation: CoreOwnedAllocation<B>) {
52        debug_assert!(self.buffer.is_none());
53        self.install(allocation);
54    }
55}
56
57/// Borrowed access to a live, active, generation-bound committed buffer.
58pub struct LeasedBufferView<'a, B> {
59    pub(super) identity: &'a ResourceTransactionIdentity,
60    pub(super) admission: &'a StaticProvisioningBinding,
61    pub(super) resource_id: &'a ResourceId,
62    pub(super) generation: u64,
63    pub(super) descriptor: &'a BufferDescriptor,
64    pub(super) buffer: &'a B,
65}
66
67impl<'a, B> LeasedBufferView<'a, B> {
68    pub fn identity(&self) -> &ResourceTransactionIdentity {
69        self.identity
70    }
71
72    pub fn admission(&self) -> &StaticProvisioningBinding {
73        self.admission
74    }
75
76    pub fn resource_id(&self) -> &ResourceId {
77        self.resource_id
78    }
79
80    pub const fn generation(&self) -> u64 {
81        self.generation
82    }
83
84    pub fn committed_descriptor(&self) -> &BufferDescriptor {
85        self.descriptor
86    }
87
88    pub fn buffer(&self) -> &B {
89        self.buffer
90    }
91}
92
93#[must_use = "a resource lease is the batch owner of committed buffers"]
94pub struct StaticProvisioningLease<R>
95where
96    R: DeviceRuntime,
97{
98    pub(super) slots: Vec<OwnedLeaseSlot<R::Buffer>>,
99    pub(super) identity: ResourceTransactionIdentity,
100    pub(super) admission: StaticProvisioningBinding,
101    // Backend context drops after all static and dynamic buffers.
102    pub(super) runtime: Arc<R>,
103}
104
105impl<R> StaticProvisioningLease<R>
106where
107    R: DeviceRuntime,
108{
109    pub(crate) fn runtime(&self) -> &Arc<R> {
110        &self.runtime
111    }
112
113    pub(super) fn new(
114        runtime: Arc<R>,
115        identity: &ResourceTransactionIdentity,
116        admission: &StaticProvisioningBinding,
117        reservations: &ResourceReservationBatch,
118    ) -> Self {
119        Self {
120            slots: reservations
121                .reservations()
122                .iter()
123                .map(OwnedLeaseSlot::new)
124                .collect(),
125            identity: identity.clone(),
126            admission: admission.clone(),
127            runtime,
128        }
129    }
130
131    pub fn identity(&self) -> &ResourceTransactionIdentity {
132        &self.identity
133    }
134
135    pub fn admission(&self) -> &StaticProvisioningBinding {
136        &self.admission
137    }
138
139    pub fn state(&self) -> ResourceLeaseState {
140        let mut states = self
141            .slots
142            .iter()
143            .filter(|slot| slot.buffer.is_some())
144            .map(|slot| slot.entry.state);
145        let Some(first) = states.next() else {
146            return ResourceLeaseState::Cancelled;
147        };
148        if states.all(|state| state == first) {
149            first
150        } else {
151            ResourceLeaseState::Mixed
152        }
153    }
154
155    pub fn entries(&self) -> impl Iterator<Item = &ResourceLeaseEntry> {
156        self.slots
157            .iter()
158            .filter(|slot| slot.buffer.is_some())
159            .map(|slot| &slot.entry)
160    }
161
162    pub fn plan_static_entries(&self) -> impl Iterator<Item = &ResourceLeaseEntry> {
163        self.slots
164            .iter()
165            .filter(|slot| slot.buffer.is_some())
166            .map(|slot| &slot.entry)
167    }
168
169    pub(crate) fn view(
170        &self,
171        resource_id: &ResourceId,
172        generation: u64,
173    ) -> Result<LeasedBufferView<'_, R::Buffer>, VNextError> {
174        let slot = self
175            .slots
176            .iter()
177            .find(|slot| {
178                slot.entry.resource_id == *resource_id && slot.entry.generation == generation
179            })
180            .ok_or_else(|| invalid_resource("lease does not contain that resource generation"))?;
181        if slot.entry.state != ResourceLeaseState::Active {
182            return Err(VNextError::InvalidLeaseTransition {
183                lease_id: self.identity.transaction_id.to_string(),
184                from: slot.entry.state.as_str(),
185                action: "borrow_live_buffer",
186            });
187        }
188        let descriptor = slot
189            .descriptor
190            .as_ref()
191            .ok_or_else(|| invalid_resource("lease resource is not committed"))?;
192        let buffer = slot
193            .buffer
194            .as_ref()
195            .ok_or_else(|| invalid_resource("lease resource buffer is not live"))?;
196        Ok(LeasedBufferView {
197            identity: &self.identity,
198            admission: &self.admission,
199            resource_id: &slot.entry.resource_id,
200            generation: slot.entry.generation,
201            descriptor,
202            buffer,
203        })
204    }
205
206    /// Borrows the plan-static slot selected by the immutable memory plan.
207    /// The slot index is prepared once from canonical allocation order; live
208    /// generation, descriptor, and buffer ownership are still checked here.
209    pub(crate) fn plan_static_view(
210        &self,
211        slot_index: usize,
212        allocation: &ResourceAllocation,
213    ) -> Result<LeasedBufferView<'_, R::Buffer>, VNextError> {
214        let slot = self
215            .slots
216            .get(slot_index)
217            .ok_or_else(|| invalid_resource("plan-static slot index is out of range"))?;
218        let descriptor = slot
219            .descriptor
220            .as_ref()
221            .ok_or_else(|| invalid_resource("plan-static resource is not committed"))?;
222        let buffer = slot
223            .buffer
224            .as_ref()
225            .ok_or_else(|| invalid_resource("plan-static resource buffer is not live"))?;
226        if slot.entry.resource_id() != allocation.resource_id()
227            || slot.entry.size_bytes() != allocation.size_bytes()
228            || slot.entry.alignment_bytes() != allocation.alignment_bytes()
229            || slot.entry.usage() != allocation.usage()
230            || slot.entry.element_type() != allocation.element_type()
231            || slot.entry.generation() == 0
232            || slot.entry.state() != ResourceLeaseState::Active
233            || slot.actual_resource_id.as_ref() != Some(allocation.resource_id())
234            || slot.actual_generation != Some(slot.entry.generation())
235            || descriptor.resource_id != *allocation.resource_id()
236            || descriptor.size_bytes != allocation.size_bytes()
237            || descriptor.alignment_bytes != allocation.alignment_bytes()
238            || descriptor.usage != allocation.usage()
239            || descriptor.element_type != allocation.element_type()
240        {
241            return Err(invalid_resource(
242                "plan-static slot differs from its immutable allocation",
243            ));
244        }
245        Ok(LeasedBufferView {
246            identity: &self.identity,
247            admission: &self.admission,
248            resource_id: &slot.entry.resource_id,
249            generation: slot.entry.generation,
250            descriptor,
251            buffer,
252        })
253    }
254
255    pub(super) fn buffer(&self, order: usize) -> Option<&R::Buffer> {
256        self.slots.get(order).and_then(|slot| slot.buffer.as_ref())
257    }
258
259    pub(super) fn install(&mut self, order: usize, allocation: CoreOwnedAllocation<R::Buffer>) {
260        self.slots[order].install(allocation);
261    }
262
263    pub(super) fn clear(&mut self, order: usize) {
264        self.slots[order].clear();
265    }
266
267    pub(super) fn transition_subset(
268        &mut self,
269        orders: &[usize],
270        action: ResourceLeaseAction,
271    ) -> Result<
272        (
273            ResourceLeaseState,
274            ResourceLeaseState,
275            Vec<ResourceLeaseEntry>,
276        ),
277        VNextError,
278    > {
279        if orders.is_empty() {
280            return Err(invalid_resource(
281                "lease transition subset must not be empty",
282            ));
283        }
284        let mut unique = BTreeSet::new();
285        let mut common_before = None;
286        for &order in orders {
287            if !unique.insert(order) {
288                return Err(invalid_resource("lease transition subset is duplicated"));
289            }
290            let slot = self
291                .slots
292                .get(order)
293                .ok_or_else(|| invalid_resource("lease transition order is out of bounds"))?;
294            if slot.buffer.is_none() {
295                return Err(invalid_resource(
296                    "lease transition targets a non-live buffer",
297                ));
298            }
299            let before = slot.entry.state;
300            if common_before.is_some_and(|common| common != before) {
301                return Err(invalid_resource(
302                    "one lease receipt cannot hide heterogeneous before states",
303                ));
304            }
305            if expected_lease_transition(action, before).is_none() {
306                return Err(VNextError::InvalidLeaseTransition {
307                    lease_id: self.identity.transaction_id.to_string(),
308                    from: before.as_str(),
309                    action: action.as_str(),
310                });
311            }
312            common_before = Some(before);
313        }
314        let before = common_before.expect("non-empty subset has a before state");
315        let after = expected_lease_transition(action, before)
316            .expect("lease subset was preflight validated");
317        for &order in orders {
318            self.slots[order].entry.state = after;
319        }
320        Ok((
321            before,
322            after,
323            orders
324                .iter()
325                .map(|&order| self.slots[order].entry.clone())
326                .collect(),
327        ))
328    }
329
330    pub(super) fn take_owned_buffers(
331        &mut self,
332        reservations: &ResourceReservationBatch,
333    ) -> Vec<ResourceOwnedBuffer<R::Buffer>> {
334        self.slots
335            .iter_mut()
336            .zip(reservations.reservations())
337            .enumerate()
338            .filter_map(|(order, (slot, reservation))| {
339                let allocation = slot.take_allocation()?;
340                Some(ResourceOwnedBuffer {
341                    order,
342                    expected_resource_id: reservation.resource_id.clone(),
343                    actual_resource_id: allocation.resource_id,
344                    expected_generation: reservation.generation,
345                    actual_generation: allocation.generation,
346                    expected_descriptor: BufferDescriptor {
347                        resource_id: reservation.resource_id.clone(),
348                        size_bytes: reservation.size_bytes,
349                        alignment_bytes: reservation.alignment_bytes,
350                        usage: reservation.usage,
351                        element_type: reservation.element_type,
352                    },
353                    actual_descriptor: allocation.descriptor,
354                    buffer: allocation.buffer,
355                })
356            })
357            .collect()
358    }
359
360    pub(super) fn restore_owned_buffers(&mut self, buffers: Vec<ResourceOwnedBuffer<R::Buffer>>) {
361        for buffer in buffers {
362            let (order, allocation) = buffer.into_allocation();
363            self.slots[order].restore_allocation(allocation);
364        }
365    }
366}
367
368macro_rules! scoped_resource_admission_request {
369    ($name:ident, $single_sequence:literal) => {
370        #[derive(Debug, Clone, PartialEq, Eq)]
371        pub struct $name {
372            pub(super) work_shape: ResourceWorkShape,
373            pub(super) fit_policy: AdmissionFitPolicy,
374            pub(super) pressure_action: AdmissionPressureAction,
375        }
376
377        impl $name {
378            pub fn new(
379                work_shape: ResourceWorkShape,
380                fit_policy: AdmissionFitPolicy,
381                pressure_action: AdmissionPressureAction,
382            ) -> Result<Self, VNextError> {
383                if $single_sequence
384                    && (work_shape.immediate_sequences() != 1 || work_shape.fit_sequences() != 1)
385                {
386                    return Err(invalid_resource(
387                        "sequence resource admission requires a single-sequence shape",
388                    ));
389                }
390                Ok(Self {
391                    work_shape,
392                    fit_policy,
393                    pressure_action,
394                })
395            }
396
397            pub fn work_shape(&self) -> &ResourceWorkShape {
398                &self.work_shape
399            }
400
401            pub(crate) const fn immediate_shape(&self) -> DynamicResourceShape {
402                self.work_shape.immediate_shape()
403            }
404
405            pub(crate) const fn fit_shape(&self) -> DynamicResourceShape {
406                match self.fit_policy {
407                    AdmissionFitPolicy::ImmediateOnly => self.work_shape.immediate_shape(),
408                    AdmissionFitPolicy::FullInputMustFit => self.work_shape.fit_shape(),
409                }
410            }
411
412            pub const fn fit_policy(&self) -> AdmissionFitPolicy {
413                self.fit_policy
414            }
415
416            pub const fn pressure_action(&self) -> AdmissionPressureAction {
417                self.pressure_action
418            }
419        }
420    };
421}
422
423scoped_resource_admission_request!(RequestResourceAdmissionRequest, false);
424scoped_resource_admission_request!(SequenceResourceAdmissionRequest, true);