1#[cfg(all(test, not(miri), not(target_arch = "spirv")))]
2mod cpu_tests;
3#[cfg(all(test, not(miri), not(target_arch = "spirv")))]
4mod gpu_tests;
5
6use crate::shader::MIN_SUBGROUP_SIZE;
7use crate::shader::constants::{
8 K, NUM_MATCH_BUCKETS, NUM_S_BUCKETS, NUM_TABLES, REDUCED_MATCHES_COUNT,
9};
10use crate::shader::find_matches_and_compute_f7::{NUM_ELEMENTS_PER_S_BUCKET, ProofTargets};
11use crate::shader::types::{Position, PositionExt};
12use core::mem::MaybeUninit;
13use spirv_std_nightly::arch::{
14 atomic_or, subgroup_ballot, subgroup_memory_barrier, subgroup_shuffle, subgroup_u_min,
15 workgroup_memory_barrier_with_group_sync,
16};
17use spirv_std_nightly::glam::UVec3;
18use spirv_std_nightly::memory::{Scope, Semantics};
19use spirv_std_nightly::spirv;
20#[cfg(not(target_arch = "spirv"))]
21use subspace_core_primitives::pos::PosProof;
22
23pub const WORKGROUP_SIZE: u32 = 256;
26const PROOF_X_SOURCES: usize = 2usize.pow(NUM_TABLES as u32 - 1);
27const PROOF_BITS: usize = PROOF_X_SOURCES * K as usize;
28const PROOF_BYTES: usize = PROOF_BITS.div_ceil(u8::BITS as usize);
29pub const PROOF_U32_WORDS: usize = PROOF_BYTES.div_ceil(size_of::<u32>());
30pub const FOUND_PROOFS_U32_WORDS: usize = {
31 assert!(NUM_S_BUCKETS.is_multiple_of(u32::BITS as usize));
32
33 NUM_S_BUCKETS / u32::BITS as usize
34};
35
36#[derive(Debug, Copy, Clone)]
37#[repr(C)]
38pub struct Proofs {
39 found_proofs: [MaybeUninit<u32>; FOUND_PROOFS_U32_WORDS],
40 proofs: [MaybeUninit<u32>; PROOF_U32_WORDS * NUM_S_BUCKETS],
43}
44
45#[derive(Debug, Copy, Clone)]
47#[cfg(not(target_arch = "spirv"))]
48#[repr(C)]
49pub struct ProofsHost {
50 pub mut(self) found_proofs: [u8; NUM_S_BUCKETS / u8::BITS as usize],
54 pub mut(self) proofs: [PosProof; NUM_S_BUCKETS],
58}
59
60#[cfg(not(target_arch = "spirv"))]
61const _: () = {
62 assert!(size_of::<Proofs>() == size_of::<ProofsHost>());
63};
64
65#[expect(
71 clippy::too_many_arguments,
72 reason = "Both I/O and Vulkan stuff together take a lot of arguments"
73)]
74#[inline(always)]
75fn find_local_proof_targets<const SUBGROUP_SIZE: u32>(
76 local_invocation_id: u32,
77 subgroup_id: u32,
78 subgroup_local_invocation_id: u32,
79 positions_group_index: u32,
80 bucket_sizes: &mut [u32; NUM_S_BUCKETS],
81 buckets: &[[ProofTargets; NUM_ELEMENTS_PER_S_BUCKET]; NUM_S_BUCKETS],
82 found_proofs: &mut [MaybeUninit<u32>; FOUND_PROOFS_U32_WORDS],
83 found_proofs_scratch: &mut [MaybeUninit<u32>; (WORKGROUP_SIZE / u32::BITS) as usize],
84) -> [Position; 2] {
85 let local_invocation_id = local_invocation_id as usize;
86 let base = positions_group_index * SUBGROUP_SIZE;
87
88 let mut min = [Position::SENTINEL; 2];
89
90 let local_bucket_size = {
91 let bucket_id = base + subgroup_local_invocation_id;
92
93 let local_bucket_size = bucket_sizes[bucket_id as usize];
94 bucket_sizes[bucket_id as usize] = 0;
95
96 local_bucket_size
97 };
98
99 for local_bucket_id in 0..SUBGROUP_SIZE {
100 let bucket_id = (base + local_bucket_id) as usize;
101 let bucket_size = subgroup_shuffle(local_bucket_size, local_bucket_id);
102 let bucket = &buckets[bucket_id];
103
104 let mut local_min_absolute_position = [u32::MAX];
111 let mut local_min_positions = [Position::SENTINEL; 2];
112
113 for index in (subgroup_local_invocation_id..bucket_size).step_by(SUBGROUP_SIZE as usize) {
114 let proof_targets = bucket[index as usize];
115 if proof_targets.absolute_position < local_min_absolute_position[0] {
116 local_min_absolute_position[0] = proof_targets.absolute_position;
117 local_min_positions = proof_targets.positions;
118 }
119 }
120
121 let min_absolute_position = subgroup_u_min(local_min_absolute_position[0]);
122 let source_lane_mask =
123 subgroup_ballot(local_min_absolute_position[0] == min_absolute_position);
124 let source_lane_mask = source_lane_mask.to_array();
128 let mut source_lane = u32::MAX;
129 for i in 0..SUBGROUP_SIZE.div_ceil(u32::BITS) {
130 let word = source_lane_mask[i as usize];
131 if word != 0 {
132 source_lane = word.trailing_zeros() + i * u32::BITS;
133 break;
134 }
135 }
136 let local_min = subgroup_shuffle(local_min_positions, source_lane);
137
138 if subgroup_local_invocation_id == local_bucket_id {
139 min = local_min;
140 }
141 }
142
143 let has_proof = local_bucket_size > 0;
144 let found_proofs_words = subgroup_ballot(has_proof);
145
146 if SUBGROUP_SIZE >= u32::BITS {
147 if subgroup_local_invocation_id == 0 {
152 let start_word = (base / u32::BITS) as usize;
153 found_proofs[start_word].write(found_proofs_words.x);
154
155 if SUBGROUP_SIZE >= 2 * u32::BITS {
156 found_proofs[start_word + 1].write(found_proofs_words.y);
157
158 if SUBGROUP_SIZE >= 4 * u32::BITS {
159 found_proofs[start_word + 2].write(found_proofs_words.z);
160 found_proofs[start_word + 3].write(found_proofs_words.w);
161 }
162 }
163 }
164 } else {
165 if local_invocation_id < found_proofs_scratch.len() {
166 found_proofs_scratch[local_invocation_id].write(0);
167 }
168
169 workgroup_memory_barrier_with_group_sync();
170
171 if subgroup_local_invocation_id == 0 {
176 let local_start_bit = subgroup_id * SUBGROUP_SIZE;
177 let local_word_index = (local_start_bit / u32::BITS) as usize;
178 let local_word_shift = local_start_bit % u32::BITS;
179
180 let found_proofs_word =
182 unsafe { found_proofs_scratch[local_word_index].assume_init_mut() };
183 unsafe {
186 atomic_or::<_, { Scope::Workgroup }, const { Semantics::NONE }>(
187 found_proofs_word,
188 found_proofs_words.x << local_word_shift,
189 );
190 }
191 }
192
193 workgroup_memory_barrier_with_group_sync();
194
195 if local_invocation_id < found_proofs_scratch.len() {
196 let workgroup_base_group_index = positions_group_index - subgroup_id;
197 let workgroup_start_bucket = workgroup_base_group_index * SUBGROUP_SIZE;
198 let global_start_word = (workgroup_start_bucket / u32::BITS) as usize;
199
200 let found_proofs_word =
202 unsafe { found_proofs_scratch[local_invocation_id].assume_init() };
203 found_proofs[global_start_word + local_invocation_id].write(found_proofs_word);
204 }
205 }
206
207 min
208}
209
210#[expect(
212 clippy::too_many_arguments,
213 reason = "Both I/O and Vulkan stuff together take a lot of arguments"
214)]
215fn find_proofs_impl<const SUBGROUP_SIZE: u32>(
216 local_invocation_id: u32,
217 subgroup_id: u32,
218 subgroup_local_invocation_id: u32,
219 positions_group_index: u32,
220 table_2_positions: &[[Position; 2]; REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS],
224 table_3_positions: &[[Position; 2]; REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS],
225 table_4_positions: &[[Position; 2]; REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS],
226 table_5_positions: &[[Position; 2]; REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS],
227 table_6_positions: &[[Position; 2]; REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS],
228 bucket_sizes: &mut [u32; NUM_S_BUCKETS],
229 buckets: &[[ProofTargets; NUM_ELEMENTS_PER_S_BUCKET]; NUM_S_BUCKETS],
230 found_proofs: &mut [MaybeUninit<u32>; FOUND_PROOFS_U32_WORDS],
231 proofs: &mut [MaybeUninit<u32>; PROOF_U32_WORDS * NUM_S_BUCKETS],
235 found_proofs_scratch: &mut [MaybeUninit<u32>; (WORKGROUP_SIZE / u32::BITS) as usize],
236) {
237 let table_6_proof_targets = find_local_proof_targets::<SUBGROUP_SIZE>(
238 local_invocation_id,
239 subgroup_id,
240 subgroup_local_invocation_id,
241 positions_group_index,
242 bucket_sizes,
243 buckets,
244 found_proofs,
245 found_proofs_scratch,
246 );
247
248 {
252 let positions_group_words = SUBGROUP_SIZE as usize * PROOF_U32_WORDS;
253 let base_word = positions_group_index as usize * positions_group_words;
254
255 for offset in (subgroup_local_invocation_id as usize..positions_group_words)
256 .step_by(SUBGROUP_SIZE as usize)
257 {
258 proofs[base_word + offset].write(0);
259 }
260
261 subgroup_memory_barrier();
262 }
263
264 let left_right = (subgroup_local_invocation_id % 2) as usize;
266 const {
268 assert!(MIN_SUBGROUP_SIZE >= 2);
269 }
270
271 let mut chunk_index = 0u32;
274 loop {
276 let table_6_proof_targets = subgroup_shuffle(
277 table_6_proof_targets,
278 SUBGROUP_SIZE / 2 * (chunk_index & 1) + subgroup_local_invocation_id / 2,
279 );
280 let table_6_proof_target = table_6_proof_targets[left_right];
281
282 let table_5_proof_targets = if table_6_proof_target == Position::SENTINEL {
283 [Position::SENTINEL; 2]
284 } else {
285 table_6_positions[table_6_proof_target as usize]
286 };
287
288 chunk_index <<= 1u8;
290 loop {
291 let table_5_proof_targets = subgroup_shuffle(
292 table_5_proof_targets,
293 SUBGROUP_SIZE / 2 * (chunk_index & 1) + subgroup_local_invocation_id / 2,
294 );
295 let table_5_proof_target = table_5_proof_targets[left_right];
296
297 let table_4_proof_targets = if table_5_proof_target == Position::SENTINEL {
298 [Position::SENTINEL; 2]
299 } else {
300 table_5_positions[table_5_proof_target as usize]
301 };
302
303 chunk_index <<= 1u8;
305 loop {
306 let table_4_proof_targets = subgroup_shuffle(
307 table_4_proof_targets,
308 SUBGROUP_SIZE / 2 * (chunk_index & 1) + subgroup_local_invocation_id / 2,
309 );
310 let table_4_proof_target = table_4_proof_targets[left_right];
311
312 let table_3_proof_targets = if table_4_proof_target == Position::SENTINEL {
313 [Position::SENTINEL; 2]
314 } else {
315 table_4_positions[table_4_proof_target as usize]
316 };
317
318 chunk_index <<= 1u8;
320 loop {
321 let table_3_proof_targets = subgroup_shuffle(
322 table_3_proof_targets,
323 SUBGROUP_SIZE / 2 * (chunk_index & 1) + subgroup_local_invocation_id / 2,
324 );
325 let table_3_proof_target = table_3_proof_targets[left_right];
326
327 let table_2_proof_targets = if table_3_proof_target == Position::SENTINEL {
328 [Position::SENTINEL; 2]
329 } else {
330 table_3_positions[table_3_proof_target as usize]
331 };
332
333 chunk_index <<= 1u8;
335 loop {
336 let table_2_proof_targets = subgroup_shuffle(
337 table_2_proof_targets,
338 SUBGROUP_SIZE / 2 * (chunk_index & 1)
339 + subgroup_local_invocation_id / 2,
340 );
341 let table_2_proof_target = table_2_proof_targets[left_right];
342
343 let [x_left, x_right] = if table_2_proof_target == Position::SENTINEL {
344 [Position::SENTINEL; 2]
345 } else {
346 table_2_positions[table_2_proof_target as usize]
347 };
348
349 let global_x_left_offset =
350 subgroup_local_invocation_id * 2 + chunk_index * SUBGROUP_SIZE * 2;
351 let group_proof_index = global_x_left_offset / PROOF_X_SOURCES as u32;
352 let x_left_offset = global_x_left_offset % PROOF_X_SOURCES as u32;
353 let global_proof_index =
354 positions_group_index * SUBGROUP_SIZE + group_proof_index;
355
356 let proof_base = global_proof_index as usize * PROOF_U32_WORDS;
357 let first_proof_word_index =
358 ((u32::from(K) * x_left_offset) / u32::BITS) as usize;
359
360 let mut local_proof_words = [0u32; 3];
365 let x_left_offset_in_bits = u32::from(K) * x_left_offset;
366 {
367 let bit_offset = x_left_offset_in_bits % u32::BITS;
368 let x_shifted_to_start = x_left << (u32::BITS - u32::from(K));
369
370 let first_word = x_shifted_to_start >> bit_offset;
371 let second_word =
372 x_shifted_to_start.unbounded_shl(u32::BITS - bit_offset);
373
374 local_proof_words[0] = first_word;
375 local_proof_words[1] = second_word;
376 }
377
378 let max_local_proof_word_index = {
379 let x_right_offset_in_bits = x_left_offset_in_bits + u32::from(K);
380 let local_proof_words_index = (x_right_offset_in_bits / u32::BITS)
381 as usize
382 - first_proof_word_index;
383 let bit_offset = x_right_offset_in_bits % u32::BITS;
384 let x_shifted_to_start = x_right << (u32::BITS - u32::from(K));
385
386 let first_word = x_shifted_to_start >> bit_offset;
387 let second_word =
388 x_shifted_to_start.unbounded_shl(u32::BITS - bit_offset);
389
390 local_proof_words[local_proof_words_index] |= first_word;
391 local_proof_words[local_proof_words_index + 1] = second_word;
392
393 local_proof_words_index + usize::from(second_word != 0)
394 };
395
396 {
398 let word = unsafe {
401 proofs[proof_base + first_proof_word_index].assume_init_mut()
402 };
403 unsafe {
406 atomic_or::<_, { Scope::Subgroup }, const { Semantics::NONE }>(
407 word,
408 local_proof_words[0].to_be(),
409 );
410 }
411 }
412 if max_local_proof_word_index > 0 {
414 let word = unsafe {
417 proofs[proof_base + first_proof_word_index + 1].assume_init_mut()
418 };
419 unsafe {
422 atomic_or::<_, { Scope::Subgroup }, const { Semantics::NONE }>(
423 word,
424 local_proof_words[1].to_be(),
425 );
426 }
427 }
428 if max_local_proof_word_index > 1 {
429 let word = unsafe {
432 proofs[proof_base + first_proof_word_index + 2].assume_init_mut()
433 };
434 unsafe {
437 atomic_or::<_, { Scope::Subgroup }, const { Semantics::NONE }>(
438 word,
439 local_proof_words[2].to_be(),
440 );
441 }
442 }
443
444 if chunk_index & 1 == 1 {
445 break;
446 }
447 chunk_index += 1;
448 }
449 chunk_index >>= 1u8;
450
451 if chunk_index & 1 == 1 {
452 break;
453 }
454 chunk_index += 1;
455 }
456 chunk_index >>= 1u8;
457
458 if chunk_index & 1 == 1 {
459 break;
460 }
461 chunk_index += 1;
462 }
463 chunk_index >>= 1u8;
464
465 if chunk_index & 1 == 1 {
466 break;
467 }
468 chunk_index += 1;
469 }
470 chunk_index >>= 1u8;
471
472 if chunk_index & 1 == 1 {
473 break;
474 }
475 chunk_index += 1;
476 }
477}
478
479#[spirv(compute(threads(256), entry_point_name = "find_proofs"))]
484#[expect(
485 clippy::too_many_arguments,
486 reason = "Both I/O and Vulkan stuff together take a lot of arguments"
487)]
488pub fn find_proofs(
489 #[spirv(workgroup_id)] workgroup_id: UVec3,
490 #[spirv(local_invocation_id)] local_invocation_id: UVec3,
491 #[spirv(subgroup_id)] subgroup_id: u32,
492 #[spirv(subgroup_size)] subgroup_size: u32,
493 #[spirv(num_subgroups)] num_subgroups: u32,
494 #[spirv(subgroup_local_invocation_id)] subgroup_local_invocation_id: u32,
495 #[spirv(storage_buffer, descriptor_set = 0, binding = 0)]
496 table_2_positions: &[[Position; 2]; REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS],
497 #[spirv(storage_buffer, descriptor_set = 0, binding = 1)]
498 table_3_positions: &[[Position; 2]; REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS],
499 #[spirv(storage_buffer, descriptor_set = 0, binding = 2)]
500 table_4_positions: &[[Position; 2]; REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS],
501 #[spirv(storage_buffer, descriptor_set = 0, binding = 3)]
502 table_5_positions: &[[Position; 2]; REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS],
503 #[spirv(storage_buffer, descriptor_set = 0, binding = 4)]
504 table_6_positions: &[[Position; 2]; REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS],
505 #[spirv(storage_buffer, descriptor_set = 0, binding = 5)] bucket_sizes: &mut [u32;
506 NUM_S_BUCKETS],
507 #[spirv(storage_buffer, descriptor_set = 0, binding = 6)] buckets: &[[ProofTargets; NUM_ELEMENTS_PER_S_BUCKET];
508 NUM_S_BUCKETS],
509 #[spirv(storage_buffer, descriptor_set = 0, binding = 7)] proofs: &mut Proofs,
510 #[spirv(workgroup)] found_proofs_scratch: &mut [MaybeUninit<u32>;
511 (WORKGROUP_SIZE / u32::BITS) as usize],
512) {
513 let local_invocation_id = local_invocation_id.x;
514 let workgroup_id = workgroup_id.x;
515
516 let global_subgroup_id = workgroup_id * num_subgroups + subgroup_id;
517
518 let positions_group_index = global_subgroup_id;
519 match subgroup_size {
523 1 => {
525 find_proofs_impl::<1>(
526 local_invocation_id,
527 subgroup_id,
528 subgroup_local_invocation_id,
529 positions_group_index,
530 table_2_positions,
531 table_3_positions,
532 table_4_positions,
533 table_5_positions,
534 table_6_positions,
535 bucket_sizes,
536 buckets,
537 &mut proofs.found_proofs,
538 &mut proofs.proofs,
539 found_proofs_scratch,
540 );
541 }
542 2 => {
544 find_proofs_impl::<2>(
545 local_invocation_id,
546 subgroup_id,
547 subgroup_local_invocation_id,
548 positions_group_index,
549 table_2_positions,
550 table_3_positions,
551 table_4_positions,
552 table_5_positions,
553 table_6_positions,
554 bucket_sizes,
555 buckets,
556 &mut proofs.found_proofs,
557 &mut proofs.proofs,
558 found_proofs_scratch,
559 );
560 }
561 4 => {
563 find_proofs_impl::<4>(
564 local_invocation_id,
565 subgroup_id,
566 subgroup_local_invocation_id,
567 positions_group_index,
568 table_2_positions,
569 table_3_positions,
570 table_4_positions,
571 table_5_positions,
572 table_6_positions,
573 bucket_sizes,
574 buckets,
575 &mut proofs.found_proofs,
576 &mut proofs.proofs,
577 found_proofs_scratch,
578 );
579 }
580 8 => {
582 find_proofs_impl::<8>(
583 local_invocation_id,
584 subgroup_id,
585 subgroup_local_invocation_id,
586 positions_group_index,
587 table_2_positions,
588 table_3_positions,
589 table_4_positions,
590 table_5_positions,
591 table_6_positions,
592 bucket_sizes,
593 buckets,
594 &mut proofs.found_proofs,
595 &mut proofs.proofs,
596 found_proofs_scratch,
597 );
598 }
599 16 => {
601 find_proofs_impl::<16>(
602 local_invocation_id,
603 subgroup_id,
604 subgroup_local_invocation_id,
605 positions_group_index,
606 table_2_positions,
607 table_3_positions,
608 table_4_positions,
609 table_5_positions,
610 table_6_positions,
611 bucket_sizes,
612 buckets,
613 &mut proofs.found_proofs,
614 &mut proofs.proofs,
615 found_proofs_scratch,
616 );
617 }
618 32 => {
620 find_proofs_impl::<32>(
621 local_invocation_id,
622 subgroup_id,
623 subgroup_local_invocation_id,
624 positions_group_index,
625 table_2_positions,
626 table_3_positions,
627 table_4_positions,
628 table_5_positions,
629 table_6_positions,
630 bucket_sizes,
631 buckets,
632 &mut proofs.found_proofs,
633 &mut proofs.proofs,
634 found_proofs_scratch,
635 );
636 }
637 64 => {
639 find_proofs_impl::<64>(
640 local_invocation_id,
641 subgroup_id,
642 subgroup_local_invocation_id,
643 positions_group_index,
644 table_2_positions,
645 table_3_positions,
646 table_4_positions,
647 table_5_positions,
648 table_6_positions,
649 bucket_sizes,
650 buckets,
651 &mut proofs.found_proofs,
652 &mut proofs.proofs,
653 found_proofs_scratch,
654 );
655 }
656 128 => {
658 find_proofs_impl::<128>(
659 local_invocation_id,
660 subgroup_id,
661 subgroup_local_invocation_id,
662 positions_group_index,
663 table_2_positions,
664 table_3_positions,
665 table_4_positions,
666 table_5_positions,
667 table_6_positions,
668 bucket_sizes,
669 buckets,
670 &mut proofs.found_proofs,
671 &mut proofs.proofs,
672 found_proofs_scratch,
673 );
674 }
675 _ => {
676 unreachable!("All Vulkan targets use power of two and subgroup size <= 128")
678 }
679 }
680}