1use crate::NQuin;
19use std::io::Seek;
20use std::path::Path;
21use wgpu::util::DeviceExt;
22
23pub use WebGpuIntegrator as PlatformGpuIntegrator;
27
28#[derive(Debug)]
31pub enum GpuError {
32 DirectStorageUnavailable(String),
33 WebGPUUnavailable(String),
34 ShaderCompilationFailed(String),
35 BufferAllocationFailed(String),
36 DispatchFailed(String),
37 ReadbackFailed(String),
38 InvalidOffset { offset: u64, required: u64 },
39}
40
41pub trait GpuIntegrator: Send {
48 fn integrate_simpsons_gpu(
61 &mut self,
62 file_path: &Path,
63 offset: u64,
64 size: u64,
65 step_size: f32,
66 ) -> Result<f64, GpuError>;
67
68 fn integrate_simpson_38_gpu(
70 &mut self,
71 file_path: &Path,
72 offset: u64,
73 size: u64,
74 step_size: f32,
75 ) -> Result<f64, GpuError>;
76
77 fn available_vram(&self) -> u64;
79}
80
81pub struct WebGpuIntegrator {
88 device: wgpu::Device,
89 queue: wgpu::Queue,
90 compute_pipeline: wgpu::ComputePipeline,
91 simpson_38_pipeline: wgpu::ComputePipeline,
92}
93
94impl WebGpuIntegrator {
95 pub async fn new() -> Result<Self, GpuError> {
99 let instance = wgpu::Instance::default();
100
101 let adapter = instance
102 .request_adapter(&wgpu::RequestAdapterOptions {
103 power_preference: wgpu::PowerPreference::HighPerformance,
104 force_fallback_adapter: false,
105 compatible_surface: None,
106 ..Default::default()
108 })
109 .await
110 .map_err(|e| GpuError::WebGPUUnavailable(format!("No adapter found: {e}")))?;
111
112 let (device, queue) = adapter
113 .request_device(&wgpu::DeviceDescriptor::default())
114 .await
115 .map_err(|e| GpuError::WebGPUUnavailable(format!("Device request failed: {e}")))?;
116
117 let shader_src = include_str!("../shaders/calculus.wgsl");
119 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
120 label: Some("Calculus Shader"),
121 source: wgpu::ShaderSource::Wgsl(shader_src.into()),
122 });
123
124 let compute_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
125 label: Some("Calculus Pipeline"),
126 layout: None,
127 module: &shader,
128 entry_point: Some("simpsons_integration"),
129 compilation_options: Default::default(),
130 cache: None,
131 });
132
133 let simpson_38_pipeline =
134 device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
135 label: Some("Simpson 3/8 Pipeline"),
136 layout: None,
137 module: &shader,
138 entry_point: Some("simpson_38_integration"),
139 compilation_options: Default::default(),
140 cache: None,
141 });
142
143 Ok(Self {
144 device,
145 queue,
146 compute_pipeline,
147 simpson_38_pipeline,
148 })
149 }
150
151 async fn execute_compute(&self, input_data: &[u8], step_size: f32) -> Result<f64, GpuError> {
153 if input_data.len() % core::mem::size_of::<f32>() != 0 {
154 return Err(GpuError::DispatchFailed(
155 "calculus input must contain whole f32 values".to_string(),
156 ));
157 }
158 let element_count = input_data.len() / core::mem::size_of::<f32>();
159 if element_count < 3 || element_count % 2 == 0 {
160 return Err(GpuError::DispatchFailed(
161 "Simpson 1/3 requires an odd sample count of at least three".to_string(),
162 ));
163 }
164 if !step_size.is_finite() || step_size == 0.0 {
165 return Err(GpuError::DispatchFailed(
166 "step size must be finite and non-zero".to_string(),
167 ));
168 }
169
170 let input_buffer = self
172 .device
173 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
174 label: Some("Input Buffer"),
175 contents: input_data,
176 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
177 });
178
179 let num_workgroups = (element_count + 63) / 64;
182 let output_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
183 label: Some("Workgroup Reduction Buffer"),
184 size: (num_workgroups * 4) as u64,
185 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
186 mapped_at_creation: false,
187 });
188
189 #[repr(C)]
191 #[derive(bytemuck::Pod, bytemuck::Zeroable, Copy, Clone)]
192 struct Uniforms {
193 step_size: f32,
194 total_elements: u32,
195 }
196
197 let uniforms = Uniforms {
198 step_size,
199 total_elements: element_count as u32,
200 };
201
202 let step_buffer = self
203 .device
204 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
205 label: Some("Uniforms Buffer"),
206 contents: bytemuck::cast_slice(&[uniforms]),
207 usage: wgpu::BufferUsages::UNIFORM,
208 });
209
210 let bind_group_layout = self.compute_pipeline.get_bind_group_layout(0);
212 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
213 label: Some("Calculus Bind Group"),
214 layout: &bind_group_layout,
215 entries: &[
216 wgpu::BindGroupEntry {
217 binding: 0,
218 resource: input_buffer.as_entire_binding(),
219 },
220 wgpu::BindGroupEntry {
221 binding: 1,
222 resource: output_buffer.as_entire_binding(),
223 },
224 wgpu::BindGroupEntry {
225 binding: 2,
226 resource: step_buffer.as_entire_binding(),
227 },
228 ],
229 });
230
231 let mut encoder = self
233 .device
234 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
235 label: Some("Calculus Encoder"),
236 });
237
238 {
240 let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
241 label: Some("Calculus Pass"),
242 timestamp_writes: None,
243 });
244 compute_pass.set_pipeline(&self.compute_pipeline);
245 compute_pass.set_bind_group(0, &bind_group, &[]);
246 compute_pass.dispatch_workgroups(num_workgroups as u32, 1, 1);
247 }
248
249 let staging_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
251 label: Some("Staging Buffer"),
252 size: (num_workgroups * 4) as u64,
253 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
254 mapped_at_creation: false,
255 });
256
257 encoder.copy_buffer_to_buffer(
258 &output_buffer,
259 0,
260 &staging_buffer,
261 0,
262 (num_workgroups * 4) as u64,
263 );
264
265 self.queue.submit(Some(encoder.finish()));
267
268 let buffer_slice = staging_buffer.slice(..);
270 let (sender, receiver) = futures_channel::oneshot::channel();
271 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
272 let _ = sender.send(result);
273 });
274
275 self.queue.submit(None);
276
277 let _mapping_result = receiver.await.unwrap();
278
279 let result_data = buffer_slice
281 .get_mapped_range()
282 .expect("wgpu buffer map_range failed");
283
284 let workgroup_results: &[f32] = bytemuck::cast_slice(&*result_data);
286 let mut sum = 0.0f64;
287 let mut compensation = 0.0f64;
288
289 for &partial in workgroup_results {
290 let y = partial as f64 - compensation;
291 let t = sum + y;
292 compensation = (t - sum) - y;
293 sum = t;
294 }
295
296 Ok(sum)
297 }
298
299 async fn execute_simpson_38_compute(
301 &self,
302 input_data: &[u8],
303 step_size: f32,
304 ) -> Result<f64, GpuError> {
305 if input_data.len() % core::mem::size_of::<f32>() != 0 {
306 return Err(GpuError::DispatchFailed(
307 "calculus input must contain whole f32 values".to_string(),
308 ));
309 }
310 let element_count = input_data.len() / core::mem::size_of::<f32>();
311 if element_count < 4 || (element_count - 1) % 3 != 0 {
312 return Err(GpuError::DispatchFailed(
313 "Simpson 3/8 requires a positive panel count divisible by three".to_string(),
314 ));
315 }
316 if !step_size.is_finite() || step_size == 0.0 {
317 return Err(GpuError::DispatchFailed(
318 "step size must be finite and non-zero".to_string(),
319 ));
320 }
321
322 let input_buffer = self
323 .device
324 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
325 label: Some("Simpson 3/8 Input Buffer"),
326 contents: input_data,
327 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
328 });
329
330 let num_workgroups = (element_count + 63) / 64;
331 let output_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
332 label: Some("Simpson 3/8 Reduction Buffer"),
333 size: (num_workgroups * 4) as u64,
334 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
335 mapped_at_creation: false,
336 });
337
338 #[repr(C)]
339 #[derive(bytemuck::Pod, bytemuck::Zeroable, Copy, Clone)]
340 struct Uniforms {
341 step_size: f32,
342 total_elements: u32,
343 }
344
345 let uniforms = Uniforms {
346 step_size,
347 total_elements: element_count as u32,
348 };
349 let step_buffer = self
350 .device
351 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
352 label: Some("Simpson 3/8 Uniforms"),
353 contents: bytemuck::cast_slice(&[uniforms]),
354 usage: wgpu::BufferUsages::UNIFORM,
355 });
356
357 let bgl = self.simpson_38_pipeline.get_bind_group_layout(0);
358 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
359 label: Some("Simpson 3/8 Bind Group"),
360 layout: &bgl,
361 entries: &[
362 wgpu::BindGroupEntry {
363 binding: 0,
364 resource: input_buffer.as_entire_binding(),
365 },
366 wgpu::BindGroupEntry {
367 binding: 1,
368 resource: output_buffer.as_entire_binding(),
369 },
370 wgpu::BindGroupEntry {
371 binding: 2,
372 resource: step_buffer.as_entire_binding(),
373 },
374 ],
375 });
376
377 let mut encoder = self
378 .device
379 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
380 label: Some("Simpson 3/8 Encoder"),
381 });
382 {
383 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
384 label: Some("Simpson 3/8 Pass"),
385 timestamp_writes: None,
386 });
387 pass.set_pipeline(&self.simpson_38_pipeline);
388 pass.set_bind_group(0, &bind_group, &[]);
389 pass.dispatch_workgroups(num_workgroups as u32, 1, 1);
390 }
391
392 let staging = self.device.create_buffer(&wgpu::BufferDescriptor {
393 label: Some("Simpson 3/8 Staging"),
394 size: (num_workgroups * 4) as u64,
395 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
396 mapped_at_creation: false,
397 });
398 encoder.copy_buffer_to_buffer(&output_buffer, 0, &staging, 0, (num_workgroups * 4) as u64);
399 self.queue.submit(Some(encoder.finish()));
400
401 let slice = staging.slice(..);
402 let (tx, rx) = futures_channel::oneshot::channel();
403 slice.map_async(wgpu::MapMode::Read, move |r| {
404 let _ = tx.send(r);
405 });
406 self.queue.submit(None);
407 let _ = rx.await.unwrap();
408
409 let mapped = slice
410 .get_mapped_range()
411 .expect("wgpu buffer map_range failed");
412 let partials: &[f32] = bytemuck::cast_slice(&*mapped);
413
414 let mut sum = 0.0f64;
416 let mut comp = 0.0f64;
417 for &p in partials {
418 let y = p as f64 - comp;
419 let t = sum + y;
420 comp = (t - sum) - y;
421 sum = t;
422 }
423 Ok(sum)
424 }
425}
426
427#[async_trait::async_trait]
428impl GpuIntegrator for WebGpuIntegrator {
429 fn integrate_simpsons_gpu(
430 &mut self,
431 file_path: &Path,
432 offset: u64,
433 size: u64,
434 step_size: f32,
435 ) -> Result<f64, GpuError> {
436 let mut file = std::fs::File::open(file_path)
438 .map_err(|e| GpuError::WebGPUUnavailable(format!("File open failed: {e}")))?;
439
440 use std::io::Read;
441 file.seek(std::io::SeekFrom::Start(offset))
442 .map_err(|e| GpuError::WebGPUUnavailable(format!("Seek failed: {e}")))?;
443
444 let mut buffer = vec![0u8; size as usize];
445 file.read_exact(&mut buffer)
446 .map_err(|e| GpuError::WebGPUUnavailable(format!("Read failed: {e}")))?;
447
448 let result = self.execute_compute(&buffer, step_size);
452
453 let handle = tokio::runtime::Handle::try_current()
456 .map_err(|e| GpuError::WebGPUUnavailable(format!("Tokio handle failed: {e}")))?;
457 handle.block_on(result)
458 }
459
460 fn integrate_simpson_38_gpu(
461 &mut self,
462 file_path: &Path,
463 offset: u64,
464 size: u64,
465 step_size: f32,
466 ) -> Result<f64, GpuError> {
467 let mut file = std::fs::File::open(file_path)
469 .map_err(|e| GpuError::WebGPUUnavailable(format!("File open failed: {e}")))?;
470
471 use std::io::Read;
472 file.seek(std::io::SeekFrom::Start(offset))
473 .map_err(|e| GpuError::WebGPUUnavailable(format!("Seek failed: {e}")))?;
474
475 let mut buffer = vec![0u8; size as usize];
476 file.read_exact(&mut buffer)
477 .map_err(|e| GpuError::WebGPUUnavailable(format!("Read failed: {e}")))?;
478
479 let handle = tokio::runtime::Handle::try_current()
481 .map_err(|e| GpuError::WebGPUUnavailable(format!("Tokio handle failed: {e}")))?;
482 handle.block_on(self.execute_simpson_38_compute(&buffer, step_size))
483 }
484
485 fn available_vram(&self) -> u64 {
486 self.device.limits().max_buffer_size
490 }
491}
492
493pub fn pack_gpu_result_into_quin(quin: &mut NQuin, result: f64) {
500 quin.metadata = f64::to_bits(result);
501}
502
503pub fn extract_gpu_result_from_quin(quin: &NQuin) -> f64 {
505 f64::from_bits(quin.metadata)
506}
507
508pub fn create_gpu_job_quin(job_id: u64, opcode: u8, file_offset: u64, step_size: f32) -> NQuin {
513 let mut quin = NQuin::default();
514 quin.subject = job_id;
515 quin.predicate = (opcode as u64) | (q_hash("calculus:gpu") << 8);
516 quin.object = file_offset;
517 quin.context = f32::to_bits(step_size) as u64;
518 quin.metadata = 0; quin.parity = 0; quin
521}
522
523const fn q_hash(s: &str) -> u64 {
528 let mut hash: u64 = 0xcbf29ce484222325;
529 let bytes = s.as_bytes();
530 let mut i = 0;
531 while i < bytes.len() {
532 hash = hash ^ (bytes[i] as u64);
533 hash = hash.wrapping_mul(0x100000001b3);
534 i += 1;
535 }
536 hash & 0x0FFF_FFFF_FFFF_FFFF
537}
538
539#[cfg(test)]
542mod tests {
543 use super::*;
544
545 #[test]
546 fn test_gpu_result_packing() {
547 let result = 42.5f64;
548 let mut quin = NQuin::default();
549
550 pack_gpu_result_into_quin(&mut quin, result);
551
552 let extracted = extract_gpu_result_from_quin(&quin);
553 assert_eq!(extracted, result);
554 }
555
556 #[test]
557 fn test_gpu_job_quin_creation() {
558 let quin = create_gpu_job_quin(
559 12345, 0x50, 4096, 0.001,
561 );
562
563 assert_eq!(quin.subject, 12345);
564 assert_eq!(quin.object, 4096);
565 assert_eq!(f32::from_bits(quin.context as u32), 0.001);
566 }
567
568 #[cfg(feature = "wgsl-forge")]
569 #[test]
570 fn calculus_shader_is_portable_and_semantically_named() {
571 let source = include_str!("../shaders/calculus.wgsl");
572 crate::wgsl_forge::validate_wgsl(source).expect("calculus WGSL must Naga-validate");
573 assert!(!source.contains("array<f64>"));
574 assert!(!source.contains(": f64"));
575 assert!(!source.contains("rk4_step"));
576 assert!(source.contains("simpson_38_integration"));
577 }
578}