1pub const MAX_WORKGROUPS_PER_DIM: u32 = 65_535;
25
26pub const COPY_BYTES_PER_ROW_ALIGNMENT: u32 = 256;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub struct Limits {
38 pub max_workgroup_size_x: u32,
40 pub max_workgroup_size_y: u32,
42 pub max_workgroup_size_z: u32,
44 pub max_invocations_per_workgroup: u32,
47 pub max_workgroups_per_dim: u32,
49}
50
51impl Limits {
52 #[must_use]
58 pub fn portable_default() -> Self {
59 Self {
60 max_workgroup_size_x: 256,
61 max_workgroup_size_y: 256,
62 max_workgroup_size_z: 64,
63 max_invocations_per_workgroup: 256,
64 max_workgroups_per_dim: MAX_WORKGROUPS_PER_DIM,
65 }
66 }
67
68 #[must_use]
71 pub fn desktop_default() -> Self {
72 Self {
73 max_workgroup_size_x: 1024,
74 max_workgroup_size_y: 1024,
75 max_workgroup_size_z: 64,
76 max_invocations_per_workgroup: 1024,
77 max_workgroups_per_dim: MAX_WORKGROUPS_PER_DIM,
78 }
79 }
80}
81
82#[inline]
84#[must_use]
85pub fn div_ceil_u32(a: u32, b: u32) -> u32 {
86 match a.checked_div(b) {
87 Some(q) => q + u32::from(a % b != 0),
88 None => 0,
89 }
90}
91
92#[inline]
97#[must_use]
98pub fn align_up_u32(value: u32, align: u32) -> u32 {
99 if align == 0 {
100 return value;
101 }
102 div_ceil_u32(value, align).saturating_mul(align)
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub struct Workgroup1D {
108 pub x: u32,
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub struct Workgroup2D {
115 pub x: u32,
117 pub y: u32,
119}
120
121impl Workgroup2D {
122 #[must_use]
124 pub fn total(self) -> u32 {
125 self.x.saturating_mul(self.y)
126 }
127}
128
129#[must_use]
136pub fn plan_workgroup_1d(limits: &Limits, preferred: u32) -> Workgroup1D {
137 let ceiling = limits
138 .max_workgroup_size_x
139 .min(limits.max_invocations_per_workgroup);
140 let want = preferred.max(1).min(ceiling.max(1));
141 Workgroup1D {
142 x: floor_pow2(want).max(1),
143 }
144}
145
146#[must_use]
156pub fn plan_workgroup_square(limits: &Limits, preferred_tile: u32) -> Workgroup2D {
157 let axis_cap = limits.max_workgroup_size_x.min(limits.max_workgroup_size_y);
158 let mut t = floor_pow2(preferred_tile.max(1)).max(1);
159 t = t.min(floor_pow2(axis_cap.max(1)).max(1));
160 while t > 1 && t.saturating_mul(t) > limits.max_invocations_per_workgroup {
162 t >>= 1;
163 }
164 Workgroup2D { x: t, y: t }
165}
166
167#[inline]
169#[must_use]
170pub fn floor_pow2(v: u32) -> u32 {
171 if v == 0 {
172 0
173 } else {
174 1u32 << (31 - v.leading_zeros())
175 }
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
181pub struct DispatchGrid {
182 pub x: u32,
184 pub y: u32,
186 pub z: u32,
188}
189
190impl DispatchGrid {
191 #[must_use]
193 pub fn total(self) -> u64 {
194 u64::from(self.x) * u64::from(self.y) * u64::from(self.z)
195 }
196}
197
198#[allow(clippy::missing_errors_doc)]
213pub fn plan_dispatch_1d(
214 limits: &Limits,
215 total_threads: u64,
216 workgroup_x: u32,
217) -> Result<(DispatchGrid, u32), String> {
218 let wg = workgroup_x.max(1);
219 let groups = div_ceil_u64(total_threads, u64::from(wg));
220 let max_dim = u64::from(limits.max_workgroups_per_dim.max(1));
221
222 if groups <= max_dim {
223 let gx = u32::try_from(groups).unwrap_or(limits.max_workgroups_per_dim);
224 return Ok((DispatchGrid { x: gx, y: 1, z: 1 }, gx));
225 }
226
227 let grid_x = max_dim;
229 let grid_y = div_ceil_u64(groups, grid_x);
230 if grid_y > max_dim {
231 return Err(format!(
232 "dispatch of {groups} workgroups exceeds 2-D capacity ({max_dim} x {max_dim})"
233 ));
234 }
235 let gx = u32::try_from(grid_x).unwrap_or(limits.max_workgroups_per_dim);
236 let gy = u32::try_from(grid_y).unwrap_or(limits.max_workgroups_per_dim);
237 Ok((DispatchGrid { x: gx, y: gy, z: 1 }, gx))
238}
239
240#[allow(clippy::missing_errors_doc)]
246pub fn plan_dispatch_2d(
247 limits: &Limits,
248 rows_m: u32,
249 cols_n: u32,
250 tile: Workgroup2D,
251 batch: u32,
252) -> Result<DispatchGrid, String> {
253 let gx = div_ceil_u32(cols_n, tile.x.max(1));
254 let gy = div_ceil_u32(rows_m, tile.y.max(1));
255 let gz = batch.max(1);
256 let cap = limits.max_workgroups_per_dim.max(1);
257 if gx > cap || gy > cap || gz > cap {
258 return Err(format!(
259 "2-D dispatch ({gx}, {gy}, {gz}) exceeds per-axis cap {cap}"
260 ));
261 }
262 Ok(DispatchGrid {
263 x: gx,
264 y: gy,
265 z: gz,
266 })
267}
268
269#[inline]
271#[must_use]
272pub fn div_ceil_u64(a: u64, b: u64) -> u64 {
273 match a.checked_div(b) {
274 Some(q) => q + u64::from(a % b != 0),
275 None => 0,
276 }
277}
278
279#[derive(Debug, Clone, Copy, PartialEq, Eq)]
281pub struct TextureRowLayout {
282 pub unpadded_bytes_per_row: u32,
284 pub padded_bytes_per_row: u32,
286 pub total_bytes: u64,
288}
289
290#[must_use]
297pub fn texture_row_layout(width: u32, height: u32, bytes_per_texel: u32) -> TextureRowLayout {
298 let unpadded = width.saturating_mul(bytes_per_texel);
299 let padded = align_up_u32(unpadded, COPY_BYTES_PER_ROW_ALIGNMENT);
300 TextureRowLayout {
301 unpadded_bytes_per_row: unpadded,
302 padded_bytes_per_row: padded,
303 total_bytes: u64::from(padded) * u64::from(height),
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 #[test]
312 fn div_ceil_u32_basic() {
313 assert_eq!(div_ceil_u32(0, 4), 0);
314 assert_eq!(div_ceil_u32(1, 4), 1);
315 assert_eq!(div_ceil_u32(4, 4), 1);
316 assert_eq!(div_ceil_u32(5, 4), 2);
317 assert_eq!(div_ceil_u32(8, 4), 2);
318 assert_eq!(div_ceil_u32(10, 0), 0);
320 }
321
322 #[test]
323 fn align_up_u32_to_256() {
324 assert_eq!(align_up_u32(0, 256), 0);
325 assert_eq!(align_up_u32(1, 256), 256);
326 assert_eq!(align_up_u32(256, 256), 256);
327 assert_eq!(align_up_u32(257, 256), 512);
328 assert_eq!(align_up_u32(513, 256), 768);
329 assert_eq!(align_up_u32(123, 0), 123);
331 }
332
333 #[test]
334 fn floor_pow2_values() {
335 assert_eq!(floor_pow2(0), 0);
336 assert_eq!(floor_pow2(1), 1);
337 assert_eq!(floor_pow2(2), 2);
338 assert_eq!(floor_pow2(3), 2);
339 assert_eq!(floor_pow2(255), 128);
340 assert_eq!(floor_pow2(256), 256);
341 assert_eq!(floor_pow2(257), 256);
342 assert_eq!(floor_pow2(1024), 1024);
343 }
344
345 #[test]
346 fn plan_workgroup_1d_respects_invocation_cap() {
347 let lim = Limits::portable_default(); assert_eq!(plan_workgroup_1d(&lim, 256).x, 256);
350 assert_eq!(plan_workgroup_1d(&lim, 1024).x, 256);
352 assert_eq!(plan_workgroup_1d(&lim, 200).x, 128);
354 assert_eq!(plan_workgroup_1d(&lim, 0).x, 1);
356 }
357
358 #[test]
359 fn plan_workgroup_1d_desktop_allows_larger() {
360 let lim = Limits::desktop_default(); assert_eq!(plan_workgroup_1d(&lim, 1024).x, 1024);
362 assert_eq!(plan_workgroup_1d(&lim, 2048).x, 1024);
363 }
364
365 #[test]
366 fn plan_workgroup_square_caps_at_16_on_portable() {
367 let lim = Limits::portable_default(); let wg = plan_workgroup_square(&lim, 32);
369 assert_eq!(wg, Workgroup2D { x: 16, y: 16 });
370 assert!(wg.total() <= lim.max_invocations_per_workgroup);
371 }
372
373 #[test]
374 fn plan_workgroup_square_allows_32_on_desktop() {
375 let lim = Limits::desktop_default(); let wg = plan_workgroup_square(&lim, 32);
377 assert_eq!(wg, Workgroup2D { x: 32, y: 32 });
378 assert_eq!(wg.total(), 1024);
379 }
380
381 #[test]
382 fn plan_workgroup_square_respects_preferred_smaller() {
383 let lim = Limits::desktop_default();
384 let wg = plan_workgroup_square(&lim, 8);
386 assert_eq!(wg, Workgroup2D { x: 8, y: 8 });
387 }
388
389 #[test]
390 fn plan_dispatch_1d_simple() {
391 let lim = Limits::portable_default();
392 let (grid, grid_x) = plan_dispatch_1d(&lim, 1000, 256).expect("plan");
393 assert_eq!(grid, DispatchGrid { x: 4, y: 1, z: 1 });
395 assert_eq!(grid_x, 4);
396 }
397
398 #[test]
399 fn plan_dispatch_1d_exact_multiple() {
400 let lim = Limits::portable_default();
401 let (grid, _) = plan_dispatch_1d(&lim, 512, 256).expect("plan");
402 assert_eq!(grid.x, 2);
403 assert_eq!(grid.total(), 2);
404 }
405
406 #[test]
407 fn plan_dispatch_1d_folds_into_2d_when_too_wide() {
408 let lim = Limits::portable_default(); let total_groups = u64::from(MAX_WORKGROUPS_PER_DIM) * 2 + 1;
411 let total_threads = total_groups; let (grid, grid_x) = plan_dispatch_1d(&lim, total_threads, 1).expect("plan");
413 assert!(grid.y > 1, "expected 2-D fold, got {grid:?}");
414 assert_eq!(grid.x, MAX_WORKGROUPS_PER_DIM);
415 assert_eq!(grid_x, MAX_WORKGROUPS_PER_DIM);
416 assert!(grid.total() >= total_groups);
418 assert!(u64::from(grid.x) * u64::from(grid.y) >= total_groups);
420 }
421
422 #[test]
423 fn plan_dispatch_1d_errors_when_beyond_2d_capacity() {
424 let lim = Limits::portable_default();
425 let beyond = u64::from(MAX_WORKGROUPS_PER_DIM) * u64::from(MAX_WORKGROUPS_PER_DIM) + 1;
427 let err = plan_dispatch_1d(&lim, beyond, 1).unwrap_err();
428 assert!(err.contains("exceeds 2-D capacity"));
429 }
430
431 #[test]
432 fn plan_dispatch_2d_gemm() {
433 let lim = Limits::portable_default();
434 let tile = Workgroup2D { x: 16, y: 16 };
435 let grid = plan_dispatch_2d(&lim, 100, 200, tile, 1).expect("plan");
437 assert_eq!(grid.x, div_ceil_u32(200, 16)); assert_eq!(grid.y, div_ceil_u32(100, 16)); assert_eq!(grid.z, 1);
440 }
441
442 #[test]
443 fn plan_dispatch_2d_batched() {
444 let lim = Limits::portable_default();
445 let tile = Workgroup2D { x: 8, y: 8 };
446 let grid = plan_dispatch_2d(&lim, 32, 32, tile, 7).expect("plan");
447 assert_eq!(grid.z, 7);
448 assert_eq!(grid.x, 4);
449 assert_eq!(grid.y, 4);
450 }
451
452 #[test]
453 fn plan_dispatch_2d_errors_when_axis_overflows() {
454 let lim = Limits::portable_default();
455 let tile = Workgroup2D { x: 1, y: 1 };
456 let huge = MAX_WORKGROUPS_PER_DIM + 10;
458 let err = plan_dispatch_2d(&lim, 4, huge, tile, 1).unwrap_err();
459 assert!(err.contains("exceeds per-axis cap"));
460 }
461
462 #[test]
463 fn texture_row_layout_padding() {
464 let layout = texture_row_layout(100, 50, 4);
466 assert_eq!(layout.unpadded_bytes_per_row, 400);
467 assert_eq!(layout.padded_bytes_per_row, 512);
468 assert_eq!(layout.total_bytes, 512 * 50);
469 assert_eq!(
471 layout.padded_bytes_per_row % COPY_BYTES_PER_ROW_ALIGNMENT,
472 0
473 );
474 }
475
476 #[test]
477 fn texture_row_layout_already_aligned() {
478 let layout = texture_row_layout(64, 10, 4);
480 assert_eq!(layout.unpadded_bytes_per_row, 256);
481 assert_eq!(layout.padded_bytes_per_row, 256);
482 assert_eq!(layout.total_bytes, 2560);
483 }
484
485 #[test]
486 fn limits_portable_default_is_conservative() {
487 let lim = Limits::portable_default();
488 assert_eq!(lim.max_invocations_per_workgroup, 256);
489 assert_eq!(lim.max_workgroups_per_dim, MAX_WORKGROUPS_PER_DIM);
490 let desk = Limits::desktop_default();
492 assert!(desk.max_invocations_per_workgroup >= lim.max_invocations_per_workgroup);
493 }
494}