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::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::glam::UVec3;
18use spirv_std::memory::{Scope, Semantics};
19use spirv_std::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>; const { 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 found_proofs: [u8; const { NUM_S_BUCKETS / u8::BITS as usize }],
56 pub proofs: [PosProof; NUM_S_BUCKETS],
62}
63
64#[cfg(not(target_arch = "spirv"))]
65const _: () = {
66 assert!(size_of::<Proofs>() == size_of::<ProofsHost>());
67};
68
69#[expect(
75 clippy::too_many_arguments,
76 reason = "Both I/O and Vulkan stuff together take a lot of arguments"
77)]
78#[inline(always)]
79fn find_local_proof_targets<const SUBGROUP_SIZE: u32>(
80 local_invocation_id: u32,
81 subgroup_id: u32,
82 subgroup_local_invocation_id: u32,
83 positions_group_index: u32,
84 bucket_sizes: &mut [u32; NUM_S_BUCKETS],
85 buckets: &[[ProofTargets; NUM_ELEMENTS_PER_S_BUCKET]; NUM_S_BUCKETS],
86 found_proofs: &mut [MaybeUninit<u32>; FOUND_PROOFS_U32_WORDS],
87 found_proofs_scratch: &mut [MaybeUninit<u32>; const { (WORKGROUP_SIZE / u32::BITS) as usize }],
88) -> [Position; 2] {
89 let local_invocation_id = local_invocation_id as usize;
90 let base = positions_group_index * SUBGROUP_SIZE;
91
92 let mut min = [Position::SENTINEL; 2];
93
94 let local_bucket_size = {
95 let bucket_id = base + subgroup_local_invocation_id;
96
97 let local_bucket_size = bucket_sizes[bucket_id as usize];
98 bucket_sizes[bucket_id as usize] = 0;
99
100 local_bucket_size
101 };
102
103 for local_bucket_id in 0..SUBGROUP_SIZE {
104 let bucket_id = (base + local_bucket_id) as usize;
105 let bucket_size = subgroup_shuffle(local_bucket_size, local_bucket_id);
106 let bucket = &buckets[bucket_id];
107
108 let mut local_min_absolute_position = [u32::MAX];
115 let mut local_min_positions = [Position::SENTINEL; 2];
116
117 for index in (subgroup_local_invocation_id..bucket_size).step_by(SUBGROUP_SIZE as usize) {
118 let proof_targets = bucket[index as usize];
119 if proof_targets.absolute_position < local_min_absolute_position[0] {
120 local_min_absolute_position[0] = proof_targets.absolute_position;
121 local_min_positions = proof_targets.positions;
122 }
123 }
124
125 let min_absolute_position = subgroup_u_min(local_min_absolute_position[0]);
126 let source_lane_mask =
127 subgroup_ballot(local_min_absolute_position[0] == min_absolute_position);
128 let source_lane_mask = source_lane_mask.to_array();
132 let mut source_lane = u32::MAX;
133 for i in 0..SUBGROUP_SIZE.div_ceil(u32::BITS) {
134 let word = source_lane_mask[i as usize];
135 if word != 0 {
136 source_lane = word.trailing_zeros() + i * u32::BITS;
137 break;
138 }
139 }
140 let local_min = subgroup_shuffle(local_min_positions, source_lane);
141
142 if subgroup_local_invocation_id == local_bucket_id {
143 min = local_min;
144 }
145 }
146
147 let has_proof = local_bucket_size > 0;
148 let found_proofs_words = subgroup_ballot(has_proof);
149
150 if SUBGROUP_SIZE >= u32::BITS {
151 if subgroup_local_invocation_id == 0 {
156 let start_word = (base / u32::BITS) as usize;
157 found_proofs[start_word].write(found_proofs_words.x);
158
159 if SUBGROUP_SIZE >= 2 * u32::BITS {
160 found_proofs[start_word + 1].write(found_proofs_words.y);
161
162 if SUBGROUP_SIZE >= 4 * u32::BITS {
163 found_proofs[start_word + 2].write(found_proofs_words.z);
164 found_proofs[start_word + 3].write(found_proofs_words.w);
165 }
166 }
167 }
168 } else {
169 if local_invocation_id < found_proofs_scratch.len() {
170 found_proofs_scratch[local_invocation_id].write(0);
171 }
172
173 workgroup_memory_barrier_with_group_sync();
174
175 if subgroup_local_invocation_id == 0 {
180 let local_start_bit = subgroup_id * SUBGROUP_SIZE;
181 let local_word_index = (local_start_bit / u32::BITS) as usize;
182 let local_word_shift = local_start_bit % u32::BITS;
183
184 let found_proofs_word =
186 unsafe { found_proofs_scratch[local_word_index].assume_init_mut() };
187 unsafe {
190 atomic_or::<_, const { Scope::Workgroup as u32 }, const { Semantics::NONE.bits() }>(
191 found_proofs_word,
192 found_proofs_words.x << local_word_shift,
193 );
194 }
195 }
196
197 workgroup_memory_barrier_with_group_sync();
198
199 if local_invocation_id < found_proofs_scratch.len() {
200 let workgroup_base_group_index = positions_group_index - subgroup_id;
201 let workgroup_start_bucket = workgroup_base_group_index * SUBGROUP_SIZE;
202 let global_start_word = (workgroup_start_bucket / u32::BITS) as usize;
203
204 let found_proofs_word =
206 unsafe { found_proofs_scratch[local_invocation_id].assume_init() };
207 found_proofs[global_start_word + local_invocation_id].write(found_proofs_word);
208 }
209 }
210
211 min
212}
213
214#[expect(
216 clippy::too_many_arguments,
217 reason = "Both I/O and Vulkan stuff together take a lot of arguments"
218)]
219fn find_proofs_impl<const SUBGROUP_SIZE: u32>(
220 local_invocation_id: u32,
221 subgroup_id: u32,
222 subgroup_local_invocation_id: u32,
223 positions_group_index: u32,
224 table_2_positions: &[[Position; 2]; const { REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS }],
228 table_3_positions: &[[Position; 2]; const { REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS }],
229 table_4_positions: &[[Position; 2]; const { REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS }],
230 table_5_positions: &[[Position; 2]; const { REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS }],
231 table_6_positions: &[[Position; 2]; const { REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS }],
232 bucket_sizes: &mut [u32; NUM_S_BUCKETS],
233 buckets: &[[ProofTargets; NUM_ELEMENTS_PER_S_BUCKET]; NUM_S_BUCKETS],
234 found_proofs: &mut [MaybeUninit<u32>; FOUND_PROOFS_U32_WORDS],
235 proofs: &mut [MaybeUninit<u32>; const { PROOF_U32_WORDS * NUM_S_BUCKETS }],
239 found_proofs_scratch: &mut [MaybeUninit<u32>; const { (WORKGROUP_SIZE / u32::BITS) as usize }],
240) {
241 let table_6_proof_targets = find_local_proof_targets::<SUBGROUP_SIZE>(
242 local_invocation_id,
243 subgroup_id,
244 subgroup_local_invocation_id,
245 positions_group_index,
246 bucket_sizes,
247 buckets,
248 found_proofs,
249 found_proofs_scratch,
250 );
251
252 {
256 let positions_group_words = SUBGROUP_SIZE as usize * PROOF_U32_WORDS;
257 let base_word = positions_group_index as usize * positions_group_words;
258
259 for offset in (subgroup_local_invocation_id as usize..positions_group_words)
260 .step_by(SUBGROUP_SIZE as usize)
261 {
262 proofs[base_word + offset].write(0);
263 }
264
265 subgroup_memory_barrier();
266 }
267
268 let left_right = (subgroup_local_invocation_id % 2) as usize;
270 const {
272 assert!(MIN_SUBGROUP_SIZE >= 2);
273 }
274
275 let mut chunk_index = 0u32;
278 loop {
280 let table_6_proof_targets = subgroup_shuffle(
281 table_6_proof_targets,
282 SUBGROUP_SIZE / 2 * (chunk_index & 1) + subgroup_local_invocation_id / 2,
283 );
284 let table_6_proof_target = table_6_proof_targets[left_right];
285
286 let table_5_proof_targets = if table_6_proof_target == Position::SENTINEL {
287 [Position::SENTINEL; 2]
288 } else {
289 table_6_positions[table_6_proof_target as usize]
290 };
291
292 chunk_index <<= 1u8;
294 loop {
295 let table_5_proof_targets = subgroup_shuffle(
296 table_5_proof_targets,
297 SUBGROUP_SIZE / 2 * (chunk_index & 1) + subgroup_local_invocation_id / 2,
298 );
299 let table_5_proof_target = table_5_proof_targets[left_right];
300
301 let table_4_proof_targets = if table_5_proof_target == Position::SENTINEL {
302 [Position::SENTINEL; 2]
303 } else {
304 table_5_positions[table_5_proof_target as usize]
305 };
306
307 chunk_index <<= 1u8;
309 loop {
310 let table_4_proof_targets = subgroup_shuffle(
311 table_4_proof_targets,
312 SUBGROUP_SIZE / 2 * (chunk_index & 1) + subgroup_local_invocation_id / 2,
313 );
314 let table_4_proof_target = table_4_proof_targets[left_right];
315
316 let table_3_proof_targets = if table_4_proof_target == Position::SENTINEL {
317 [Position::SENTINEL; 2]
318 } else {
319 table_4_positions[table_4_proof_target as usize]
320 };
321
322 chunk_index <<= 1u8;
324 loop {
325 let table_3_proof_targets = subgroup_shuffle(
326 table_3_proof_targets,
327 SUBGROUP_SIZE / 2 * (chunk_index & 1) + subgroup_local_invocation_id / 2,
328 );
329 let table_3_proof_target = table_3_proof_targets[left_right];
330
331 let table_2_proof_targets = if table_3_proof_target == Position::SENTINEL {
332 [Position::SENTINEL; 2]
333 } else {
334 table_3_positions[table_3_proof_target as usize]
335 };
336
337 chunk_index <<= 1u8;
339 loop {
340 let table_2_proof_targets = subgroup_shuffle(
341 table_2_proof_targets,
342 SUBGROUP_SIZE / 2 * (chunk_index & 1)
343 + subgroup_local_invocation_id / 2,
344 );
345 let table_2_proof_target = table_2_proof_targets[left_right];
346
347 let [x_left, x_right] = if table_2_proof_target == Position::SENTINEL {
348 [Position::SENTINEL; 2]
349 } else {
350 table_2_positions[table_2_proof_target as usize]
351 };
352
353 let global_x_left_offset =
354 subgroup_local_invocation_id * 2 + chunk_index * SUBGROUP_SIZE * 2;
355 let group_proof_index = global_x_left_offset / PROOF_X_SOURCES as u32;
356 let x_left_offset = global_x_left_offset % PROOF_X_SOURCES as u32;
357 let global_proof_index =
358 positions_group_index * SUBGROUP_SIZE + group_proof_index;
359
360 let proof_base = global_proof_index as usize * PROOF_U32_WORDS;
361 let first_proof_word_index =
362 ((u32::from(K) * x_left_offset) / u32::BITS) as usize;
363
364 let mut local_proof_words = [0u32; 3];
369 let x_left_offset_in_bits = u32::from(K) * x_left_offset;
370 {
371 let bit_offset = x_left_offset_in_bits % u32::BITS;
372 let x_shifted_to_start = x_left << (u32::BITS - u32::from(K));
373
374 let first_word = x_shifted_to_start >> bit_offset;
375 let second_word =
376 x_shifted_to_start.unbounded_shl(u32::BITS - bit_offset);
377
378 local_proof_words[0] = first_word;
379 local_proof_words[1] = second_word;
380 }
381
382 let max_local_proof_word_index = {
383 let x_right_offset_in_bits = x_left_offset_in_bits + u32::from(K);
384 let local_proof_words_index = (x_right_offset_in_bits / u32::BITS)
385 as usize
386 - first_proof_word_index;
387 let bit_offset = x_right_offset_in_bits % u32::BITS;
388 let x_shifted_to_start = x_right << (u32::BITS - u32::from(K));
389
390 let first_word = x_shifted_to_start >> bit_offset;
391 let second_word =
392 x_shifted_to_start.unbounded_shl(u32::BITS - bit_offset);
393
394 local_proof_words[local_proof_words_index] |= first_word;
395 local_proof_words[local_proof_words_index + 1] = second_word;
396
397 local_proof_words_index + usize::from(second_word != 0)
398 };
399
400 {
402 let word = unsafe {
405 proofs[proof_base + first_proof_word_index].assume_init_mut()
406 };
407 unsafe {
410 atomic_or::<
411 _,
412 const { Scope::Subgroup as u32 },
413 const { Semantics::NONE.bits() },
414 >(
415 word, local_proof_words[0].to_be()
416 );
417 }
418 }
419 if max_local_proof_word_index > 0 {
421 let word = unsafe {
424 proofs[proof_base + first_proof_word_index + 1].assume_init_mut()
425 };
426 unsafe {
429 atomic_or::<
430 _,
431 const { Scope::Subgroup as u32 },
432 const { Semantics::NONE.bits() },
433 >(
434 word, local_proof_words[1].to_be()
435 );
436 }
437 }
438 if max_local_proof_word_index > 1 {
439 let word = unsafe {
442 proofs[proof_base + first_proof_word_index + 2].assume_init_mut()
443 };
444 unsafe {
447 atomic_or::<
448 _,
449 const { Scope::Subgroup as u32 },
450 const { Semantics::NONE.bits() },
451 >(
452 word, local_proof_words[2].to_be()
453 );
454 }
455 }
456
457 if chunk_index & 1 == 1 {
458 break;
459 }
460 chunk_index += 1;
461 }
462 chunk_index >>= 1u8;
463
464 if chunk_index & 1 == 1 {
465 break;
466 }
467 chunk_index += 1;
468 }
469 chunk_index >>= 1u8;
470
471 if chunk_index & 1 == 1 {
472 break;
473 }
474 chunk_index += 1;
475 }
476 chunk_index >>= 1u8;
477
478 if chunk_index & 1 == 1 {
479 break;
480 }
481 chunk_index += 1;
482 }
483 chunk_index >>= 1u8;
484
485 if chunk_index & 1 == 1 {
486 break;
487 }
488 chunk_index += 1;
489 }
490}
491
492#[spirv(compute(threads(256), entry_point_name = "find_proofs"))]
497#[expect(
498 clippy::too_many_arguments,
499 reason = "Both I/O and Vulkan stuff together take a lot of arguments"
500)]
501pub fn find_proofs(
502 #[spirv(workgroup_id)] workgroup_id: UVec3,
503 #[spirv(local_invocation_id)] local_invocation_id: UVec3,
504 #[spirv(subgroup_id)] subgroup_id: u32,
505 #[spirv(subgroup_size)] subgroup_size: u32,
506 #[spirv(num_subgroups)] num_subgroups: u32,
507 #[spirv(subgroup_local_invocation_id)] subgroup_local_invocation_id: u32,
508 #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] table_2_positions: &[[Position; 2];
509 const {
510 REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS
511 }],
512 #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] table_3_positions: &[[Position; 2];
513 const {
514 REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS
515 }],
516 #[spirv(storage_buffer, descriptor_set = 0, binding = 2)] table_4_positions: &[[Position; 2];
517 const {
518 REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS
519 }],
520 #[spirv(storage_buffer, descriptor_set = 0, binding = 3)] table_5_positions: &[[Position; 2];
521 const {
522 REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS
523 }],
524 #[spirv(storage_buffer, descriptor_set = 0, binding = 4)] table_6_positions: &[[Position; 2];
525 const {
526 REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS
527 }],
528 #[spirv(storage_buffer, descriptor_set = 0, binding = 5)] bucket_sizes: &mut [u32;
529 NUM_S_BUCKETS],
530 #[spirv(storage_buffer, descriptor_set = 0, binding = 6)] buckets: &[[ProofTargets; NUM_ELEMENTS_PER_S_BUCKET];
531 NUM_S_BUCKETS],
532 #[spirv(storage_buffer, descriptor_set = 0, binding = 7)] proofs: &mut Proofs,
533 #[spirv(workgroup)] found_proofs_scratch: &mut [MaybeUninit<u32>; const {
534 (WORKGROUP_SIZE / u32::BITS) as usize
535 }],
536) {
537 let local_invocation_id = local_invocation_id.x;
538 let workgroup_id = workgroup_id.x;
539
540 let global_subgroup_id = workgroup_id * num_subgroups + subgroup_id;
541
542 let positions_group_index = global_subgroup_id;
543 match subgroup_size {
547 1 => {
549 find_proofs_impl::<1>(
550 local_invocation_id,
551 subgroup_id,
552 subgroup_local_invocation_id,
553 positions_group_index,
554 table_2_positions,
555 table_3_positions,
556 table_4_positions,
557 table_5_positions,
558 table_6_positions,
559 bucket_sizes,
560 buckets,
561 &mut proofs.found_proofs,
562 &mut proofs.proofs,
563 found_proofs_scratch,
564 );
565 }
566 2 => {
568 find_proofs_impl::<2>(
569 local_invocation_id,
570 subgroup_id,
571 subgroup_local_invocation_id,
572 positions_group_index,
573 table_2_positions,
574 table_3_positions,
575 table_4_positions,
576 table_5_positions,
577 table_6_positions,
578 bucket_sizes,
579 buckets,
580 &mut proofs.found_proofs,
581 &mut proofs.proofs,
582 found_proofs_scratch,
583 );
584 }
585 4 => {
587 find_proofs_impl::<4>(
588 local_invocation_id,
589 subgroup_id,
590 subgroup_local_invocation_id,
591 positions_group_index,
592 table_2_positions,
593 table_3_positions,
594 table_4_positions,
595 table_5_positions,
596 table_6_positions,
597 bucket_sizes,
598 buckets,
599 &mut proofs.found_proofs,
600 &mut proofs.proofs,
601 found_proofs_scratch,
602 );
603 }
604 8 => {
606 find_proofs_impl::<8>(
607 local_invocation_id,
608 subgroup_id,
609 subgroup_local_invocation_id,
610 positions_group_index,
611 table_2_positions,
612 table_3_positions,
613 table_4_positions,
614 table_5_positions,
615 table_6_positions,
616 bucket_sizes,
617 buckets,
618 &mut proofs.found_proofs,
619 &mut proofs.proofs,
620 found_proofs_scratch,
621 );
622 }
623 16 => {
625 find_proofs_impl::<16>(
626 local_invocation_id,
627 subgroup_id,
628 subgroup_local_invocation_id,
629 positions_group_index,
630 table_2_positions,
631 table_3_positions,
632 table_4_positions,
633 table_5_positions,
634 table_6_positions,
635 bucket_sizes,
636 buckets,
637 &mut proofs.found_proofs,
638 &mut proofs.proofs,
639 found_proofs_scratch,
640 );
641 }
642 32 => {
644 find_proofs_impl::<32>(
645 local_invocation_id,
646 subgroup_id,
647 subgroup_local_invocation_id,
648 positions_group_index,
649 table_2_positions,
650 table_3_positions,
651 table_4_positions,
652 table_5_positions,
653 table_6_positions,
654 bucket_sizes,
655 buckets,
656 &mut proofs.found_proofs,
657 &mut proofs.proofs,
658 found_proofs_scratch,
659 );
660 }
661 64 => {
663 find_proofs_impl::<64>(
664 local_invocation_id,
665 subgroup_id,
666 subgroup_local_invocation_id,
667 positions_group_index,
668 table_2_positions,
669 table_3_positions,
670 table_4_positions,
671 table_5_positions,
672 table_6_positions,
673 bucket_sizes,
674 buckets,
675 &mut proofs.found_proofs,
676 &mut proofs.proofs,
677 found_proofs_scratch,
678 );
679 }
680 128 => {
682 find_proofs_impl::<128>(
683 local_invocation_id,
684 subgroup_id,
685 subgroup_local_invocation_id,
686 positions_group_index,
687 table_2_positions,
688 table_3_positions,
689 table_4_positions,
690 table_5_positions,
691 table_6_positions,
692 bucket_sizes,
693 buckets,
694 &mut proofs.found_proofs,
695 &mut proofs.proofs,
696 found_proofs_scratch,
697 );
698 }
699 _ => {
700 unreachable!("All Vulkan targets use power of two and subgroup size <= 128")
702 }
703 }
704}