1use super::{GpuCsrBuffers, GpuDevice};
7use crate::NodeId;
8use anyhow::{Context, Result};
9
10#[repr(C)]
12#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
13struct BfsParams {
14 num_nodes: u32,
15 current_level: u32,
16 source_node: u32,
17 _padding: u32,
18}
19
20#[derive(Debug, Clone)]
22pub struct GpuBfsResult {
23 pub distances: Vec<u32>,
25
26 pub visited_count: usize,
28}
29
30impl GpuBfsResult {
31 #[must_use]
33 pub fn distance(&self, node: NodeId) -> Option<u32> {
34 self.distances.get(node.0 as usize).copied().filter(|&d| d != u32::MAX)
35 }
36
37 #[must_use]
39 pub fn is_reachable(&self, node: NodeId) -> bool {
40 self.distance(node).is_some()
41 }
42}
43
44async fn read_buffer_u32(device: &GpuDevice, buffer: &wgpu::Buffer) -> Result<u32> {
46 let staging_buffer = device.create_buffer(
47 "Staging Buffer",
48 4,
49 wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
50 )?;
51
52 let mut encoder =
53 device.device().create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
54 encoder.copy_buffer_to_buffer(buffer, 0, &staging_buffer, 0, 4);
55 device.queue().submit(Some(encoder.finish()));
56
57 let buffer_slice = staging_buffer.slice(..);
58 let (tx, rx) = futures_intrusive::channel::shared::oneshot_channel();
59
60 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
61 let _ = tx.send(result);
62 });
63
64 device.device().poll(wgpu::Maintain::Wait);
65 rx.receive().await.context("Failed to receive map result")?.context("Buffer mapping failed")?;
66
67 let data = buffer_slice.get_mapped_range();
68 let value = u32::from_ne_bytes(data[0..4].try_into()?);
69 drop(data);
70 staging_buffer.unmap();
71
72 Ok(value)
73}
74
75async fn read_distances(
77 device: &GpuDevice,
78 distances_buffer: &wgpu::Buffer,
79 num_nodes: usize,
80) -> Result<Vec<u32>> {
81 let size = (num_nodes * std::mem::size_of::<u32>()) as u64;
82 let staging_buffer = device.create_buffer(
83 "Distances Staging",
84 size,
85 wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
86 )?;
87
88 let mut encoder =
89 device.device().create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
90 encoder.copy_buffer_to_buffer(distances_buffer, 0, &staging_buffer, 0, size);
91 device.queue().submit(Some(encoder.finish()));
92
93 let buffer_slice = staging_buffer.slice(..);
94 let (tx, rx) = futures_intrusive::channel::shared::oneshot_channel();
95
96 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
97 let _ = tx.send(result);
98 });
99
100 device.device().poll(wgpu::Maintain::Wait);
101 rx.receive().await.context("Failed to receive map result")?.context("Buffer mapping failed")?;
102
103 let data = buffer_slice.get_mapped_range();
104 let distances: Vec<u32> = bytemuck::cast_slice(&data).to_vec();
105 drop(data);
106 staging_buffer.unmap();
107
108 Ok(distances)
109}
110
111#[allow(clippy::too_many_lines)]
142#[allow(clippy::cast_possible_truncation)]
143pub async fn gpu_bfs(
144 device: &GpuDevice,
145 buffers: &GpuCsrBuffers,
146 source: NodeId,
147) -> Result<GpuBfsResult> {
148 const SHADER: &str = include_str!("shaders/bfs_simple.wgsl");
150 let shader_module = device.device().create_shader_module(wgpu::ShaderModuleDescriptor {
151 label: Some("BFS Shader"),
152 source: wgpu::ShaderSource::Wgsl(SHADER.into()),
153 });
154
155 let bind_group_layout =
157 device.device().create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
158 label: Some("BFS Bind Group Layout"),
159 entries: &[
160 wgpu::BindGroupLayoutEntry {
162 binding: 0,
163 visibility: wgpu::ShaderStages::COMPUTE,
164 ty: wgpu::BindingType::Buffer {
165 ty: wgpu::BufferBindingType::Uniform,
166 has_dynamic_offset: false,
167 min_binding_size: None,
168 },
169 count: None,
170 },
171 wgpu::BindGroupLayoutEntry {
173 binding: 1,
174 visibility: wgpu::ShaderStages::COMPUTE,
175 ty: wgpu::BindingType::Buffer {
176 ty: wgpu::BufferBindingType::Storage { read_only: true },
177 has_dynamic_offset: false,
178 min_binding_size: None,
179 },
180 count: None,
181 },
182 wgpu::BindGroupLayoutEntry {
184 binding: 2,
185 visibility: wgpu::ShaderStages::COMPUTE,
186 ty: wgpu::BindingType::Buffer {
187 ty: wgpu::BufferBindingType::Storage { read_only: true },
188 has_dynamic_offset: false,
189 min_binding_size: None,
190 },
191 count: None,
192 },
193 wgpu::BindGroupLayoutEntry {
195 binding: 3,
196 visibility: wgpu::ShaderStages::COMPUTE,
197 ty: wgpu::BindingType::Buffer {
198 ty: wgpu::BufferBindingType::Storage { read_only: false },
199 has_dynamic_offset: false,
200 min_binding_size: None,
201 },
202 count: None,
203 },
204 wgpu::BindGroupLayoutEntry {
206 binding: 4,
207 visibility: wgpu::ShaderStages::COMPUTE,
208 ty: wgpu::BindingType::Buffer {
209 ty: wgpu::BufferBindingType::Storage { read_only: false },
210 has_dynamic_offset: false,
211 min_binding_size: None,
212 },
213 count: None,
214 },
215 ],
216 });
217
218 let pipeline_layout = device.device().create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
220 label: Some("BFS Pipeline Layout"),
221 bind_group_layouts: &[&bind_group_layout],
222 push_constant_ranges: &[],
223 });
224
225 let compute_pipeline =
226 device.device().create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
227 label: Some("BFS Pipeline"),
228 layout: Some(&pipeline_layout),
229 module: &shader_module,
230 entry_point: "bfs_level",
231 compilation_options: wgpu::PipelineCompilationOptions::default(),
232 cache: None,
233 });
234
235 let num_nodes = buffers.num_nodes();
237
238 let params_buffer = device.create_buffer_init(
240 "BFS Params",
241 bytemuck::bytes_of(&BfsParams {
242 num_nodes: num_nodes as u32,
243 current_level: 0,
244 source_node: source.0,
245 _padding: 0,
246 }),
247 wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
248 )?;
249
250 let mut initial_distances = vec![u32::MAX; num_nodes];
252 if (source.0 as usize) < num_nodes {
253 initial_distances[source.0 as usize] = 0;
254 }
255
256 let distances_buffer = device.create_buffer_init(
257 "BFS Distances",
258 bytemuck::cast_slice(&initial_distances),
259 wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
260 )?;
261
262 let updated_buffer = device.create_buffer_init(
264 "BFS Updated Flag",
265 bytemuck::bytes_of(&0u32),
266 wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::COPY_SRC,
267 )?;
268
269 let bind_group = device.device().create_bind_group(&wgpu::BindGroupDescriptor {
271 label: Some("BFS Bind Group"),
272 layout: &bind_group_layout,
273 entries: &[
274 wgpu::BindGroupEntry { binding: 0, resource: params_buffer.as_entire_binding() },
275 wgpu::BindGroupEntry { binding: 1, resource: buffers.row_offsets.as_entire_binding() },
276 wgpu::BindGroupEntry { binding: 2, resource: buffers.col_indices.as_entire_binding() },
277 wgpu::BindGroupEntry { binding: 3, resource: distances_buffer.as_entire_binding() },
278 wgpu::BindGroupEntry { binding: 4, resource: updated_buffer.as_entire_binding() },
279 ],
280 });
281
282 let workgroup_size = 256;
284 let num_workgroups = (num_nodes as u32).div_ceil(workgroup_size).max(1);
285
286 for level in 0..num_nodes {
287 device.queue().write_buffer(&updated_buffer, 0, bytemuck::bytes_of(&0u32));
289
290 device.queue().write_buffer(
292 ¶ms_buffer,
293 0,
294 bytemuck::bytes_of(&BfsParams {
295 num_nodes: num_nodes as u32,
296 current_level: level as u32,
297 source_node: source.0,
298 _padding: 0,
299 }),
300 );
301
302 let mut encoder = device.device().create_command_encoder(&wgpu::CommandEncoderDescriptor {
304 label: Some("BFS Command Encoder"),
305 });
306
307 {
308 let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
309 label: Some("BFS Compute Pass"),
310 timestamp_writes: None,
311 });
312
313 compute_pass.set_pipeline(&compute_pipeline);
314 compute_pass.set_bind_group(0, &bind_group, &[]);
315 compute_pass.dispatch_workgroups(num_workgroups, 1, 1);
316 }
317
318 device.queue().submit(Some(encoder.finish()));
320
321 device.device().poll(wgpu::Maintain::Wait);
323
324 let updated_value = read_buffer_u32(device, &updated_buffer).await?;
326
327 if updated_value == 0 {
329 break;
330 }
331 }
332
333 let distances = read_distances(device, &distances_buffer, num_nodes).await?;
335 let visited_count = distances.iter().filter(|&&d| d != u32::MAX).count();
336
337 Ok(GpuBfsResult { distances, visited_count })
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343 use crate::CsrGraph;
344
345 #[tokio::test]
346 async fn test_gpu_bfs_simple_chain() {
347 if !GpuDevice::is_gpu_available().await {
348 eprintln!("⚠️ Skipping test_gpu_bfs_simple_chain: GPU not available");
349 return;
350 }
351
352 let device = GpuDevice::new().await.unwrap();
353
354 let mut graph = CsrGraph::new();
356 graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
357 graph.add_edge(NodeId(1), NodeId(2), 1.0).unwrap();
358
359 let buffers = GpuCsrBuffers::from_csr_graph(&device, &graph).unwrap();
360 let result = gpu_bfs(&device, &buffers, NodeId(0)).await.unwrap();
361
362 assert_eq!(result.distance(NodeId(0)), Some(0));
364 assert_eq!(result.distance(NodeId(1)), Some(1));
365 assert_eq!(result.distance(NodeId(2)), Some(2));
366 }
367
368 #[tokio::test]
369 async fn test_gpu_bfs_disconnected() {
370 if !GpuDevice::is_gpu_available().await {
371 eprintln!("⚠️ Skipping test_gpu_bfs_disconnected: GPU not available");
372 return;
373 }
374
375 let device = GpuDevice::new().await.unwrap();
376
377 let mut graph = CsrGraph::new();
379 graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
380 graph.add_edge(NodeId(2), NodeId(2), 1.0).unwrap(); let buffers = GpuCsrBuffers::from_csr_graph(&device, &graph).unwrap();
383 let result = gpu_bfs(&device, &buffers, NodeId(0)).await.unwrap();
384
385 assert_eq!(result.distance(NodeId(0)), Some(0));
386 assert!(!result.is_reachable(NodeId(2))); }
388
389 #[test]
390 fn test_gpu_bfs_result_api() {
391 let result = GpuBfsResult { distances: vec![0, 1, u32::MAX], visited_count: 2 };
392
393 assert_eq!(result.distance(NodeId(0)), Some(0));
394 assert_eq!(result.distance(NodeId(1)), Some(1));
395 assert_eq!(result.distance(NodeId(2)), None); assert!(result.is_reachable(NodeId(0)));
398 assert!(result.is_reachable(NodeId(1)));
399 assert!(!result.is_reachable(NodeId(2)));
400 }
401}