1use crate::shader::MIN_SUBGROUP_SIZE;
2use crate::shader::constants::{
3 K, NUM_MATCH_BUCKETS, NUM_S_BUCKETS, NUM_TABLES, REDUCED_MATCHES_COUNT,
4};
5use crate::shader::find_matches_and_compute_f7::{NUM_ELEMENTS_PER_S_BUCKET, ProofTargets};
6use crate::shader::types::{Position, PositionExt};
7use core::mem::MaybeUninit;
8use spirv_std::arch::{
9 atomic_or, subgroup_ballot, subgroup_memory_barrier, subgroup_shuffle, subgroup_u_min,
10 workgroup_memory_barrier_with_group_sync,
11};
12use spirv_std::glam::UVec3;
13use spirv_std::memory::{Scope, Semantics};
14use spirv_std::spirv;
15#[cfg(not(target_arch = "spirv"))]
16use subspace_core_primitives::pos::PosProof;
17
18pub const WORKGROUP_SIZE: u32 = 256;
21const PROOF_X_SOURCES: usize = 2usize.pow(NUM_TABLES as u32 - 1);
22const PROOF_BITS: usize = PROOF_X_SOURCES * K as usize;
23const PROOF_BYTES: usize = PROOF_BITS.div_ceil(u8::BITS as usize);
24pub const PROOF_U32_WORDS: usize = PROOF_BYTES.div_ceil(size_of::<u32>());
25pub const FOUND_PROOFS_U32_WORDS: usize = {
26 assert!(NUM_S_BUCKETS.is_multiple_of(u32::BITS as usize));
27
28 NUM_S_BUCKETS / u32::BITS as usize
29};
30
31#[derive(Debug, Copy, Clone)]
32#[repr(C)]
33pub struct Proofs {
34 found_proofs: [MaybeUninit<u32>; FOUND_PROOFS_U32_WORDS],
35 proofs: [MaybeUninit<u32>; PROOF_U32_WORDS * NUM_S_BUCKETS],
38}
39
40#[derive(Debug, Copy, Clone)]
42#[cfg(not(target_arch = "spirv"))]
43#[repr(C)]
44pub struct ProofsHost {
45 pub found_proofs: [u8; NUM_S_BUCKETS / u8::BITS as usize],
49 pub proofs: [PosProof; NUM_S_BUCKETS],
53}
54
55#[cfg(not(target_arch = "spirv"))]
56const _: () = {
57 assert!(size_of::<Proofs>() == size_of::<ProofsHost>());
58};
59
60#[expect(
66 clippy::too_many_arguments,
67 reason = "Both I/O and Vulkan stuff together take a lot of arguments"
68)]
69#[inline(always)]
70fn find_local_proof_targets<const SUBGROUP_SIZE: u32>(
71 local_invocation_id: u32,
72 subgroup_id: u32,
73 subgroup_local_invocation_id: u32,
74 positions_group_index: u32,
75 bucket_sizes: &mut [u32; NUM_S_BUCKETS],
76 buckets: &[[ProofTargets; NUM_ELEMENTS_PER_S_BUCKET]; NUM_S_BUCKETS],
77 found_proofs: &mut [MaybeUninit<u32>; FOUND_PROOFS_U32_WORDS],
78 found_proofs_scratch: &mut [MaybeUninit<u32>; (WORKGROUP_SIZE / u32::BITS) as usize],
79) -> [Position; 2] {
80 let local_invocation_id = local_invocation_id as usize;
81 let base = positions_group_index * SUBGROUP_SIZE;
82
83 let mut min = [Position::SENTINEL; 2];
84
85 let local_bucket_size = {
86 let bucket_id = base + subgroup_local_invocation_id;
87
88 let local_bucket_size = bucket_sizes[bucket_id as usize];
89 bucket_sizes[bucket_id as usize] = 0;
90
91 local_bucket_size
92 };
93
94 for local_bucket_id in 0..SUBGROUP_SIZE {
95 let bucket_id = (base + local_bucket_id) as usize;
96 let bucket_size = subgroup_shuffle(local_bucket_size, local_bucket_id);
97 let bucket = &buckets[bucket_id];
98
99 let mut local_min_absolute_position = [u32::MAX];
106 let mut local_min_positions = [Position::SENTINEL; 2];
107
108 for index in (subgroup_local_invocation_id..bucket_size).step_by(SUBGROUP_SIZE as usize) {
109 let proof_targets = bucket[index as usize];
110 if proof_targets.absolute_position < local_min_absolute_position[0] {
111 local_min_absolute_position[0] = proof_targets.absolute_position;
112 local_min_positions = proof_targets.positions;
113 }
114 }
115
116 let min_absolute_position = subgroup_u_min(local_min_absolute_position[0]);
117 let source_lane_mask =
118 subgroup_ballot(local_min_absolute_position[0] == min_absolute_position);
119 let source_lane_mask = source_lane_mask.to_array();
123 let mut source_lane = u32::MAX;
124 for i in 0..SUBGROUP_SIZE.div_ceil(u32::BITS) {
125 let word = source_lane_mask[i as usize];
126 if word != 0 {
127 source_lane = word.trailing_zeros() + i * u32::BITS;
128 break;
129 }
130 }
131 let local_min = subgroup_shuffle(local_min_positions, source_lane);
132
133 if subgroup_local_invocation_id == local_bucket_id {
134 min = local_min;
135 }
136 }
137
138 let has_proof = local_bucket_size > 0;
139 let found_proofs_words = subgroup_ballot(has_proof);
140
141 if SUBGROUP_SIZE >= u32::BITS {
142 if subgroup_local_invocation_id == 0 {
147 let start_word = (base / u32::BITS) as usize;
148 found_proofs[start_word].write(found_proofs_words.x);
149
150 if SUBGROUP_SIZE >= 2 * u32::BITS {
151 found_proofs[start_word + 1].write(found_proofs_words.y);
152
153 if SUBGROUP_SIZE >= 4 * u32::BITS {
154 found_proofs[start_word + 2].write(found_proofs_words.z);
155 found_proofs[start_word + 3].write(found_proofs_words.w);
156 }
157 }
158 }
159 } else {
160 if local_invocation_id < found_proofs_scratch.len() {
161 found_proofs_scratch[local_invocation_id].write(0);
162 }
163
164 workgroup_memory_barrier_with_group_sync();
165
166 if subgroup_local_invocation_id == 0 {
171 let local_start_bit = subgroup_id * SUBGROUP_SIZE;
172 let local_word_index = (local_start_bit / u32::BITS) as usize;
173 let local_word_shift = local_start_bit % u32::BITS;
174
175 let found_proofs_word =
177 unsafe { found_proofs_scratch[local_word_index].assume_init_mut() };
178 unsafe {
181 atomic_or::<_, { Scope::Workgroup as u32 }, { Semantics::NONE.bits() }>(
182 found_proofs_word,
183 found_proofs_words.x << local_word_shift,
184 );
185 }
186 }
187
188 workgroup_memory_barrier_with_group_sync();
189
190 if local_invocation_id < found_proofs_scratch.len() {
191 let workgroup_base_group_index = positions_group_index - subgroup_id;
192 let workgroup_start_bucket = workgroup_base_group_index * SUBGROUP_SIZE;
193 let global_start_word = (workgroup_start_bucket / u32::BITS) as usize;
194
195 let found_proofs_word =
197 unsafe { found_proofs_scratch[local_invocation_id].assume_init() };
198 found_proofs[global_start_word + local_invocation_id].write(found_proofs_word);
199 }
200 }
201
202 min
203}
204
205#[expect(
207 clippy::too_many_arguments,
208 reason = "Both I/O and Vulkan stuff together take a lot of arguments"
209)]
210fn find_proofs_impl<const SUBGROUP_SIZE: u32>(
211 local_invocation_id: u32,
212 subgroup_id: u32,
213 subgroup_local_invocation_id: u32,
214 positions_group_index: u32,
215 table_2_positions: &[[Position; 2]; REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS],
219 table_3_positions: &[[Position; 2]; REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS],
220 table_4_positions: &[[Position; 2]; REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS],
221 table_5_positions: &[[Position; 2]; REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS],
222 table_6_positions: &[[Position; 2]; REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS],
223 bucket_sizes: &mut [u32; NUM_S_BUCKETS],
224 buckets: &[[ProofTargets; NUM_ELEMENTS_PER_S_BUCKET]; NUM_S_BUCKETS],
225 found_proofs: &mut [MaybeUninit<u32>; FOUND_PROOFS_U32_WORDS],
226 proofs: &mut [MaybeUninit<u32>; PROOF_U32_WORDS * NUM_S_BUCKETS],
230 found_proofs_scratch: &mut [MaybeUninit<u32>; (WORKGROUP_SIZE / u32::BITS) as usize],
231) where
232 [(); PROOF_X_SOURCES.div_ceil(SUBGROUP_SIZE as usize)]:,
233 [(); PROOF_U32_WORDS.div_ceil(SUBGROUP_SIZE as usize)]:,
234{
235 let table_6_proof_targets = find_local_proof_targets::<SUBGROUP_SIZE>(
236 local_invocation_id,
237 subgroup_id,
238 subgroup_local_invocation_id,
239 positions_group_index,
240 bucket_sizes,
241 buckets,
242 found_proofs,
243 found_proofs_scratch,
244 );
245
246 {
250 let positions_group_words = SUBGROUP_SIZE as usize * PROOF_U32_WORDS;
251 let base_word = positions_group_index as usize * positions_group_words;
252
253 for offset in (subgroup_local_invocation_id as usize..positions_group_words)
254 .step_by(SUBGROUP_SIZE as usize)
255 {
256 proofs[base_word + offset].write(0);
257 }
258
259 subgroup_memory_barrier();
260 }
261
262 let left_right = (subgroup_local_invocation_id % 2) as usize;
264 const {
266 assert!(MIN_SUBGROUP_SIZE >= 2);
267 }
268
269 let mut chunk_index = 0u32;
272 loop {
274 let table_6_proof_targets = subgroup_shuffle(
275 table_6_proof_targets,
276 SUBGROUP_SIZE / 2 * (chunk_index & 1) + subgroup_local_invocation_id / 2,
277 );
278 let table_6_proof_target = table_6_proof_targets[left_right];
279
280 let table_5_proof_targets = if table_6_proof_target == Position::SENTINEL {
281 [Position::SENTINEL; 2]
282 } else {
283 table_6_positions[table_6_proof_target as usize]
284 };
285
286 chunk_index <<= 1u8;
288 loop {
289 let table_5_proof_targets = subgroup_shuffle(
290 table_5_proof_targets,
291 SUBGROUP_SIZE / 2 * (chunk_index & 1) + subgroup_local_invocation_id / 2,
292 );
293 let table_5_proof_target = table_5_proof_targets[left_right];
294
295 let table_4_proof_targets = if table_5_proof_target == Position::SENTINEL {
296 [Position::SENTINEL; 2]
297 } else {
298 table_5_positions[table_5_proof_target as usize]
299 };
300
301 chunk_index <<= 1u8;
303 loop {
304 let table_4_proof_targets = subgroup_shuffle(
305 table_4_proof_targets,
306 SUBGROUP_SIZE / 2 * (chunk_index & 1) + subgroup_local_invocation_id / 2,
307 );
308 let table_4_proof_target = table_4_proof_targets[left_right];
309
310 let table_3_proof_targets = if table_4_proof_target == Position::SENTINEL {
311 [Position::SENTINEL; 2]
312 } else {
313 table_4_positions[table_4_proof_target as usize]
314 };
315
316 chunk_index <<= 1u8;
318 loop {
319 let table_3_proof_targets = subgroup_shuffle(
320 table_3_proof_targets,
321 SUBGROUP_SIZE / 2 * (chunk_index & 1) + subgroup_local_invocation_id / 2,
322 );
323 let table_3_proof_target = table_3_proof_targets[left_right];
324
325 let table_2_proof_targets = if table_3_proof_target == Position::SENTINEL {
326 [Position::SENTINEL; 2]
327 } else {
328 table_3_positions[table_3_proof_target as usize]
329 };
330
331 chunk_index <<= 1u8;
333 loop {
334 let table_2_proof_targets = subgroup_shuffle(
335 table_2_proof_targets,
336 SUBGROUP_SIZE / 2 * (chunk_index & 1)
337 + subgroup_local_invocation_id / 2,
338 );
339 let table_2_proof_target = table_2_proof_targets[left_right];
340
341 let [x_left, x_right] = if table_2_proof_target == Position::SENTINEL {
342 [Position::SENTINEL; 2]
343 } else {
344 table_2_positions[table_2_proof_target as usize]
345 };
346
347 let global_x_left_offset =
348 subgroup_local_invocation_id * 2 + chunk_index * SUBGROUP_SIZE * 2;
349 let group_proof_index = global_x_left_offset / PROOF_X_SOURCES as u32;
350 let x_left_offset = global_x_left_offset % PROOF_X_SOURCES as u32;
351 let global_proof_index =
352 positions_group_index * SUBGROUP_SIZE + group_proof_index;
353
354 let proof_base = global_proof_index as usize * PROOF_U32_WORDS;
355 let first_proof_word_index =
356 ((u32::from(K) * x_left_offset) / u32::BITS) as usize;
357
358 let mut local_proof_words = [0u32; 3];
363 let x_left_offset_in_bits = u32::from(K) * x_left_offset;
364 {
365 let bit_offset = x_left_offset_in_bits % u32::BITS;
366 let x_shifted_to_start = x_left << (u32::BITS - u32::from(K));
367
368 let first_word = x_shifted_to_start >> bit_offset;
369 let second_word =
370 x_shifted_to_start.unbounded_shl(u32::BITS - bit_offset);
371
372 local_proof_words[0] = first_word;
373 local_proof_words[1] = second_word;
374 }
375
376 let max_local_proof_word_index = {
377 let x_right_offset_in_bits = x_left_offset_in_bits + u32::from(K);
378 let local_proof_words_index = (x_right_offset_in_bits / u32::BITS)
379 as usize
380 - first_proof_word_index;
381 let bit_offset = x_right_offset_in_bits % u32::BITS;
382 let x_shifted_to_start = x_right << (u32::BITS - u32::from(K));
383
384 let first_word = x_shifted_to_start >> bit_offset;
385 let second_word =
386 x_shifted_to_start.unbounded_shl(u32::BITS - bit_offset);
387
388 local_proof_words[local_proof_words_index] |= first_word;
389 local_proof_words[local_proof_words_index + 1] = second_word;
390
391 local_proof_words_index + usize::from(second_word != 0)
392 };
393
394 {
396 let word = unsafe {
399 proofs[proof_base + first_proof_word_index].assume_init_mut()
400 };
401 unsafe {
404 atomic_or::<
405 _,
406 { Scope::Subgroup as u32 },
407 { Semantics::NONE.bits() },
408 >(
409 word, local_proof_words[0].to_be()
410 );
411 }
412 }
413 if max_local_proof_word_index > 0 {
415 let word = unsafe {
418 proofs[proof_base + first_proof_word_index + 1].assume_init_mut()
419 };
420 unsafe {
423 atomic_or::<
424 _,
425 { Scope::Subgroup as u32 },
426 { Semantics::NONE.bits() },
427 >(
428 word, local_proof_words[1].to_be()
429 );
430 }
431 }
432 if max_local_proof_word_index > 1 {
433 let word = unsafe {
436 proofs[proof_base + first_proof_word_index + 2].assume_init_mut()
437 };
438 unsafe {
441 atomic_or::<
442 _,
443 { Scope::Subgroup as u32 },
444 { Semantics::NONE.bits() },
445 >(
446 word, local_proof_words[2].to_be()
447 );
448 }
449 }
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 chunk_index >>= 1u8;
478
479 if chunk_index & 1 == 1 {
480 break;
481 }
482 chunk_index += 1;
483 }
484}
485
486#[spirv(compute(threads(256), entry_point_name = "find_proofs"))]
491#[expect(
492 clippy::too_many_arguments,
493 reason = "Both I/O and Vulkan stuff together take a lot of arguments"
494)]
495pub fn find_proofs(
496 #[spirv(workgroup_id)] workgroup_id: UVec3,
497 #[spirv(local_invocation_id)] local_invocation_id: UVec3,
498 #[spirv(subgroup_id)] subgroup_id: u32,
499 #[spirv(subgroup_size)] subgroup_size: u32,
500 #[spirv(num_subgroups)] num_subgroups: u32,
501 #[spirv(subgroup_local_invocation_id)] subgroup_local_invocation_id: u32,
502 #[spirv(storage_buffer, descriptor_set = 0, binding = 0)]
503 table_2_positions: &[[Position; 2]; REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS],
504 #[spirv(storage_buffer, descriptor_set = 0, binding = 1)]
505 table_3_positions: &[[Position; 2]; REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS],
506 #[spirv(storage_buffer, descriptor_set = 0, binding = 2)]
507 table_4_positions: &[[Position; 2]; REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS],
508 #[spirv(storage_buffer, descriptor_set = 0, binding = 3)]
509 table_5_positions: &[[Position; 2]; REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS],
510 #[spirv(storage_buffer, descriptor_set = 0, binding = 4)]
511 table_6_positions: &[[Position; 2]; REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS],
512 #[spirv(storage_buffer, descriptor_set = 0, binding = 5)] bucket_sizes: &mut [u32;
513 NUM_S_BUCKETS],
514 #[spirv(storage_buffer, descriptor_set = 0, binding = 6)] buckets: &[[ProofTargets; NUM_ELEMENTS_PER_S_BUCKET];
515 NUM_S_BUCKETS],
516 #[spirv(storage_buffer, descriptor_set = 0, binding = 7)] proofs: &mut Proofs,
517 #[spirv(workgroup)] found_proofs_scratch: &mut [MaybeUninit<u32>;
518 (WORKGROUP_SIZE / u32::BITS) as usize],
519) {
520 let local_invocation_id = local_invocation_id.x;
521 let workgroup_id = workgroup_id.x;
522
523 let global_subgroup_id = workgroup_id * num_subgroups + subgroup_id;
524
525 let positions_group_index = global_subgroup_id;
526 match subgroup_size {
530 1 => {
532 find_proofs_impl::<1>(
533 local_invocation_id,
534 subgroup_id,
535 subgroup_local_invocation_id,
536 positions_group_index,
537 table_2_positions,
538 table_3_positions,
539 table_4_positions,
540 table_5_positions,
541 table_6_positions,
542 bucket_sizes,
543 buckets,
544 &mut proofs.found_proofs,
545 &mut proofs.proofs,
546 found_proofs_scratch,
547 );
548 }
549 2 => {
551 find_proofs_impl::<2>(
552 local_invocation_id,
553 subgroup_id,
554 subgroup_local_invocation_id,
555 positions_group_index,
556 table_2_positions,
557 table_3_positions,
558 table_4_positions,
559 table_5_positions,
560 table_6_positions,
561 bucket_sizes,
562 buckets,
563 &mut proofs.found_proofs,
564 &mut proofs.proofs,
565 found_proofs_scratch,
566 );
567 }
568 4 => {
570 find_proofs_impl::<4>(
571 local_invocation_id,
572 subgroup_id,
573 subgroup_local_invocation_id,
574 positions_group_index,
575 table_2_positions,
576 table_3_positions,
577 table_4_positions,
578 table_5_positions,
579 table_6_positions,
580 bucket_sizes,
581 buckets,
582 &mut proofs.found_proofs,
583 &mut proofs.proofs,
584 found_proofs_scratch,
585 );
586 }
587 8 => {
589 find_proofs_impl::<8>(
590 local_invocation_id,
591 subgroup_id,
592 subgroup_local_invocation_id,
593 positions_group_index,
594 table_2_positions,
595 table_3_positions,
596 table_4_positions,
597 table_5_positions,
598 table_6_positions,
599 bucket_sizes,
600 buckets,
601 &mut proofs.found_proofs,
602 &mut proofs.proofs,
603 found_proofs_scratch,
604 );
605 }
606 16 => {
608 find_proofs_impl::<16>(
609 local_invocation_id,
610 subgroup_id,
611 subgroup_local_invocation_id,
612 positions_group_index,
613 table_2_positions,
614 table_3_positions,
615 table_4_positions,
616 table_5_positions,
617 table_6_positions,
618 bucket_sizes,
619 buckets,
620 &mut proofs.found_proofs,
621 &mut proofs.proofs,
622 found_proofs_scratch,
623 );
624 }
625 32 => {
627 find_proofs_impl::<32>(
628 local_invocation_id,
629 subgroup_id,
630 subgroup_local_invocation_id,
631 positions_group_index,
632 table_2_positions,
633 table_3_positions,
634 table_4_positions,
635 table_5_positions,
636 table_6_positions,
637 bucket_sizes,
638 buckets,
639 &mut proofs.found_proofs,
640 &mut proofs.proofs,
641 found_proofs_scratch,
642 );
643 }
644 64 => {
646 find_proofs_impl::<64>(
647 local_invocation_id,
648 subgroup_id,
649 subgroup_local_invocation_id,
650 positions_group_index,
651 table_2_positions,
652 table_3_positions,
653 table_4_positions,
654 table_5_positions,
655 table_6_positions,
656 bucket_sizes,
657 buckets,
658 &mut proofs.found_proofs,
659 &mut proofs.proofs,
660 found_proofs_scratch,
661 );
662 }
663 128 => {
665 find_proofs_impl::<128>(
666 local_invocation_id,
667 subgroup_id,
668 subgroup_local_invocation_id,
669 positions_group_index,
670 table_2_positions,
671 table_3_positions,
672 table_4_positions,
673 table_5_positions,
674 table_6_positions,
675 bucket_sizes,
676 buckets,
677 &mut proofs.found_proofs,
678 &mut proofs.proofs,
679 found_proofs_scratch,
680 );
681 }
682 _ => {
683 unreachable!("All Vulkan targets use power of two and subgroup size <= 128")
685 }
686 }
687}