Skip to main content

dope_core/driver/settings/
mod.rs

1use std::{num, process};
2
3use crate::{
4    driver::route::table,
5    io::{datagram, socket::option, transfer},
6};
7
8mod config;
9
10pub use config::Config;
11
12const MIN_SUBMISSION_CAPACITY: u32 = (option::MAX_STREAM_OPTIONS + 1) as u32;
13const MAX_SUBMISSION_CAPACITY: u32 = 32_768;
14const MAX_COMPLETION_CAPACITY: u32 = 65_536;
15
16/// Backend-neutral intent for making completed work visible to the driver.
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18#[repr(u8)]
19pub enum CompletionProgress {
20    /// Avoid voluntarily deferring backend completion work.
21    Prompt,
22    /// Permit a backend to coalesce completion work until the next driver entry.
23    BatchedWhenSupported,
24}
25
26const _: () = {
27    assert!(std::mem::size_of::<CompletionProgress>() == std::mem::size_of::<bool>());
28    assert!(std::mem::align_of::<CompletionProgress>() == std::mem::align_of::<bool>());
29};
30
31/// Static driver policy with a validated receive-buffer layout.
32///
33/// Invalid layouts cannot become profile constants:
34///
35/// ```compile_fail
36/// use dope_core::driver::settings::{Profile, QueueLayout, Receive};
37///
38/// struct Invalid;
39/// impl Profile for Invalid {
40///     const QUEUES: QueueLayout = QueueLayout::fixed::<64, 128>();
41///     const RECEIVE: Receive = Receive::fixed::<3, 4096>();
42/// }
43///
44/// let _ = Invalid::RECEIVE;
45/// ```
46pub trait Profile: 'static {
47    const QUEUES: QueueLayout;
48    const SCHEDULER: SchedulerLayout = SchedulerLayout::DEFAULT;
49    const COMPLETION_PROGRESS: CompletionProgress = CompletionProgress::BatchedWhenSupported;
50
51    /// Maximum listener-owned accept slots realized by `for_tcp_profile`.
52    const MAX_ACCEPT_SLOTS: u32 = 65_279;
53    /// Descriptor slots available to outbound pools and standalone files.
54    const OUTBOUND_SLOTS: u32 = 256;
55    const RECEIVE: Receive = Receive::DEFAULT;
56}
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59#[repr(transparent)]
60struct SubmissionCapacity(u32);
61
62impl SubmissionCapacity {
63    const fn new(entries: u32) -> Option<Self> {
64        if entries == 0 {
65            return None;
66        }
67        if entries < MIN_SUBMISSION_CAPACITY
68            || !entries.is_power_of_two()
69            || entries > MAX_SUBMISSION_CAPACITY
70        {
71            return None;
72        }
73        Some(Self(entries))
74    }
75
76    const fn get(self) -> u32 {
77        self.0
78    }
79}
80
81#[derive(Clone, Copy, Debug, PartialEq, Eq)]
82pub struct QueueLayout {
83    submissions: SubmissionCapacity,
84    completions: u32,
85}
86
87struct FixedQueueLayout<const SUBMISSIONS: u32, const COMPLETIONS: u32>;
88
89impl<const SUBMISSIONS: u32, const COMPLETIONS: u32> FixedQueueLayout<SUBMISSIONS, COMPLETIONS> {
90    const VALUE: QueueLayout = {
91        assert!(SUBMISSIONS >= MIN_SUBMISSION_CAPACITY);
92        assert!(SUBMISSIONS.is_power_of_two());
93        assert!(SUBMISSIONS <= MAX_SUBMISSION_CAPACITY);
94        assert!(COMPLETIONS.is_power_of_two());
95        assert!(COMPLETIONS <= MAX_COMPLETION_CAPACITY);
96        assert!(COMPLETIONS >= SUBMISSIONS);
97        QueueLayout {
98            submissions: SubmissionCapacity(SUBMISSIONS),
99            completions: COMPLETIONS,
100        }
101    };
102}
103
104impl QueueLayout {
105    pub const MAX: Self = Self::fixed::<MAX_SUBMISSION_CAPACITY, MAX_COMPLETION_CAPACITY>();
106
107    /// Creates a queue layout validated during compilation.
108    ///
109    /// ```compile_fail
110    /// use dope_core::driver::settings::QueueLayout;
111    ///
112    /// let _ = QueueLayout::fixed::<64, 32>();
113    /// ```
114    pub const fn fixed<const SUBMISSIONS: u32, const COMPLETIONS: u32>() -> Self {
115        FixedQueueLayout::<SUBMISSIONS, COMPLETIONS>::VALUE
116    }
117
118    /// Creates power-of-two backend queues. Submissions cover atomic stream
119    /// setup and cap at 32,768; completions cover submissions and cap at 65,536.
120    #[must_use]
121    pub const fn new(submissions: u32, completions: u32) -> Option<Self> {
122        let Some(submissions) = SubmissionCapacity::new(submissions) else {
123            return None;
124        };
125        if completions == 0 {
126            return None;
127        }
128        if !completions.is_power_of_two()
129            || completions > MAX_COMPLETION_CAPACITY
130            || completions < submissions.get()
131        {
132            return None;
133        }
134        Some(Self {
135            submissions,
136            completions,
137        })
138    }
139
140    #[must_use]
141    pub const fn submissions(self) -> u32 {
142        self.submissions.get()
143    }
144
145    #[must_use]
146    pub const fn completions(self) -> u32 {
147        self.completions
148    }
149}
150
151const _: () = {
152    assert!(QueueLayout::new(0, 8).is_none());
153    assert!(QueueLayout::new(MIN_SUBMISSION_CAPACITY / 2, 8).is_none());
154    assert!(QueueLayout::new(64, 32).is_none());
155    assert!(QueueLayout::MAX.submissions() == MAX_SUBMISSION_CAPACITY);
156    assert!(QueueLayout::MAX.completions() == MAX_COMPLETION_CAPACITY);
157    assert!(std::mem::size_of::<QueueLayout>() == std::mem::size_of::<[u32; 2]>());
158};
159
160/// Scheduler-owned index capacity matching ready and timer slot indices.
161#[derive(Clone, Copy, Debug, PartialEq, Eq)]
162#[repr(transparent)]
163pub struct ScheduleCapacity(u32);
164
165impl ScheduleCapacity {
166    pub const ZERO: Self = Self(0);
167    pub const MAX: Self = Self(u32::MAX);
168
169    /// Creates a scheduler capacity represented directly by its internal index.
170    pub const fn fixed<const SLOTS: u32>() -> Self {
171        Self(SLOTS)
172    }
173
174    #[must_use]
175    pub const fn new(slots: usize) -> Option<Self> {
176        if slots <= u32::MAX as usize {
177            Some(Self(slots as u32))
178        } else {
179            None
180        }
181    }
182
183    #[must_use]
184    pub const fn get(self) -> usize {
185        self.0 as usize
186    }
187}
188
189/// Independent ready-slot and timer-cache bounds. Timer registrations beyond
190/// the realized cache remain live in the intrusive overflow.
191#[derive(Clone, Copy, Debug, PartialEq, Eq)]
192pub struct SchedulerLayout {
193    ready: ScheduleCapacity,
194    timer_cache_limit: ScheduleCapacity,
195}
196
197impl SchedulerLayout {
198    pub const DEFAULT: Self = Self::fixed::<1024, 1024>();
199
200    pub const fn fixed<const READY: u32, const TIMER_CACHE: u32>() -> Self {
201        Self {
202            ready: ScheduleCapacity::fixed::<READY>(),
203            timer_cache_limit: ScheduleCapacity::fixed::<TIMER_CACHE>(),
204        }
205    }
206
207    #[must_use]
208    pub const fn new(ready: usize, timer_cache_limit: usize) -> Option<Self> {
209        let Some(ready) = ScheduleCapacity::new(ready) else {
210            return None;
211        };
212        let Some(timer_cache_limit) = ScheduleCapacity::new(timer_cache_limit) else {
213            return None;
214        };
215        Some(Self {
216            ready,
217            timer_cache_limit,
218        })
219    }
220
221    #[must_use]
222    pub const fn ready(self) -> ScheduleCapacity {
223        self.ready
224    }
225
226    #[must_use]
227    pub const fn timer_cache_limit(self) -> ScheduleCapacity {
228        self.timer_cache_limit
229    }
230
231    #[must_use]
232    pub const fn with_ready(mut self, ready: ScheduleCapacity) -> Self {
233        self.ready = ready;
234        self
235    }
236
237    #[must_use]
238    pub const fn with_timer_cache_limit(mut self, timer_cache_limit: ScheduleCapacity) -> Self {
239        self.timer_cache_limit = timer_cache_limit;
240        self
241    }
242}
243
244const _: () = {
245    assert!(ScheduleCapacity::new(0).is_some());
246    assert!(ScheduleCapacity::MAX.get() == u32::MAX as usize);
247    if let Some(overflow) = (u32::MAX as usize).checked_add(1) {
248        assert!(ScheduleCapacity::new(overflow).is_none());
249    }
250    assert!(std::mem::size_of::<ScheduleCapacity>() == std::mem::size_of::<u32>());
251    assert!(std::mem::size_of::<SchedulerLayout>() == std::mem::size_of::<[u32; 2]>());
252};
253
254#[derive(Clone, Copy, Debug, PartialEq, Eq)]
255pub struct Receive {
256    entries: u16,
257    len: u32,
258}
259
260struct FixedReceive<const ENTRIES: u16, const LEN: u32>;
261
262impl<const ENTRIES: u16, const LEN: u32> FixedReceive<ENTRIES, LEN> {
263    const VALUE: Receive = {
264        assert!(ENTRIES >= MIN_RECEIVE_ENTRIES);
265        assert!(ENTRIES.is_power_of_two());
266        assert!(LEN != 0);
267        assert!(LEN <= transfer::MAX_BYTES as u32);
268        assert!((ENTRIES as usize) <= (isize::MAX as usize) / (LEN as usize));
269        Receive {
270            entries: ENTRIES,
271            len: LEN,
272        }
273    };
274}
275
276const MIN_RECEIVE_ENTRIES: u16 = 2;
277
278const _: () = {
279    assert!(std::mem::size_of::<Receive>() == std::mem::size_of::<u64>());
280};
281
282impl Receive {
283    pub const DEFAULT: Self = Self::fixed::<4096, 4096>();
284    pub const MIN_DATAGRAM_BUFFER_LEN: u32 = datagram::SlotLen::MIN_BYTES;
285
286    /// Creates a receive-buffer layout validated during compilation.
287    ///
288    /// ```compile_fail
289    /// use dope_core::driver::settings::Receive;
290    ///
291    /// let _ = Receive::fixed::<1, 4096>();
292    /// ```
293    pub const fn fixed<const ENTRIES: u16, const LEN: u32>() -> Self {
294        FixedReceive::<ENTRIES, LEN>::VALUE
295    }
296
297    /// Creates a cross-backend layout with at least two power-of-two slots.
298    /// Slot length must fit transfer fields and total allocation in `isize`.
299    #[must_use]
300    pub const fn new(entries: u16, len: u32) -> Option<Self> {
301        if len == 0 {
302            return None;
303        }
304        if len > transfer::MAX_BYTES as u32 {
305            return None;
306        }
307        Self::with_len(entries, len)
308    }
309
310    /// Creates a layout with the requested cross-backend datagram payload capacity.
311    #[must_use]
312    pub const fn for_datagram_payload(entries: u16, payload_len: u32) -> Option<Self> {
313        let Some(len) = datagram::SlotLen::for_payload(payload_len) else {
314            return None;
315        };
316        Self::with_len(entries, len.nonzero().get())
317    }
318
319    const fn with_len(entries: u16, len: u32) -> Option<Self> {
320        if entries < MIN_RECEIVE_ENTRIES || !entries.is_power_of_two() {
321            return None;
322        }
323        let Some(bytes) = (entries as usize).checked_mul(len as usize) else {
324            return None;
325        };
326        if bytes > isize::MAX as usize {
327            return None;
328        }
329        Some(Self { entries, len })
330    }
331
332    #[must_use]
333    pub const fn entries(self) -> u16 {
334        self.entries
335    }
336
337    #[must_use]
338    pub const fn buffer_len(self) -> usize {
339        self.len as usize
340    }
341
342    pub(crate) fn nonzero_buffer_len(self) -> num::NonZeroU32 {
343        let Some(len) = num::NonZeroU32::new(self.len) else {
344            process::abort();
345        };
346        len
347    }
348
349    pub(crate) const fn backing_bytes(self) -> usize {
350        self.entries as usize * self.len as usize
351    }
352}
353
354/// A fixed-file layout representable by every backend target table.
355#[derive(Clone, Copy, Debug, PartialEq, Eq)]
356pub struct FileSlots {
357    accept_end: table::Capacity,
358    table_end: table::Capacity,
359}
360
361struct FixedFileSlots<const ACCEPT: u32, const OUTBOUND: u32>;
362
363impl<const ACCEPT: u32, const OUTBOUND: u32> FixedFileSlots<ACCEPT, OUTBOUND> {
364    const VALUE: FileSlots = {
365        assert!(ACCEPT <= u32::MAX - OUTBOUND);
366        let total = ACCEPT + OUTBOUND;
367        assert!(total != 0);
368        FileSlots {
369            accept_end: table::Capacity::fixed(ACCEPT),
370            table_end: table::Capacity::fixed(total),
371        }
372    };
373}
374
375const _: () =
376    assert!(std::mem::size_of::<FileSlots>() == std::mem::size_of::<[table::Capacity; 2]>());
377
378impl FileSlots {
379    /// Creates a fixed-file layout validated during compilation.
380    ///
381    /// ```compile_fail
382    /// use dope_core::driver::settings::FileSlots;
383    ///
384    /// let _ = FileSlots::fixed::<16, { u32::MAX }>();
385    /// ```
386    pub const fn fixed<const ACCEPT: u32, const OUTBOUND: u32>() -> Self {
387        FixedFileSlots::<ACCEPT, OUTBOUND>::VALUE
388    }
389
390    /// Creates independent accept and outbound domains in one fixed-file table.
391    #[must_use]
392    pub const fn new(accept: u32, outbound: u32) -> Option<Self> {
393        let Some(total) = accept.checked_add(outbound) else {
394            return None;
395        };
396        if total == 0 {
397            return None;
398        }
399        let Some(accept_end) = table::Capacity::new(accept as usize) else {
400            return None;
401        };
402        let Some(table_end) = table::Capacity::new(total as usize) else {
403            return None;
404        };
405        Some(Self {
406            accept_end,
407            table_end,
408        })
409    }
410
411    #[must_use]
412    pub const fn capacity(self) -> u32 {
413        self.table_end.raw()
414    }
415
416    #[must_use]
417    pub const fn accept(self) -> u32 {
418        self.accept_end.raw()
419    }
420
421    #[must_use]
422    pub const fn outbound(self) -> u32 {
423        self.table_end.raw() - self.accept_end.raw()
424    }
425
426    pub(crate) const fn table_capacity(self) -> table::Capacity {
427        self.table_end
428    }
429
430    pub(crate) const fn accept_capacity(self) -> table::Capacity {
431        self.accept_end
432    }
433}