Skip to main content

vyre_driver/
bindless_policy.rs

1//! D9 substrate: bindless buffers / textures decision policy.
2//!
3//! When a kernel binds many resources (think 100+ small buffers in a
4//! sparse compute graph), the per-binding setup cost  -  bind group
5//! creation, descriptor set rebinds  -  dominates dispatch latency.
6//! Bindless mode replaces N descriptor entries with one descriptor
7//! array indexed at runtime, eliminating the rebind churn.
8//!
9//! Concrete backends expose bindless access through their own native
10//! resource-indexing primitives. Not every adapter supports it; the
11//! policy here owns the decision given a probed capability + resource
12//! count.
13//!
14//! Pure decision: no Program walk, no descriptor scan. Caller passes
15//! the resource count and the backend's bindless capability bit.
16
17/// Backend support level for bindless resources.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum BindlessSupport {
20    /// Backend has full bindless support: descriptor arrays plus
21    /// dynamic indexing.
22    Full,
23    /// Backend supports descriptor arrays but with a fixed size and no
24    /// runtime indexing of unbound slots. Useful when every slot is
25    /// guaranteed bound; not useful for sparse access.
26    Static,
27    /// Backend has no bindless support. Always use traditional
28    /// per-resource bindings.
29    Unsupported,
30}
31
32/// Inputs to the bindless decision.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct BindlessInputs {
35    /// Number of resources the kernel binds. Below the threshold,
36    /// traditional bindings beat bindless on every backend (the
37    /// per-bindless-handle setup cost has its own constant).
38    pub resource_count: u32,
39    /// Backend's bindless support level (probed once per backend
40    /// startup).
41    pub support: BindlessSupport,
42    /// Whether the kernel's access pattern is dynamic (different
43    /// indices per thread / per dispatch). Only `Full` support
44    /// handles dynamic indexing; `Static` is wasted on dynamic
45    /// access.
46    pub dynamic_indexing: bool,
47}
48
49/// Verdict from [`decide_bindless`].
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum BindlessDecision {
52    /// Use bindless  -  N resources go into a single descriptor array.
53    Bindless,
54    /// Use traditional per-resource bindings.
55    TraditionalBindings,
56}
57
58/// Threshold above which bindless wins on `Full` support backends.
59/// Below this count the per-handle bindless setup overhead dominates.
60/// Calibrated from backend microbenchmarks: around two dozen bindings
61/// is the crossover on current discrete GPUs.
62pub const BINDLESS_RESOURCE_COUNT_THRESHOLD: u32 = 24;
63
64/// Decide whether to use the bindless path for this dispatch.
65///
66/// Picks `Bindless` when:
67///   - support is `Full`, AND
68///   - resource_count >= [`BINDLESS_RESOURCE_COUNT_THRESHOLD`]
69///
70/// `Static` support is treated as `Bindless` only when the access
71/// pattern is NOT dynamic (every slot is guaranteed bound) AND the
72/// resource count clears the threshold. `Unsupported` always returns
73/// `TraditionalBindings`.
74#[must_use]
75pub fn decide_bindless(inputs: BindlessInputs) -> BindlessDecision {
76    if matches!(inputs.support, BindlessSupport::Unsupported) {
77        return BindlessDecision::TraditionalBindings;
78    }
79    if inputs.resource_count < BINDLESS_RESOURCE_COUNT_THRESHOLD {
80        return BindlessDecision::TraditionalBindings;
81    }
82    match inputs.support {
83        BindlessSupport::Full => BindlessDecision::Bindless,
84        BindlessSupport::Static if !inputs.dynamic_indexing => BindlessDecision::Bindless,
85        _ => BindlessDecision::TraditionalBindings,
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    fn inp(count: u32, support: BindlessSupport, dynamic: bool) -> BindlessInputs {
94        BindlessInputs {
95            resource_count: count,
96            support,
97            dynamic_indexing: dynamic,
98        }
99    }
100
101    #[test]
102    fn unsupported_always_returns_traditional() {
103        for count in [0, 8, 24, 100, u32::MAX] {
104            for dynamic in [false, true] {
105                assert_eq!(
106                    decide_bindless(inp(count, BindlessSupport::Unsupported, dynamic)),
107                    BindlessDecision::TraditionalBindings
108                );
109            }
110        }
111    }
112
113    #[test]
114    fn below_threshold_returns_traditional_on_full_support() {
115        // 23 < threshold(24).
116        assert_eq!(
117            decide_bindless(inp(23, BindlessSupport::Full, true)),
118            BindlessDecision::TraditionalBindings
119        );
120    }
121
122    #[test]
123    fn at_threshold_returns_bindless_on_full_support() {
124        assert_eq!(
125            decide_bindless(inp(24, BindlessSupport::Full, true)),
126            BindlessDecision::Bindless
127        );
128    }
129
130    #[test]
131    fn above_threshold_returns_bindless_on_full_support() {
132        assert_eq!(
133            decide_bindless(inp(100, BindlessSupport::Full, false)),
134            BindlessDecision::Bindless
135        );
136    }
137
138    #[test]
139    fn static_support_with_dynamic_access_returns_traditional() {
140        // Static can't satisfy dynamic indexing of unbound slots  -
141        // dynamic access on Static-only support falls back.
142        assert_eq!(
143            decide_bindless(inp(100, BindlessSupport::Static, true)),
144            BindlessDecision::TraditionalBindings
145        );
146    }
147
148    #[test]
149    fn static_support_with_static_access_returns_bindless() {
150        // Static support with non-dynamic access is the sweet spot
151        // for fixed descriptor arrays.
152        assert_eq!(
153            decide_bindless(inp(100, BindlessSupport::Static, false)),
154            BindlessDecision::Bindless
155        );
156    }
157
158    #[test]
159    fn static_support_below_threshold_returns_traditional() {
160        // Even with non-dynamic access, low count → traditional.
161        assert_eq!(
162            decide_bindless(inp(10, BindlessSupport::Static, false)),
163            BindlessDecision::TraditionalBindings
164        );
165    }
166
167    #[test]
168    fn zero_resources_always_traditional() {
169        for support in [
170            BindlessSupport::Full,
171            BindlessSupport::Static,
172            BindlessSupport::Unsupported,
173        ] {
174            assert_eq!(
175                decide_bindless(inp(0, support, false)),
176                BindlessDecision::TraditionalBindings
177            );
178        }
179    }
180
181    #[test]
182    fn threshold_constant_matches_documentation() {
183        // Pin the calibrated threshold so casual edits don't move it
184        // without a corresponding benchmark update.
185        assert_eq!(BINDLESS_RESOURCE_COUNT_THRESHOLD, 24);
186    }
187}