1use std::collections::HashMap;
33use std::sync::atomic::{AtomicU64, Ordering};
34use std::sync::{Arc, Mutex};
35
36use oxicuda_backend::{
37 BackendResult, BackendTranspose, BinaryOp, ComputeBackend, ReduceOp, UnaryOp,
38};
39
40use crate::WebGpuBackend;
41use crate::error::{WebGpuError, WebGpuResult};
42use crate::memory::WebGpuBufferInfo;
43
44#[derive(Debug)]
51pub struct WasmGpuDevice {
52 #[allow(dead_code)]
54 pub(crate) instance: wgpu::Instance,
55 #[allow(dead_code)]
57 pub(crate) adapter: wgpu::Adapter,
58 pub(crate) device: wgpu::Device,
60 pub(crate) queue: wgpu::Queue,
62 pub adapter_name: String,
64}
65
66impl WasmGpuDevice {
67 pub async fn from_adapter(
73 instance: wgpu::Instance,
74 adapter: wgpu::Adapter,
75 ) -> WebGpuResult<Self> {
76 let adapter_name = adapter.get_info().name.clone();
77
78 let (device, queue) = adapter
79 .request_device(&wgpu::DeviceDescriptor {
80 label: Some("oxicuda-webgpu-wasm"),
81 required_features: wgpu::Features::empty(),
82 required_limits: wgpu::Limits::default(),
83 memory_hints: wgpu::MemoryHints::default(),
84 ..Default::default()
85 })
86 .await
87 .map_err(|e| WebGpuError::DeviceRequest(e.to_string()))?;
88
89 Ok(Self {
90 instance,
91 adapter,
92 device,
93 queue,
94 adapter_name,
95 })
96 }
97}
98
99pub async fn request_adapter() -> WebGpuResult<wgpu::Adapter> {
106 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
107
108 instance
109 .request_adapter(&wgpu::RequestAdapterOptions {
110 power_preference: wgpu::PowerPreference::HighPerformance,
111 compatible_surface: None,
112 force_fallback_adapter: false,
113 })
114 .await
115 .map_err(|e| WebGpuError::DeviceRequest(e.to_string()))
116}
117
118pub struct WasmMemoryManager {
126 device: Arc<WasmGpuDevice>,
127 buffers: Mutex<HashMap<u64, WebGpuBufferInfo>>,
128 next_handle: AtomicU64,
129}
130
131impl WasmMemoryManager {
132 pub fn new(device: Arc<WasmGpuDevice>) -> Self {
134 Self {
135 device,
136 buffers: Mutex::new(HashMap::new()),
137 next_handle: AtomicU64::new(1),
138 }
139 }
140
141 pub fn alloc(&self, bytes: usize) -> WebGpuResult<u64> {
143 let size = bytes as u64;
144 let buffer = self.device.device.create_buffer(&wgpu::BufferDescriptor {
145 label: Some("oxicuda-wasm-buffer"),
146 size,
147 usage: wgpu::BufferUsages::STORAGE
148 | wgpu::BufferUsages::COPY_SRC
149 | wgpu::BufferUsages::COPY_DST,
150 mapped_at_creation: false,
151 });
152
153 let handle = self.next_handle.fetch_add(1, Ordering::Relaxed);
154
155 self.buffers
156 .lock()
157 .map_err(|_| WebGpuError::BufferMapping("mutex poisoned".into()))?
158 .insert(handle, WebGpuBufferInfo { buffer, size });
159
160 Ok(handle)
161 }
162
163 pub fn free(&self, handle: u64) -> WebGpuResult<()> {
165 self.buffers
166 .lock()
167 .map_err(|_| WebGpuError::BufferMapping("mutex poisoned".into()))?
168 .remove(&handle);
169 Ok(())
170 }
171
172 pub fn copy_htod(&self, handle: u64, src: &[u8]) -> WebGpuResult<()> {
176 let buffers = self
177 .buffers
178 .lock()
179 .map_err(|_| WebGpuError::BufferMapping("mutex poisoned".into()))?;
180
181 let buf_info = buffers
182 .get(&handle)
183 .ok_or_else(|| WebGpuError::InvalidArgument(format!("unknown handle {handle}")))?;
184
185 if src.len() as u64 > buf_info.size {
189 return Err(WebGpuError::InvalidArgument(format!(
190 "copy_htod: source is {} bytes but buffer holds only {} bytes",
191 src.len(),
192 buf_info.size
193 )));
194 }
195
196 self.device.queue.write_buffer(&buf_info.buffer, 0, src);
197 Ok(())
198 }
199
200 fn submit_readback(&self, handle: u64) -> WebGpuResult<wgpu::Buffer> {
205 let buffers = self
206 .buffers
207 .lock()
208 .map_err(|_| WebGpuError::BufferMapping("mutex poisoned".into()))?;
209
210 let buf_info = buffers
211 .get(&handle)
212 .ok_or_else(|| WebGpuError::InvalidArgument(format!("unknown handle {handle}")))?;
213
214 let staging = self.device.device.create_buffer(&wgpu::BufferDescriptor {
215 label: Some("oxicuda-wasm-staging"),
216 size: buf_info.size,
217 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
218 mapped_at_creation: false,
219 });
220
221 let mut encoder =
222 self.device
223 .device
224 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
225 label: Some("oxicuda-wasm-readback"),
226 });
227
228 encoder.copy_buffer_to_buffer(&buf_info.buffer, 0, &staging, 0, buf_info.size);
229 self.device.queue.submit(std::iter::once(encoder.finish()));
230
231 Ok(staging)
232 }
233
234 fn drain_mapped(dst: &mut [u8], staging: wgpu::Buffer) -> WebGpuResult<()> {
238 let slice = staging.slice(..);
239 let data = slice.get_mapped_range();
240 let data_len = data.len();
241 if dst.len() > data_len {
242 drop(data);
243 staging.unmap();
244 return Err(WebGpuError::InvalidArgument(format!(
245 "copy_dtoh: destination is {} bytes but buffer holds only {data_len} bytes",
246 dst.len(),
247 )));
248 }
249 let copy_len = dst.len();
250 dst[..copy_len].copy_from_slice(&data[..copy_len]);
251 drop(data);
252 staging.unmap();
253 Ok(())
254 }
255
256 #[cfg(not(target_arch = "wasm32"))]
266 pub fn copy_dtoh(&self, dst: &mut [u8], handle: u64) -> WebGpuResult<()> {
267 let staging = self.submit_readback(handle)?;
268
269 let slice = staging.slice(..);
270 let (tx, rx) = std::sync::mpsc::channel();
271 slice.map_async(wgpu::MapMode::Read, move |result| {
272 let _ = tx.send(result);
273 });
274
275 let _ = self.device.device.poll(wgpu::PollType::wait_indefinitely());
276
277 rx.recv()
278 .map_err(|_| WebGpuError::BufferMapping("channel closed before map completed".into()))?
279 .map_err(|e| WebGpuError::BufferMapping(format!("{e:?}")))?;
280
281 Self::drain_mapped(dst, staging)
282 }
283
284 #[cfg(target_arch = "wasm32")]
288 pub fn copy_dtoh(&self, _dst: &mut [u8], _handle: u64) -> WebGpuResult<()> {
289 Err(WebGpuError::Unsupported(
290 "synchronous copy_dtoh would deadlock the browser event loop; \
291 use copy_dtoh_async"
292 .into(),
293 ))
294 }
295
296 pub async fn copy_dtoh_async(&self, dst: &mut [u8], handle: u64) -> WebGpuResult<()> {
304 let staging = self.submit_readback(handle)?;
305
306 {
307 let slice = staging.slice(..);
308 let state = Arc::new(Mutex::new(MapState::default()));
309 let cb_state = Arc::clone(&state);
310 slice.map_async(wgpu::MapMode::Read, move |result| {
311 let mut guard = match cb_state.lock() {
312 Ok(g) => g,
313 Err(poisoned) => poisoned.into_inner(),
314 };
315 guard.result = Some(result);
316 if let Some(waker) = guard.waker.take() {
317 waker.wake();
318 }
319 });
320
321 let _ = self.device.device.poll(wgpu::PollType::wait_indefinitely());
324
325 MapWait { state }
326 .await
327 .map_err(|e| WebGpuError::BufferMapping(format!("{e:?}")))?;
328 }
329
330 Self::drain_mapped(dst, staging)
331 }
332}
333
334#[derive(Default)]
337struct MapState {
338 result: Option<Result<(), wgpu::BufferAsyncError>>,
339 waker: Option<std::task::Waker>,
340}
341
342struct MapWait {
346 state: Arc<Mutex<MapState>>,
347}
348
349impl std::future::Future for MapWait {
350 type Output = Result<(), wgpu::BufferAsyncError>;
351
352 fn poll(
353 self: std::pin::Pin<&mut Self>,
354 cx: &mut std::task::Context<'_>,
355 ) -> std::task::Poll<Self::Output> {
356 let mut guard = match self.state.lock() {
357 Ok(g) => g,
358 Err(poisoned) => poisoned.into_inner(),
359 };
360 if let Some(result) = guard.result.take() {
361 std::task::Poll::Ready(result)
362 } else {
363 guard.waker = Some(cx.waker().clone());
364 std::task::Poll::Pending
365 }
366 }
367}
368
369impl std::fmt::Debug for WasmMemoryManager {
370 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
371 let count = self.buffers.lock().map(|b| b.len()).unwrap_or(0);
372 write!(f, "WasmMemoryManager(buffers={count})")
373 }
374}
375
376#[derive(Debug)]
392pub struct WasmBackend {
393 inner: WebGpuBackend,
394}
395
396impl WasmBackend {
397 pub fn new() -> Self {
399 Self {
400 inner: WebGpuBackend::new(),
401 }
402 }
403
404 pub async fn init_from_canvas(_canvas_id: &str) -> Result<Self, WebGpuError> {
414 let mut backend = Self::new();
419 backend
420 .inner
421 .init()
422 .map_err(|e| WebGpuError::DeviceRequest(e.to_string()))?;
423 Ok(backend)
424 }
425}
426
427impl Default for WasmBackend {
428 fn default() -> Self {
429 Self::new()
430 }
431}
432
433impl ComputeBackend for WasmBackend {
436 fn name(&self) -> &str {
437 "webgpu-wasm"
438 }
439
440 fn init(&mut self) -> BackendResult<()> {
441 self.inner.init()
442 }
443
444 fn is_initialized(&self) -> bool {
445 self.inner.is_initialized()
446 }
447
448 #[allow(clippy::too_many_arguments)]
449 fn gemm(
450 &self,
451 trans_a: BackendTranspose,
452 trans_b: BackendTranspose,
453 m: usize,
454 n: usize,
455 k: usize,
456 alpha: f64,
457 a_ptr: u64,
458 lda: usize,
459 b_ptr: u64,
460 ldb: usize,
461 beta: f64,
462 c_ptr: u64,
463 ldc: usize,
464 ) -> BackendResult<()> {
465 self.inner.gemm(
466 trans_a, trans_b, m, n, k, alpha, a_ptr, lda, b_ptr, ldb, beta, c_ptr, ldc,
467 )
468 }
469
470 #[allow(clippy::too_many_arguments)]
471 fn conv2d_forward(
472 &self,
473 input_ptr: u64,
474 input_shape: &[usize],
475 filter_ptr: u64,
476 filter_shape: &[usize],
477 output_ptr: u64,
478 output_shape: &[usize],
479 stride: &[usize],
480 padding: &[usize],
481 ) -> BackendResult<()> {
482 self.inner.conv2d_forward(
483 input_ptr,
484 input_shape,
485 filter_ptr,
486 filter_shape,
487 output_ptr,
488 output_shape,
489 stride,
490 padding,
491 )
492 }
493
494 #[allow(clippy::too_many_arguments)]
495 fn attention(
496 &self,
497 q_ptr: u64,
498 k_ptr: u64,
499 v_ptr: u64,
500 o_ptr: u64,
501 batch: usize,
502 heads: usize,
503 seq_q: usize,
504 seq_kv: usize,
505 head_dim: usize,
506 scale: f64,
507 causal: bool,
508 ) -> BackendResult<()> {
509 self.inner.attention(
510 q_ptr, k_ptr, v_ptr, o_ptr, batch, heads, seq_q, seq_kv, head_dim, scale, causal,
511 )
512 }
513
514 fn reduce(
515 &self,
516 op: ReduceOp,
517 input_ptr: u64,
518 output_ptr: u64,
519 shape: &[usize],
520 axis: usize,
521 ) -> BackendResult<()> {
522 self.inner.reduce(op, input_ptr, output_ptr, shape, axis)
523 }
524
525 fn unary(&self, op: UnaryOp, input_ptr: u64, output_ptr: u64, n: usize) -> BackendResult<()> {
526 self.inner.unary(op, input_ptr, output_ptr, n)
527 }
528
529 fn binary(
530 &self,
531 op: BinaryOp,
532 a_ptr: u64,
533 b_ptr: u64,
534 output_ptr: u64,
535 n: usize,
536 ) -> BackendResult<()> {
537 self.inner.binary(op, a_ptr, b_ptr, output_ptr, n)
538 }
539
540 fn synchronize(&self) -> BackendResult<()> {
541 self.inner.synchronize()
542 }
543
544 fn alloc(&self, bytes: usize) -> BackendResult<u64> {
545 self.inner.alloc(bytes)
546 }
547
548 fn free(&self, ptr: u64) -> BackendResult<()> {
549 self.inner.free(ptr)
550 }
551
552 fn copy_htod(&self, dst: u64, src: &[u8]) -> BackendResult<()> {
553 self.inner.copy_htod(dst, src)
554 }
555
556 fn copy_dtoh(&self, dst: &mut [u8], src: u64) -> BackendResult<()> {
557 self.inner.copy_dtoh(dst, src)
558 }
559}
560
561#[cfg(test)]
564mod tests {
565 use super::*;
566 use oxicuda_backend::BackendError;
567
568 #[test]
570 fn wasm_module_compiles() {
571 let backend = WasmBackend::new();
572 assert!(!backend.is_initialized());
573 assert_eq!(backend.name(), "webgpu-wasm");
574
575 let debug_str = format!("{backend:?}");
577 assert!(debug_str.contains("WasmBackend"));
578 }
579
580 #[test]
582 fn wasm_feature_flag_gating() {
583 let backend = WasmBackend::new();
585 let _: &dyn ComputeBackend = &backend;
586
587 let _default = WasmBackend::default();
589 }
590
591 #[test]
593 fn wasm_public_api_accessible() {
594 fn _assert_wasm_gpu_device_exists(_: &WasmGpuDevice) {}
596
597 fn _assert_wasm_memory_manager_exists(_: &WasmMemoryManager) {}
599
600 let _b = WasmBackend::new();
602 let _b2 = WasmBackend::default();
603
604 let _fn_ptr: fn() -> _ = || request_adapter();
606 }
607
608 #[test]
610 fn wasm_backend_not_initialized_guards() {
611 let b = WasmBackend::new();
612 assert_eq!(b.alloc(1024), Err(BackendError::NotInitialized));
613 assert_eq!(b.free(1), Err(BackendError::NotInitialized));
614 assert_eq!(b.copy_htod(1, b"hello"), Err(BackendError::NotInitialized));
615
616 let mut buf = [0u8; 4];
617 assert_eq!(b.copy_dtoh(&mut buf, 1), Err(BackendError::NotInitialized));
618 assert_eq!(b.synchronize(), Err(BackendError::NotInitialized));
619 }
620
621 #[test]
623 fn wasm_backend_init_graceful() {
624 let mut b = WasmBackend::new();
625 let _result = b.init();
626 }
627
628 fn try_wasm_device() -> Option<Arc<WasmGpuDevice>> {
631 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
632 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
633 power_preference: wgpu::PowerPreference::HighPerformance,
634 compatible_surface: None,
635 force_fallback_adapter: false,
636 }))
637 .ok()?;
638 let dev = pollster::block_on(WasmGpuDevice::from_adapter(instance, adapter)).ok()?;
639 Some(Arc::new(dev))
640 }
641
642 #[test]
645 fn wasm_copy_htod_oversize_errors() {
646 let Some(dev) = try_wasm_device() else {
647 return;
648 };
649 let mm = WasmMemoryManager::new(dev);
650 let h = mm.alloc(16).expect("alloc 16 bytes");
651 let err = mm.copy_htod(h, &[0u8; 64]).unwrap_err();
652 assert!(matches!(err, WebGpuError::InvalidArgument(_)));
653 mm.free(h).expect("free");
654 }
655
656 #[cfg(not(target_arch = "wasm32"))]
659 #[test]
660 fn wasm_copy_dtoh_oversize_dst_errors() {
661 let Some(dev) = try_wasm_device() else {
662 return;
663 };
664 let mm = WasmMemoryManager::new(dev);
665 let h = mm.alloc(16).expect("alloc 16 bytes");
666 let mut dst = vec![0u8; 64];
667 let err = mm.copy_dtoh(&mut dst, h).unwrap_err();
668 assert!(matches!(err, WebGpuError::InvalidArgument(_)));
669 mm.free(h).expect("free");
670 }
671}