1use alloc::string::String;
14use alloc::vec::Vec;
15
16pub type GpuKernelId = u64;
18
19pub type GpuBufferId = u64;
21
22pub trait GpuBackend: core::fmt::Debug {
29 fn name(&self) -> &str;
31
32 fn compile(&mut self, name: &str, wgsl: &str) -> Result<GpuKernelId, GpuError>;
34
35 fn create_buffer(&mut self, data: &[u8]) -> Result<GpuBufferId, GpuError>;
37
38 fn create_buffer_uninit(&mut self, byte_len: usize) -> Result<GpuBufferId, GpuError>;
40
41 fn dispatch(
48 &mut self,
49 kernel: GpuKernelId,
50 buffers: &[GpuBufferId],
51 workgroups: [u32; 3],
52 ) -> Result<(), GpuError>;
53
54 fn dispatch_verified(
72 &mut self,
73 kernel: GpuKernelId,
74 buffers: &[GpuBufferId],
75 workgroups: [u32; 3],
76 threads_per_group: [u32; 3],
77 ) -> Result<(), GpuError> {
78 let _ = threads_per_group;
79 self.dispatch(kernel, buffers, workgroups)
80 }
81
82 fn read_buffer(&mut self, buffer: GpuBufferId) -> Result<Vec<u8>, GpuError>;
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct GpuError {
89 pub kind: GpuErrorKind,
90 pub message: String,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum GpuErrorKind {
96 Unavailable,
98 CompileFailed,
100 OutOfMemory,
102 DispatchFailed,
104 ReadbackFailed,
106}
107
108#[cfg(test)]
109mod tests {
110 use alloc::boxed::Box;
111
112 use super::*;
113 use crate::lower::RegModule;
114
115 #[derive(Debug, Default)]
117 struct FakeStats {
118 compiled: usize,
119 dispatches: usize,
120 }
121
122 #[derive(Debug)]
124 struct FakeBackend {
125 stats: alloc::rc::Rc<core::cell::RefCell<FakeStats>>,
126 buffers: Vec<(GpuBufferId, usize)>,
127 }
128
129 impl FakeBackend {
130 fn new(stats: alloc::rc::Rc<core::cell::RefCell<FakeStats>>) -> Self {
131 Self {
132 stats,
133 buffers: Vec::new(),
134 }
135 }
136 }
137
138 impl GpuBackend for FakeBackend {
139 fn name(&self) -> &str {
140 "fake"
141 }
142
143 fn compile(&mut self, _name: &str, _wgsl: &str) -> Result<GpuKernelId, GpuError> {
144 let mut stats = self.stats.borrow_mut();
145 stats.compiled += 1;
146 Ok(stats.compiled as u64)
147 }
148
149 fn create_buffer(&mut self, data: &[u8]) -> Result<GpuBufferId, GpuError> {
150 let id = self.buffers.len() as u64 + 1;
151 self.buffers.push((id, data.len()));
152 Ok(id)
153 }
154
155 fn create_buffer_uninit(&mut self, byte_len: usize) -> Result<GpuBufferId, GpuError> {
156 let id = self.buffers.len() as u64 + 1;
157 self.buffers.push((id, byte_len));
158 Ok(id)
159 }
160
161 fn dispatch(
162 &mut self,
163 _kernel: GpuKernelId,
164 _buffers: &[GpuBufferId],
165 _workgroups: [u32; 3],
166 ) -> Result<(), GpuError> {
167 self.stats.borrow_mut().dispatches += 1;
168 Ok(())
169 }
170
171 fn read_buffer(&mut self, buffer: GpuBufferId) -> Result<Vec<u8>, GpuError> {
172 let (_, size) = self
173 .buffers
174 .iter()
175 .find(|(id, _)| *id == buffer)
176 .expect("buffer exists");
177 Ok(alloc::vec![0xAA; *size])
178 }
179 }
180
181 fn empty_module() -> RegModule {
182 RegModule {
183 funcs: Vec::new(),
184 exports: Vec::new(),
185 imported_func_count: 0,
186 imported_funcs: Vec::new(),
187 imported_memories: Vec::new(),
188 imported_globals: Vec::new(),
189 imported_tables: Vec::new(),
190 start: None,
191 memories: Vec::new(),
192 globals: Vec::new(),
193 tables: Vec::new(),
194 elements: Vec::new(),
195 types: Vec::new(),
196 data: Vec::new(),
197 imported_memory_count: 0,
198 imported_global_count: 0,
199 imported_table_count: 0,
200 }
201 }
202
203 #[test]
204 fn store_has_no_gpu_by_default() {
205 let module = empty_module();
206 let store = crate::runtime::Store::instantiate(&module).unwrap();
207 assert!(!store.has_gpu());
208 }
209
210 #[test]
211 fn gpu_slot_accepts_and_exercises_a_backend() {
212 let module = empty_module();
213 let mut store = crate::runtime::Store::instantiate(&module).unwrap();
214 let stats = alloc::rc::Rc::new(core::cell::RefCell::new(FakeStats::default()));
215 store.set_gpu(Box::new(FakeBackend::new(stats.clone())));
216 assert!(
217 store
218 .with_gpu_mut(|gpu| gpu.name() == "fake")
219 .expect("backend installed")
220 );
221
222 let (kernel, buf_a, buf_out) = store
223 .with_gpu_mut(|gpu| {
224 let kernel = gpu.compile("vadd_f32x4", "@compute fn vadd() {}")?;
225 let buf_a = gpu.create_buffer(&[1, 2, 3, 4])?;
226 let buf_out = gpu.create_buffer_uninit(16)?;
227 Ok::<_, GpuError>((kernel, buf_a, buf_out))
228 })
229 .expect("backend installed")
230 .unwrap();
231 store
232 .with_gpu_mut(|gpu| gpu.dispatch(kernel, &[buf_a, buf_out], [1, 1, 1]))
233 .expect("backend installed")
234 .unwrap();
235 let result = store
236 .with_gpu_mut(|gpu| gpu.read_buffer(buf_out))
237 .expect("backend installed")
238 .unwrap();
239 assert_eq!(result, alloc::vec![0xAA; 16]);
240
241 store.clear_gpu();
242 assert!(!store.has_gpu());
243 }
244
245 #[test]
246 fn dispatch_verified_default_delegates_to_dispatch() {
247 let module = empty_module();
251 let mut store = crate::runtime::Store::instantiate(&module).unwrap();
252 let stats = alloc::rc::Rc::new(core::cell::RefCell::new(FakeStats::default()));
253 store.set_gpu(Box::new(FakeBackend::new(stats.clone())));
254 let (kernel, buf) = store
255 .with_gpu_mut(|gpu| {
256 let kernel = gpu.compile("k", "@compute fn k() {}")?;
257 let buf = gpu.create_buffer(&[0; 4])?;
258 Ok::<_, GpuError>((kernel, buf))
259 })
260 .expect("backend installed")
261 .unwrap();
262 store
264 .with_gpu_mut(|gpu| gpu.dispatch_verified(kernel, &[buf], [4, 1, 1], [128, 1, 1]))
265 .expect("backend installed")
266 .unwrap();
267 assert_eq!(
268 stats.borrow().dispatches,
269 1,
270 "default forwarded to dispatch"
271 );
272 }
273
274 fn memory_module() -> RegModule {
275 let mut module = empty_module();
276 module.memories.push(crate::types::MemType {
277 limits: crate::types::Limits { min: 1, max: None },
278 });
279 module
280 }
281
282 fn store_f32(store: &mut crate::runtime::Store, addr: u32, value: f32) {
284 store
285 .with_memory_mut(0, |mem| {
286 mem[addr as usize..addr as usize + 4].copy_from_slice(&value.to_le_bytes());
287 })
288 .expect("memory 0");
289 }
290
291 fn read_f32(store: &crate::runtime::Store, addr: u32) -> f32 {
292 store
293 .with_memory(0, |mem| {
294 f32::from_le_bytes(mem[addr as usize..addr as usize + 4].try_into().unwrap())
295 })
296 .expect("memory 0")
297 }
298
299 #[test]
300 fn f32_add_region_below_threshold_uses_cpu() {
301 let module = memory_module();
302 let mut store = crate::runtime::Store::instantiate(&module).unwrap();
303 store_f32(&mut store, 0, 1.5);
304 store_f32(&mut store, 64, 2.25);
305 let stats = alloc::rc::Rc::new(core::cell::RefCell::new(FakeStats::default()));
306 store.set_gpu(Box::new(FakeBackend::new(stats.clone())));
307
308 store.f32_add_region(0, 64, 128, 1).unwrap();
309
310 assert_eq!(read_f32(&store, 128), 3.75);
311 assert_eq!(
312 stats.borrow().dispatches,
313 0,
314 "below threshold must not dispatch"
315 );
316 }
317
318 #[test]
319 fn f32_add_region_above_threshold_dispatches() {
320 let module = memory_module();
321 let mut store = crate::runtime::Store::instantiate(&module).unwrap();
322 store.set_offload_threshold(4);
323 let stats = alloc::rc::Rc::new(core::cell::RefCell::new(FakeStats::default()));
324 store.set_gpu(Box::new(FakeBackend::new(stats.clone())));
325
326 store.f32_add_region(0, 256, 512, 8).unwrap();
328
329 let all_filled = store
331 .with_memory(0, |mem| mem[512..512 + 32].iter().all(|byte| *byte == 0xAA))
332 .expect("memory 0");
333 assert!(all_filled);
334 assert_eq!(stats.borrow().dispatches, 1);
335 assert_eq!(stats.borrow().compiled, 1, "kernel compiled once");
336
337 store.f32_add_region(0, 256, 512, 8).unwrap();
339 assert_eq!(stats.borrow().compiled, 1, "kernel not recompiled");
340 assert_eq!(stats.borrow().dispatches, 2);
341 }
342
343 #[test]
344 fn f32_add_region_without_gpu_uses_cpu() {
345 let module = memory_module();
346 let mut store = crate::runtime::Store::instantiate(&module).unwrap();
347 store_f32(&mut store, 0, 1.0);
348 store_f32(&mut store, 64, 2.0);
349 store.set_offload_threshold(0);
350
351 store.f32_add_region(0, 64, 128, 1).unwrap();
352 assert_eq!(read_f32(&store, 128), 3.0);
353 }
354
355 #[test]
356 fn f32_add_region_out_of_bounds_traps() {
357 let module = memory_module();
358 let mut store = crate::runtime::Store::instantiate(&module).unwrap();
359 let error = store.f32_add_region(0, 65533, 128, 1).unwrap_err();
360 assert_eq!(
361 error.kind,
362 crate::runtime::RuntimeErrorKind::Trap(
363 crate::runtime::RuntimeTrap::OutOfBoundsMemoryAccess
364 )
365 );
366 }
367}