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}