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