1use henad_core::helpers::fmt_bytes;
7
8use crate::display_scale::display_dims;
9use crate::gpu::primitives::pipeline::{storage_entry, uniform_entry};
10use henad_core::authoring::model::binding::{BindingDecl, BindingKind};
11
12pub(crate) fn storage_bindings(decls: &[BindingDecl]) -> u32 {
15 decls.iter().filter(|d| d.kind.is_storage_buffer()).count() as u32
16}
17
18pub(crate) fn layout_entry(i: u32, decl: &BindingDecl) -> wgpu::BindGroupLayoutEntry {
20 match decl.kind {
21 BindingKind::Storage { read_only } => storage_entry(i, read_only),
22 BindingKind::Uniform => uniform_entry(i),
23 BindingKind::StorageTexture => wgpu::BindGroupLayoutEntry {
24 binding: i,
25 visibility: wgpu::ShaderStages::COMPUTE,
26 ty: wgpu::BindingType::StorageTexture {
27 access: wgpu::StorageTextureAccess::WriteOnly,
28 format: wgpu::TextureFormat::Rgba8Unorm,
29 view_dimension: wgpu::TextureViewDimension::D2,
30 },
31 count: None,
32 },
33 }
34}
35
36#[derive(Debug)]
38pub struct Alloc {
39 pub label: String,
41 pub bytes: u64,
43}
44
45#[derive(Debug)]
47pub struct PassBindings {
48 pub label: String,
50 pub storage: u32,
52}
53
54#[derive(Debug, Default)]
59pub struct Demand {
60 pub buffers: Vec<Alloc>,
62 pub texture: Option<(u32, u32)>,
64 pub passes: Vec<PassBindings>,
66}
67
68impl Demand {
69 pub fn push(&mut self, label: String, words: usize) {
71 self.buffers.push(Alloc {
72 label,
73 bytes: words as u64 * std::mem::size_of::<u32>() as u64,
74 });
75 }
76
77 pub fn push_sides(&mut self, label: &str, words: usize, doubled: bool) {
79 self.push(format!("{label}_a"), words);
80 if doubled {
81 self.push(format!("{label}_b"), words);
82 }
83 }
84
85 pub fn push_index(&mut self, label: &str, n_cells: u32, num_agents: u32) {
88 let table = n_cells as usize + 1;
89 for name in ["counts", "cell_start", "cursor"] {
90 self.push(format!("{label}_hash_{name}"), table);
91 }
92 for name in ["agent_cell", "sorted"] {
93 self.push(format!("{label}_hash_{name}"), num_agents as usize);
94 }
95 }
96
97 pub fn set_display(&mut self, width: u32, height: u32, limits: &wgpu::Limits) {
99 self.texture = Some(display_dims(width, height, limits.max_texture_dimension_2d));
100 }
101
102 pub fn push_pass(&mut self, label: String, storage: u32) {
104 self.passes.push(PassBindings { label, storage });
105 }
106
107 pub fn bytes(&self) -> u64 {
109 let buffers: u64 = self.buffers.iter().map(|alloc| alloc.bytes).sum();
110 let texture = self
111 .texture
112 .map_or(0, |(w, h)| u64::from(w) * u64::from(h) * RGBA_BYTES);
113 buffers + texture
114 }
115
116 pub fn shortfalls(&self, limits: &wgpu::Limits) -> Vec<String> {
121 let mut lines = self.size_shortfalls(limits);
122 let allowed = limits.max_storage_buffers_per_shader_stage;
123 lines.extend(self.passes.iter().filter(|pass| pass.storage > allowed).map(|pass| {
124 format!(
125 "pass '{}' binds {} storage buffers, past the {allowed} this device allows per shader stage",
126 pass.label, pass.storage
127 )
128 }));
129 lines
130 }
131
132 fn size_shortfalls(&self, limits: &wgpu::Limits) -> Vec<String> {
133 let mut groups: Vec<Group<'_>> = Vec::new();
134 for alloc in &self.buffers {
135 let (limit, what) = if alloc.bytes > limits.max_storage_buffer_binding_size {
137 (limits.max_storage_buffer_binding_size, "storage binding")
138 } else if alloc.bytes > limits.max_buffer_size {
139 (limits.max_buffer_size, "buffer")
140 } else {
141 continue;
142 };
143 match groups.iter_mut().find(|g| g.bytes == alloc.bytes && g.limit == limit) {
144 Some(group) => group.count += 1,
145 None => groups.push(Group {
146 bytes: alloc.bytes,
147 limit,
148 what,
149 first: &alloc.label,
150 count: 1,
151 }),
152 }
153 }
154 groups.iter().map(Group::message).collect()
155 }
156}
157
158const RGBA_BYTES: u64 = 4;
159
160struct Group<'a> {
162 bytes: u64,
163 limit: u64,
164 what: &'static str,
165 first: &'a str,
166 count: usize,
167}
168
169impl Group<'_> {
170 fn message(&self) -> String {
171 let subject = match self.count {
172 1 => format!("'{}' needs", self.first),
173 n => format!("'{}' and {} more each need", self.first, n - 1),
174 };
175 format!(
176 "{subject} {}, past the {} this device allows for one {}",
177 fmt_bytes(self.bytes),
178 fmt_bytes(self.limit),
179 self.what
180 )
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::Demand;
187
188 fn limits(binding: u64, buffer: u64) -> wgpu::Limits {
189 wgpu::Limits {
190 max_storage_buffer_binding_size: binding,
191 max_buffer_size: buffer,
192 ..wgpu::Limits::default()
193 }
194 }
195
196 #[test]
198 fn a_model_that_fits_has_no_shortfalls() {
199 let mut demand = Demand::default();
200 demand.push_sides("state", 1024 * 1024, true);
201 demand.set_display(1024, 1024, &wgpu::Limits::default());
202 assert!(demand.shortfalls(&wgpu::Limits::default()).is_empty());
203 }
204
205 #[test]
207 fn a_buffer_past_the_binding_limit_is_named() {
208 let mut demand = Demand::default();
209 demand.push_sides("gpu_sir_buffer0", 36_000_000, true);
210 demand.push_sides("gpu_sir_buffer1", 36_000_000, true);
211 let found = demand.shortfalls(&limits(134_217_728, 268_435_456));
212 assert_eq!(
213 found.len(),
214 1,
215 "four same-sized sides are one line, not four: {found:?}"
216 );
217 assert!(
218 found[0].contains("gpu_sir_buffer0_a")
219 && found[0].contains("3 more")
220 && found[0].contains("storage binding"),
221 "{}",
222 found[0]
223 );
224 }
225
226 #[test]
228 fn different_sizes_get_their_own_line() {
229 let mut demand = Demand::default();
230 demand.push("small".to_owned(), 36_000_000);
231 demand.push("large".to_owned(), 50_000_000);
232 let found = demand.shortfalls(&limits(134_217_728, 268_435_456));
233 assert_eq!(found.len(), 2, "{found:?}");
234 }
235
236 #[test]
238 fn a_buffer_past_the_buffer_limit_says_so() {
239 let mut demand = Demand::default();
240 demand.push("huge".to_owned(), 1_000_000_000);
241 let found = demand.shortfalls(&limits(u64::MAX, 1 << 30));
242 assert_eq!(found.len(), 1);
243 assert!(found[0].contains("for one buffer"), "{}", found[0]);
244 }
245
246 #[test]
249 fn a_pass_over_the_binding_count_is_named() {
250 let mut demand = Demand::default();
251 demand.push_pass("gpu_ants_step".to_owned(), 9);
252 demand.push_pass("gpu_ants_merge".to_owned(), 2);
253 let found = demand.shortfalls(&wgpu::Limits::default());
254 assert_eq!(found.len(), 1, "only the step pass is over: {found:?}");
255 assert!(
256 found[0].contains("gpu_ants_step") && found[0].contains("per shader stage"),
257 "{}",
258 found[0]
259 );
260 }
261
262 #[test]
264 fn an_oversized_buffer_and_an_overbound_pass_are_both_reported() {
265 let mut demand = Demand::default();
266 demand.push("huge".to_owned(), 36_000_000);
267 demand.push_pass("wide".to_owned(), 99);
268 assert_eq!(demand.shortfalls(&wgpu::Limits::default()).len(), 2);
269 }
270
271 #[test]
273 fn display_bytes_stop_growing_with_the_grid() {
274 let mut small = Demand::default();
275 small.set_display(4096, 4096, &wgpu::Limits::default());
276 let mut huge = Demand::default();
277 huge.set_display(100_000, 100_000, &wgpu::Limits::default());
278 assert_eq!(small.bytes(), huge.bytes());
279 }
280}