Skip to main content

gpu/
web_cmd.rs

1// web_cmd -- command recording in the browser: CommandEncoder -> CommandBuffer
2//
3// The commands become words here and a GPUCommandEncoder in web/gpu.js, all in the one call of
4// Queue::submit (the op numbers: GpuHost.submit there): a frame costs one crossing into JavaScript, not
5// one per command, and nothing of WebGPU lives across calls.  What the API lets go on anywhere WebGPU
6// wants in a pass: the host opens a compute pass at the first dispatch and ends it before the next
7// thing that is no dispatch.  Barriers and layouts mean nothing here (WebGPU's usage scopes do that);
8// their calls stay so that the same code runs on every target.
9//
10// Every command checks what can be known here (the pipeline and pass state, bind group layouts, buffer
11// usages, ranges and alignments, row lengths of 256 bytes on copies); a failed check skips the
12// command and finish() returns it as GpuError::Invalid.  The rest is the browser's, later (web_dev.rs).
13// The level Voll's commands are Invalid.
14
15use alloc::rc::Rc;
16use alloc::vec::Vec;
17use core::marker::PhantomData;
18
19use crate::web_dev::{DevInner, Device};
20use crate::*;
21
22/// the row length WebGPU wants of copies between buffers and textures
23const COPY_ROW: u32 = 256;
24
25const OP_COPY_BUFFER: u32 = 1;
26const OP_FILL: u32 = 2;
27const OP_BUFFER_TO_TEXTURE: u32 = 3;
28const OP_TEXTURE_TO_BUFFER: u32 = 4;
29const OP_COMPUTE_PIPELINE: u32 = 5;
30const OP_BIND_GROUP: u32 = 6;
31const OP_PUSH: u32 = 7;
32const OP_DISPATCH: u32 = 8;
33const OP_DISPATCH_INDIRECT: u32 = 9;
34const OP_BEGIN_RENDER: u32 = 10;
35const OP_END_RENDER: u32 = 11;
36const OP_RENDER_PIPELINE: u32 = 12;
37const OP_VIEWPORT: u32 = 13;
38const OP_SCISSOR: u32 = 14;
39const OP_VERTEX: u32 = 15;
40const OP_INDEX: u32 = 16;
41const OP_DRAW: u32 = 17;
42const OP_DRAW_INDEXED: u32 = 18;
43const OP_DRAW_INDIRECT: u32 = 19;
44const OP_DRAW_INDEXED_INDIRECT: u32 = 20;
45
46/// records commands; finish() makes a CommandBuffer for Queue::submit
47pub struct CommandEncoder<'a> {
48    #[allow(dead_code)]
49    d: Rc<DevInner>,
50    w: Vec<u32>,
51    err: Option<GpuError>,
52    rendering: bool,
53    compute: Option<&'a ComputePipeline>,
54    render: Option<&'a RenderPipeline>,
55    // the running render pass's formats
56    colors: Vec<Format>,
57    depth: Option<Format>,
58}
59
60/// recorded commands, ready to submit (dropped unsubmitted: discarded)
61pub struct CommandBuffer<'a> {
62    pub(crate) words: Vec<u32>,
63    _p: PhantomData<&'a ()>,
64}
65
66impl Device {
67    /// a command encoder
68    pub fn encoder(&self) -> Result<CommandEncoder<'_>> {
69        self.d.check()?;
70        Ok(CommandEncoder { d: self.d.clone(), w: Vec::with_capacity(64), err: None, rendering: false, compute: None, render: None, colors: Vec::new(), depth: None })
71    }
72}
73
74fn split(v: u64) -> [u32; 2] {
75    [v as u32, (v >> 32) as u32]
76}
77
78impl<'a> CommandEncoder<'a> {
79    fn fail(&mut self, what: &'static str) {
80        if self.err.is_none() {
81            self.err = Some(GpuError::Invalid(what));
82        }
83    }
84
85    // not in a render pass (else the misuse)
86    fn outside(&mut self, what: &'static str) -> bool {
87        if self.rendering {
88            self.fail(what);
89        }
90        !self.rendering
91    }
92
93    fn inside(&mut self, what: &'static str) -> bool {
94        if !self.rendering {
95            self.fail(what);
96        }
97        self.rendering
98    }
99
100    pub fn copy_buffer(&mut self, src: &'a Buffer, src_off: u64, dst: &'a Buffer, dst_off: u64, size: u64) {
101        if !self.outside("copy_buffer in a render pass") {
102            return;
103        }
104        if src_off % 4 != 0 || dst_off % 4 != 0 || size % 4 != 0 || src_off.checked_add(size).is_none_or(|e| e > src.size())
105            || dst_off.checked_add(size).is_none_or(|e| e > dst.size()) {
106            return self.fail("copy_buffer: outside the buffers, or not in multiples of 4");
107        }
108        let (a, b, c) = (split(src_off), split(dst_off), split(size));
109        self.w.extend_from_slice(&[OP_COPY_BUFFER, src.handle(), a[0], a[1], dst.handle(), b[0], b[1], c[0], c[1]]);
110    }
111
112    /// fill size bytes at offset with a 32-bit value (both multiples of 4)
113    pub fn fill_buffer(&mut self, buf: &'a Buffer, offset: u64, size: u64, value: u32) {
114        if !self.outside("fill_buffer in a render pass") {
115            return;
116        }
117        if offset % 4 != 0 || size % 4 != 0 || offset.checked_add(size).is_none_or(|e| e > buf.size()) {
118            return self.fail("fill_buffer: outside the buffer, or not in multiples of 4");
119        }
120        let (a, b) = (split(offset), split(size));
121        self.w.extend_from_slice(&[OP_FILL, buf.handle(), a[0], a[1], b[0], b[1], value]);
122    }
123
124    // what a copy between a buffer and a texture needs of its layout: rows of 256 bytes -> bytes per row,
125    // rows per image
126    fn copy_layout(&mut self, r: &TextureRegion<'a>, l: &DataLayout) -> Option<(u32, u32)> {
127        let f = r.texture.format();
128        if f.is_depth() {
129            self.fail("copies of depth textures");
130            return None;
131        }
132        let (bw, bb) = (f.block_dim(), f.block_bytes());
133        let tight = r.size[0].div_ceil(bw) * bb;
134        let row = if l.bytes_per_row == 0 { tight } else { l.bytes_per_row };
135        let rows = r.size[1].div_ceil(bw);
136        if row < tight {
137            self.fail("a copy's row is shorter than the region");
138            return None;
139        }
140        // (WebGPU's rule; a tight row is fine only when it is a single row)
141        if row % COPY_ROW != 0 && rows.max(r.layers).max(r.size[2]) > 1 {
142            self.fail("a copy's rows must be multiples of 256 bytes in WebGPU");
143            return None;
144        }
145        if l.offset % bb as u64 != 0 {
146            self.fail("a copy's offset must be a multiple of the texel size");
147            return None;
148        }
149        let per = if l.rows_per_image == 0 { rows } else { l.rows_per_image };
150        Some((row, per))
151    }
152
153    /// buffer data -> a texture region
154    pub fn copy_buffer_to_texture(&mut self, src: &'a Buffer, layout: DataLayout, dst: TextureRegion<'a>) {
155        if !self.outside("copy_buffer_to_texture in a render pass") {
156            return;
157        }
158        let Some((row, per)) = self.copy_layout(&dst, &layout) else { return };
159        let o = split(layout.offset);
160        let (z, d) = if dst.texture.depth() > 1 { (dst.origin[2], dst.size[2].max(1)) } else { (dst.base_layer, dst.layers.max(1)) };
161        self.w.extend_from_slice(&[OP_BUFFER_TO_TEXTURE, src.handle(), o[0], o[1], row, per, dst.texture.handle(), dst.mip, dst.origin[0], dst.origin[1], z, dst.size[0], dst.size[1], d]);
162    }
163
164    /// a texture region -> buffer data
165    pub fn copy_texture_to_buffer(&mut self, src: TextureRegion<'a>, dst: &'a Buffer, layout: DataLayout) {
166        if !self.outside("copy_texture_to_buffer in a render pass") {
167            return;
168        }
169        let Some((row, per)) = self.copy_layout(&src, &layout) else { return };
170        let o = split(layout.offset);
171        let (z, d) = if src.texture.depth() > 1 { (src.origin[2], src.size[2].max(1)) } else { (src.base_layer, src.layers.max(1)) };
172        self.w.extend_from_slice(&[OP_TEXTURE_TO_BUFFER, src.texture.handle(), src.mip, src.origin[0], src.origin[1], z, src.size[0], src.size[1], d, dst.handle(), o[0], o[1], row, per]);
173    }
174
175    /// (WebGPU's usage scopes do this)
176    pub fn barrier(&mut self, _before: Access, _after: Access) {}
177    pub fn texture_barrier(&mut self, _tex: &'a Texture, _before: Layout, _after: Layout) {}
178    pub fn texture_barrier_range(&mut self, _tex: &'a Texture, _base_mip: u32, _mips: u32, _base_layer: u32, _layers: u32, _before: Layout, _after: Layout) {}
179
180    pub fn set_compute_pipeline(&mut self, p: &'a ComputePipeline) {
181        if self.outside("set_compute_pipeline in a render pass") {
182            self.compute = Some(p);
183            self.w.extend_from_slice(&[OP_COMPUTE_PIPELINE, p.0.h.h]);
184        }
185    }
186
187    /// bind group g (= @group(g)) of the current pipeline's layout
188    pub fn set_bind_group(&mut self, index: u32, group: &'a BindGroup) {
189        self.w.extend_from_slice(&[OP_BIND_GROUP, index, group.handle()]);
190        let layout = if self.rendering { self.render.map(|p| &p.0.layout) } else { self.compute.map(|p| &p.0.layout) };
191        let Some(layout) = layout else {
192            return self.fail("set_bind_group before a pipeline");
193        };
194        if !layout.0.groups.get(index as usize).is_some_and(|l| group.fits(l)) {
195            self.fail("set_bind_group: the group does not fit the pipeline's layout");
196        }
197    }
198
199    /// push constants at offset (bytes) for the current pipeline (a uniform block in the browser)
200    pub fn push_constants<T: Pod>(&mut self, offset: u32, data: &[T]) {
201        let bytes = bytes_of(data);
202        let layout = if self.rendering { self.render.map(|p| &p.0.layout) } else { self.compute.map(|p| &p.0.layout) };
203        let Some(layout) = layout else {
204            return self.fail("push_constants before a pipeline");
205        };
206        if offset % 4 != 0 || bytes.len() % 4 != 0 || (offset as usize).checked_add(bytes.len()).is_none_or(|e| e > layout.0.push as usize) {
207            return self.fail("push_constants: beyond the layout's block, or not in multiples of 4");
208        }
209        self.w.extend_from_slice(&[OP_PUSH, offset, bytes.len() as u32 / 4]);
210        self.w.extend(bytes.chunks_exact(4).map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]])));
211    }
212
213    pub fn dispatch(&mut self, x: u32, y: u32, z: u32) {
214        if !self.outside("dispatch in a render pass") {
215            return;
216        }
217        if self.compute.is_none() {
218            return self.fail("dispatch without a pipeline");
219        }
220        self.w.extend_from_slice(&[OP_DISPATCH, x, y, z]);
221    }
222
223    /// workgroup counts (3 x u32) from buf at offset
224    pub fn dispatch_indirect(&mut self, buf: &'a Buffer, offset: u64) {
225        if !self.outside("dispatch_indirect in a render pass") {
226            return;
227        }
228        if self.compute.is_none() || !buf.usage().contains(BufferUsage::INDIRECT) || offset % 4 != 0 || offset.checked_add(12).is_none_or(|e| e > buf.size()) {
229            return self.fail("dispatch_indirect: no pipeline, no INDIRECT usage, or outside the buffer");
230        }
231        let o = split(offset);
232        self.w.extend_from_slice(&[OP_DISPATCH_INDIRECT, buf.handle(), o[0], o[1]]);
233    }
234
235    pub fn begin_render(&mut self, desc: &RenderPassDesc<'_, 'a>) {
236        if !self.outside("begin_render in a render pass") {
237            return;
238        }
239        if desc.color.len() > COLOR_TARGETS_MAX || (desc.color.is_empty() && desc.depth.is_none()) {
240            return self.fail("begin_render: no attachments, or too many");
241        }
242        self.rendering = true;
243        self.render = None;
244        self.colors.clear();
245        self.colors.extend(desc.color.iter().map(|c| c.view.format()));
246        self.depth = desc.depth.as_ref().map(|d| d.view.format());
247        self.w.extend_from_slice(&[OP_BEGIN_RENDER, desc.color.len() as u32]);
248        for c in desc.color {
249            self.w.extend_from_slice(&[c.view.handle(), (c.load == Load::Keep) as u32, c.store as u32, c.clear[0].to_bits(), c.clear[1].to_bits(), c.clear[2].to_bits(), c.clear[3].to_bits()]);
250        }
251        match &desc.depth {
252            Some(d) => self.w.extend_from_slice(&[d.view.handle(), (d.load == Load::Keep) as u32, d.store as u32, d.clear.to_bits(), d.view.format().has_stencil() as u32]),
253            None => self.w.extend_from_slice(&[0; 5]),
254        }
255    }
256
257    pub fn end_render(&mut self) {
258        if self.inside("end_render outside a render pass") {
259            self.rendering = false;
260            self.render = None;
261            self.w.push(OP_END_RENDER);
262        }
263    }
264
265    pub fn set_render_pipeline(&mut self, p: &'a RenderPipeline) {
266        if self.inside("set_render_pipeline outside a render pass") {
267            if p.0.targets != self.colors || p.0.depth != self.depth {
268                return self.fail("set_render_pipeline: the pipeline's formats are not the render pass's");
269            }
270            self.render = Some(p);
271            self.w.extend_from_slice(&[OP_RENDER_PIPELINE, p.0.h.h]);
272        }
273    }
274
275    /// the viewport in pixels from the top left (depth range min ..= max)
276    pub fn set_viewport(&mut self, x: f32, y: f32, w: f32, h: f32, min_depth: f32, max_depth: f32) {
277        if self.inside("set_viewport outside a render pass") {
278            self.w.extend_from_slice(&[OP_VIEWPORT, x.to_bits(), y.to_bits(), w.to_bits(), h.to_bits(), min_depth.to_bits(), max_depth.to_bits()]);
279        }
280    }
281
282    pub fn set_scissor(&mut self, x: u32, y: u32, w: u32, h: u32) {
283        if self.inside("set_scissor outside a render pass") {
284            self.w.extend_from_slice(&[OP_SCISSOR, x, y, w, h]);
285        }
286    }
287
288    pub fn set_vertex_buffer(&mut self, slot: u32, buf: &'a Buffer, offset: u64) {
289        if !self.inside("set_vertex_buffer outside a render pass") {
290            return;
291        }
292        if !buf.usage().contains(BufferUsage::VERTEX) {
293            return self.fail("set_vertex_buffer: no VERTEX usage");
294        }
295        let o = split(offset);
296        self.w.extend_from_slice(&[OP_VERTEX, slot, buf.handle(), o[0], o[1]]);
297    }
298
299    pub fn set_index_buffer(&mut self, buf: &'a Buffer, offset: u64, format: IndexFormat) {
300        if !self.inside("set_index_buffer outside a render pass") {
301            return;
302        }
303        if !buf.usage().contains(BufferUsage::INDEX) {
304            return self.fail("set_index_buffer: no INDEX usage");
305        }
306        let o = split(offset);
307        self.w.extend_from_slice(&[OP_INDEX, buf.handle(), o[0], o[1], (format == IndexFormat::U32) as u32]);
308    }
309
310    fn drawing(&mut self) -> bool {
311        if !self.inside("a draw outside a render pass") {
312            return false;
313        }
314        if self.render.is_none() {
315            self.fail("a draw without a pipeline");
316            return false;
317        }
318        true
319    }
320
321    pub fn draw(&mut self, vertices: u32, instances: u32, first_vertex: u32, first_instance: u32) {
322        if self.drawing() {
323            self.w.extend_from_slice(&[OP_DRAW, vertices, instances, first_vertex, first_instance]);
324        }
325    }
326
327    pub fn draw_indexed(&mut self, indices: u32, instances: u32, first_index: u32, base_vertex: i32, first_instance: u32) {
328        if self.drawing() {
329            self.w.extend_from_slice(&[OP_DRAW_INDEXED, indices, instances, first_index, base_vertex as u32, first_instance]);
330        }
331    }
332
333    /// one draw: vertex count, instance count, first vertex, first instance (4 x u32)
334    pub fn draw_indirect(&mut self, buf: &'a Buffer, offset: u64) {
335        if !self.drawing() {
336            return;
337        }
338        if !buf.usage().contains(BufferUsage::INDIRECT) || offset % 4 != 0 || offset.checked_add(16).is_none_or(|e| e > buf.size()) {
339            return self.fail("draw_indirect: no INDIRECT usage, or outside the buffer");
340        }
341        let o = split(offset);
342        self.w.extend_from_slice(&[OP_DRAW_INDIRECT, buf.handle(), o[0], o[1]]);
343    }
344
345    /// one indexed draw: index count, instance count, first index, base vertex (i32), first instance
346    pub fn draw_indexed_indirect(&mut self, buf: &'a Buffer, offset: u64) {
347        if !self.drawing() {
348            return;
349        }
350        if !buf.usage().contains(BufferUsage::INDIRECT) || offset % 4 != 0 || offset.checked_add(20).is_none_or(|e| e > buf.size()) {
351            return self.fail("draw_indexed_indirect: no INDIRECT usage, or outside the buffer");
352        }
353        let o = split(offset);
354        self.w.extend_from_slice(&[OP_DRAW_INDEXED_INDIRECT, buf.handle(), o[0], o[1]]);
355    }
356
357    /// (level Voll)
358    pub fn draw_indexed_indirect_multi(&mut self, _buf: &'a Buffer, _offset: u64, _count: u32, _stride: u32) {
359        self.fail("draw_indexed_indirect_multi: level Voll");
360    }
361
362    /// (level Voll)
363    pub fn draw_indexed_indirect_count(&mut self, _buf: &'a Buffer, _offset: u64, _count_buf: &'a Buffer, _count_offset: u64, _max_count: u32, _stride: u32) {
364        self.fail("draw_indexed_indirect_count: level Voll");
365    }
366
367    /// (not built in the browser)
368    pub fn reset_timestamps(&mut self, _ts: &'a Timestamps, _first: u32, _n: u32) {
369        self.fail("timestamps in the browser");
370    }
371
372    /// (not built in the browser)
373    pub fn timestamp(&mut self, _ts: &'a Timestamps, _i: u32) {
374        self.fail("timestamps in the browser");
375    }
376
377    /// the recorded commands, or the first misuse
378    pub fn finish(mut self) -> Result<CommandBuffer<'a>> {
379        if self.rendering {
380            self.fail("finish inside a render pass");
381        }
382        if let Some(e) = self.err {
383            return Err(e);
384        }
385        Ok(CommandBuffer { words: core::mem::take(&mut self.w), _p: PhantomData })
386    }
387}