ironaccelerator_core/compute.rs
1//! [`ComputeDevice`] — the one compute-submission surface shared by every
2//! backend that owns a device handle.
3//!
4//! [`Backend`](crate::Backend) answers *what hardware is here and what can it
5//! do*. `ComputeDevice` answers the next question — *get bytes onto it, run a
6//! shader I compiled, get bytes back* — with a single trait the Vulkan, D3D12,
7//! OpenGL, Metal, and Level Zero backends all implement. Code written against
8//! `C: ComputeDevice` runs unchanged on any of them:
9//!
10//! ```no_run
11//! use ironaccelerator_core::ComputeDevice;
12//!
13//! fn double_in_place<C: ComputeDevice>(dev: &C, code: &[u8]) -> Result<Vec<f32>, C::Error> {
14//! let input: Vec<u8> = (0..256u32).flat_map(|i| (i as f32).to_le_bytes()).collect();
15//! let buf = dev.upload(&input)?;
16//! let pipe = dev.pipeline(code, 1)?; // one storage buffer at slot 0
17//! dev.dispatch(&pipe, &[&buf], [256 / 64, 1, 1])?;
18//! let mut out = vec![0u8; input.len()];
19//! dev.download(&buf, &mut out)?;
20//! Ok(out.chunks_exact(4).map(|c| f32::from_le_bytes(c.try_into().unwrap())).collect())
21//! }
22//! ```
23//!
24//! # It stays at the driver line
25//!
26//! The trait moves bytes and launches caller-supplied bytecode. It does not
27//! compile shaders, choose work sizes, or describe workloads — those are
28//! consumer concerns, exactly as with [`Backend`](crate::Backend). The shape is
29//! deliberately the least-common-denominator of the three backends: flat
30//! storage buffers bound at consecutive slots `0..bindings`, one entry point,
31//! host-blocking submission.
32//!
33//! # Bytecode is backend-native
34//!
35//! [`pipeline`](ComputeDevice::pipeline) takes the format the backend's driver
36//! consumes — there is no translation layer here:
37//!
38//! | backend | `code` is | entry point |
39//! |---------|-----------|-------------|
40//! | Vulkan | SPIR-V (a little-endian `u32` word stream as bytes; length a multiple of 4) | `main` |
41//! | D3D12 | a signed DXIL container (`dxc -T cs_6_0 …`) | shader's own |
42//! | OpenGL | GLSL compute-shader source, UTF-8 (`#version 430`+) | `main` |
43//! | Metal | a compiled `.metallib` (`xcrun metal … && xcrun metallib …`) | first kernel (MSL reserves `main`) |
44//! | Level Zero | OpenCL/SYCL-flavored SPIR-V — `Kernel` model, pointer args (not Vulkan's `GLCompute` SPIR-V) | `main` |
45//!
46//! The `bindings` count is how many storage buffers the shader declares at
47//! slots `0..bindings` (`binding = N` in SPIR-V/GLSL, `register(uN)` in HLSL,
48//! `[[buffer(N)]]` in Metal); [`dispatch`](ComputeDevice::dispatch) binds
49//! exactly that many in order.
50//!
51//! One wrinkle: Metal and Level Zero set the group size at dispatch, where the
52//! others declare it in the shader (`local_size` / `numthreads`). The trait
53//! carries only threadgroup counts, so those two impls assume a 1-D group of
54//! 64; each exposes a native call taking an explicit size for other cases.
55//!
56//! # Why WebGPU is not here
57//!
58//! `ironaccelerator-webgpu` does not implement this trait. WebGPU's `GPUDevice`
59//! is owned by the host page and its submission API is asynchronous and
60//! JS-side; wrapping it would mean re-exporting a binding crate's types and
61//! taking on the very dependency that backend was built to avoid. There is no
62//! device handle for the crate to hang an impl on, so it stays a host-driven
63//! adapter survey.
64
65/// A device you can allocate on, submit a compute shader to, and read back
66/// from — one uniform surface over Vulkan, D3D12, and OpenGL.
67///
68/// Implemented on the backend's owned device/context type (Vulkan `Context`,
69/// D3D12 `Context`, OpenGL `GlDevice`, Metal `Context`, Level Zero `Context`).
70/// Every method blocks until the GPU has finished the operation, matching the
71/// one-shot submission model the backends already use; batching and async are
72/// concrete-type concerns layered on top.
73///
74/// The associated types keep this zero-cost — there is no boxing and no
75/// vtable. The trait is therefore not `dyn`-safe by design; use it as a generic
76/// bound (`fn run<C: ComputeDevice>(dev: &C)`), not a trait object.
77pub trait ComputeDevice {
78 /// Backend-native buffer handle (device-local storage).
79 type Buffer;
80
81 /// Backend-native compiled compute pipeline.
82 type Pipeline;
83
84 /// Backend-native error. `Debug` so generic callers can surface it; the
85 /// concrete backends carry richer error types on their inherent APIs.
86 type Error: core::fmt::Debug;
87
88 /// Allocate an uninitialized device-local buffer of `bytes`.
89 fn device_buffer(&self, bytes: u64) -> Result<Self::Buffer, Self::Error>;
90
91 /// Stage `data` into a fresh device-local buffer and wait for the copy.
92 /// The buffer is sized to `data.len()`.
93 fn upload(&self, data: &[u8]) -> Result<Self::Buffer, Self::Error>;
94
95 /// Copy a device-local buffer back to host memory, reading
96 /// `min(out.len(), len)` bytes.
97 fn download(&self, buffer: &Self::Buffer, out: &mut [u8]) -> Result<(), Self::Error>;
98
99 /// Compile `code` (see the module table for the per-backend format) into a
100 /// pipeline that binds `bindings` storage buffers at slots `0..bindings`.
101 fn pipeline(&self, code: &[u8], bindings: u32) -> Result<Self::Pipeline, Self::Error>;
102
103 /// Bind `buffers` to slots `0..buffers.len()` and dispatch a
104 /// `groups[0] × groups[1] × groups[2]` grid of workgroups, then block until
105 /// it completes. `buffers.len()` should match the pipeline's `bindings`.
106 fn dispatch(
107 &self,
108 pipeline: &Self::Pipeline,
109 buffers: &[&Self::Buffer],
110 groups: [u32; 3],
111 ) -> Result<(), Self::Error>;
112
113 /// Length of a buffer in bytes.
114 fn buffer_len(&self, buffer: &Self::Buffer) -> u64;
115}