1use std::{
6 collections::HashMap,
7 sync::{
8 Arc, Mutex,
9 atomic::{AtomicU64, Ordering},
10 },
11};
12
13use wgpu;
14
15use crate::{
16 device::WebGpuDevice,
17 error::{WebGpuError, WebGpuResult},
18};
19
20pub struct WebGpuBufferInfo {
24 pub buffer: wgpu::Buffer,
26 pub size: u64,
28}
29
30pub struct WebGpuMemoryManager {
37 device: Arc<WebGpuDevice>,
38 buffers: Mutex<HashMap<u64, WebGpuBufferInfo>>,
39 next_handle: AtomicU64,
40}
41
42impl WebGpuMemoryManager {
43 pub fn new(device: Arc<WebGpuDevice>) -> Self {
45 Self {
46 device,
47 buffers: Mutex::new(HashMap::new()),
48 next_handle: AtomicU64::new(1),
49 }
50 }
51
52 pub fn alloc(&self, bytes: usize) -> WebGpuResult<u64> {
56 let size = bytes as u64;
57 let buffer = self.device.device.create_buffer(&wgpu::BufferDescriptor {
58 label: Some("oxicuda-webgpu-buffer"),
59 size,
60 usage: wgpu::BufferUsages::STORAGE
61 | wgpu::BufferUsages::COPY_SRC
62 | wgpu::BufferUsages::COPY_DST,
63 mapped_at_creation: false,
64 });
65
66 let handle = self.next_handle.fetch_add(1, Ordering::Relaxed);
67
68 self.buffers
69 .lock()
70 .map_err(|_| WebGpuError::BufferMapping("mutex poisoned".into()))?
71 .insert(handle, WebGpuBufferInfo { buffer, size });
72
73 Ok(handle)
74 }
75
76 pub fn free(&self, handle: u64) -> WebGpuResult<()> {
80 self.buffers
81 .lock()
82 .map_err(|_| WebGpuError::BufferMapping("mutex poisoned".into()))?
83 .remove(&handle);
84 Ok(())
85 }
86
87 pub fn copy_to_device(&self, handle: u64, src: &[u8]) -> WebGpuResult<()> {
89 let buffers = self
90 .buffers
91 .lock()
92 .map_err(|_| WebGpuError::BufferMapping("mutex poisoned".into()))?;
93
94 let buf_info = buffers
95 .get(&handle)
96 .ok_or_else(|| WebGpuError::InvalidArgument(format!("unknown handle {handle}")))?;
97
98 if src.len() as u64 > buf_info.size {
103 return Err(WebGpuError::InvalidArgument(format!(
104 "copy_to_device: source is {} bytes but buffer holds only {} bytes",
105 src.len(),
106 buf_info.size
107 )));
108 }
109
110 self.device.queue.write_buffer(&buf_info.buffer, 0, src);
111 Ok(())
112 }
113
114 pub(crate) fn lock_buffers(
119 &self,
120 ) -> WebGpuResult<std::sync::MutexGuard<'_, HashMap<u64, WebGpuBufferInfo>>> {
121 self.buffers
122 .lock()
123 .map_err(|_| WebGpuError::BufferMapping("mutex poisoned".into()))
124 }
125
126 pub fn copy_from_device(&self, dst: &mut [u8], handle: u64) -> WebGpuResult<()> {
131 let staging = {
135 let buffers = self
136 .buffers
137 .lock()
138 .map_err(|_| WebGpuError::BufferMapping("mutex poisoned".into()))?;
139
140 let buf_info = buffers
141 .get(&handle)
142 .ok_or_else(|| WebGpuError::InvalidArgument(format!("unknown handle {handle}")))?;
143
144 let staging = self.device.device.create_buffer(&wgpu::BufferDescriptor {
145 label: Some("oxicuda-webgpu-staging"),
146 size: buf_info.size,
147 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
148 mapped_at_creation: false,
149 });
150
151 let mut encoder =
152 self.device
153 .device
154 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
155 label: Some("oxicuda-webgpu-readback"),
156 });
157
158 encoder.copy_buffer_to_buffer(&buf_info.buffer, 0, &staging, 0, buf_info.size);
159 self.device.queue.submit(std::iter::once(encoder.finish()));
160
161 staging
162 };
164
165 let slice = staging.slice(..);
167 let (tx, rx) = std::sync::mpsc::channel();
168 slice.map_async(wgpu::MapMode::Read, move |result| {
169 let _ = tx.send(result);
171 });
172
173 let _ = self.device.device.poll(wgpu::PollType::wait_indefinitely());
176
177 rx.recv()
178 .map_err(|_| WebGpuError::BufferMapping("channel closed before map completed".into()))?
179 .map_err(|e| WebGpuError::BufferMapping(format!("{e:?}")))?;
180
181 let data = slice.get_mapped_range();
182 let data_len = data.len();
183 if dst.len() > data_len {
189 drop(data);
190 staging.unmap();
191 return Err(WebGpuError::InvalidArgument(format!(
192 "copy_from_device: destination is {} bytes but buffer holds only {data_len} bytes",
193 dst.len(),
194 )));
195 }
196 let copy_len = dst.len();
197 dst[..copy_len].copy_from_slice(&data[..copy_len]);
198 drop(data);
199 staging.unmap();
200
201 Ok(())
202 }
203}
204
205impl std::fmt::Debug for WebGpuMemoryManager {
206 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207 let count = self.buffers.lock().map(|b| b.len()).unwrap_or(0);
208 write!(f, "WebGpuMemoryManager(buffers={})", count)
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use crate::device::WebGpuDevice;
216
217 fn try_get_device() -> Option<Arc<WebGpuDevice>> {
218 WebGpuDevice::new().ok().map(Arc::new)
219 }
220
221 #[test]
222 fn alloc_and_free_requires_device() {
223 let Some(dev) = try_get_device() else {
224 return;
226 };
227 let mm = WebGpuMemoryManager::new(dev);
228 let h = mm.alloc(256).expect("alloc 256 bytes");
229 assert!(h > 0);
230 mm.free(h).expect("free");
231 mm.free(h).expect("double-free is a no-op");
233 }
234
235 #[test]
236 fn copy_roundtrip_requires_device() {
237 let Some(dev) = try_get_device() else {
238 return;
239 };
240 let mm = WebGpuMemoryManager::new(dev);
241
242 let src: Vec<u8> = (0u8..64).collect();
243 let h = mm.alloc(src.len()).expect("alloc");
244 mm.copy_to_device(h, &src).expect("copy_to_device");
245
246 let mut dst = vec![0u8; src.len()];
247 mm.copy_from_device(&mut dst, h).expect("copy_from_device");
248
249 assert_eq!(src, dst);
250 mm.free(h).expect("free");
251 }
252
253 #[test]
254 fn unknown_handle_returns_error() {
255 let Some(dev) = try_get_device() else {
256 return;
257 };
258 let mm = WebGpuMemoryManager::new(dev);
259 let err = mm.copy_to_device(9999, b"hello").unwrap_err();
260 assert!(matches!(err, WebGpuError::InvalidArgument(_)));
261 }
262
263 #[test]
264 fn copy_to_device_oversize_errors() {
265 let Some(dev) = try_get_device() else {
266 return;
267 };
268 let mm = WebGpuMemoryManager::new(dev);
269 let h = mm.alloc(16).expect("alloc 16 bytes");
270 let err = mm.copy_to_device(h, &[0u8; 64]).unwrap_err();
272 assert!(matches!(err, WebGpuError::InvalidArgument(_)));
273 mm.free(h).expect("free");
274 }
275
276 #[test]
277 fn copy_from_device_oversize_dst_errors() {
278 let Some(dev) = try_get_device() else {
279 return;
280 };
281 let mm = WebGpuMemoryManager::new(dev);
282 let h = mm.alloc(16).expect("alloc 16 bytes");
283 let mut dst = vec![0u8; 64];
286 let err = mm.copy_from_device(&mut dst, h).unwrap_err();
287 assert!(matches!(err, WebGpuError::InvalidArgument(_)));
288 mm.free(h).expect("free");
289 }
290}