Skip to main content

script/dom/webgpu/
gpuqueue.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::rc::Rc;
6
7use dom_struct::dom_struct;
8use js::context::{JSContext, NoGC};
9use pixels::{SnapshotAlphaMode, SnapshotPixelFormat};
10use script_bindings::cell::DomRefCell;
11use script_bindings::codegen::GenericBindings::CanvasRenderingContext2DBinding::ImageDataMethods;
12use script_bindings::codegen::GenericBindings::HTMLCanvasElementBinding::HTMLCanvasElementMethods;
13use script_bindings::codegen::GenericBindings::HTMLImageElementBinding::HTMLImageElementMethods;
14use script_bindings::codegen::GenericBindings::HTMLVideoElementBinding::HTMLVideoElementMethods;
15use script_bindings::codegen::GenericBindings::ImageBitmapBinding::ImageBitmapMethods;
16use script_bindings::codegen::GenericBindings::OffscreenCanvasBinding::OffscreenCanvasMethods;
17use script_bindings::reflector::{Reflector, reflect_dom_object_with_cx};
18use servo_base::generic_channel::GenericSharedMemory;
19use webgpu_traits::{WebGPU, WebGPUQueue, WebGPURequest};
20
21use crate::conversions::{Convert, TryConvert};
22use crate::dom::bindings::codegen::Bindings::WebGPUBinding::{
23    GPUCopyExternalImageDestInfo, GPUCopyExternalImageSourceInfo, GPUExtent3D, GPUQueueMethods,
24    GPUSize64, GPUTexelCopyBufferLayout, GPUTexelCopyTextureInfo,
25};
26use crate::dom::bindings::codegen::UnionTypes::{
27    ArrayBufferViewOrArrayBuffer as BufferSource,
28    ImageBitmapOrImageDataOrHTMLImageElementOrHTMLVideoElementOrHTMLCanvasElementOrOffscreenCanvas as GPUCopyExternalImageSource,
29};
30use crate::dom::bindings::error::{Error, Fallible};
31use crate::dom::bindings::reflector::DomGlobal;
32use crate::dom::bindings::root::{Dom, DomRoot};
33use crate::dom::bindings::str::USVString;
34use crate::dom::globalscope::GlobalScope;
35use crate::dom::promise::Promise;
36use crate::dom::webgpu::gpubuffer::GPUBuffer;
37use crate::dom::webgpu::gpucommandbuffer::GPUCommandBuffer;
38use crate::dom::webgpu::gpudevice::GPUDevice;
39use crate::routed_promise::{RoutedPromiseListener, callback_promise};
40
41#[dom_struct]
42pub(crate) struct GPUQueue {
43    reflector_: Reflector,
44    #[ignore_malloc_size_of = "defined in webgpu"]
45    #[no_trace]
46    channel: WebGPU,
47    device: DomRefCell<Option<Dom<GPUDevice>>>,
48    label: DomRefCell<USVString>,
49    #[no_trace]
50    queue: WebGPUQueue,
51}
52
53impl GPUQueue {
54    fn new_inherited(channel: WebGPU, queue: WebGPUQueue) -> Self {
55        GPUQueue {
56            channel,
57            reflector_: Reflector::new(),
58            device: DomRefCell::new(None),
59            label: DomRefCell::new(USVString::default()),
60            queue,
61        }
62    }
63
64    pub(crate) fn new(
65        cx: &mut JSContext,
66        global: &GlobalScope,
67        channel: WebGPU,
68        queue: WebGPUQueue,
69    ) -> DomRoot<Self> {
70        reflect_dom_object_with_cx(
71            Box::new(GPUQueue::new_inherited(channel, queue)),
72            global,
73            cx,
74        )
75    }
76}
77
78impl GPUQueue {
79    pub(crate) fn set_device(&self, no_gc: &NoGC, device: &GPUDevice) {
80        *self.device.safe_borrow_mut(no_gc) = Some(Dom::from_ref(device));
81    }
82
83    pub(crate) fn id(&self) -> WebGPUQueue {
84        self.queue
85    }
86}
87
88impl GPUQueueMethods<crate::DomTypeHolder> for GPUQueue {
89    /// <https://gpuweb.github.io/gpuweb/#dom-gpuobjectbase-label>
90    fn Label(&self) -> USVString {
91        self.label.borrow().clone()
92    }
93
94    /// <https://gpuweb.github.io/gpuweb/#dom-gpuobjectbase-label>
95    fn SetLabel(&self, no_gc: &NoGC, value: USVString) {
96        *self.label.safe_borrow_mut(no_gc) = value;
97    }
98
99    /// <https://gpuweb.github.io/gpuweb/#dom-gpuqueue-submit>
100    fn Submit(&self, command_buffers: Vec<DomRoot<GPUCommandBuffer>>) {
101        let command_buffers = command_buffers.iter().map(|cb| cb.id().0).collect();
102        self.channel
103            .0
104            .send(WebGPURequest::Submit {
105                device_id: self.device.borrow().as_ref().unwrap().id().0,
106                queue_id: self.queue.0,
107                command_buffers,
108            })
109            .unwrap();
110    }
111
112    /// <https://gpuweb.github.io/gpuweb/#dom-gpuqueue-writebuffer>
113    #[expect(unsafe_code)]
114    fn WriteBuffer(
115        &self,
116        buffer: &GPUBuffer,
117        buffer_offset: GPUSize64,
118        data: BufferSource,
119        data_offset: GPUSize64,
120        size: Option<GPUSize64>,
121    ) -> Fallible<()> {
122        // Step 1
123        let (sizeof_element, data_len): (usize, usize) = match &data {
124            BufferSource::ArrayBufferView(d) => {
125                (d.get_array_type().byte_size().unwrap_or(1), d.len())
126            },
127            BufferSource::ArrayBuffer(d) => (1, d.len()),
128        };
129        // Step 2
130        let data_size: usize = data_len / sizeof_element;
131        debug_assert_eq!(data_len % sizeof_element, 0);
132        // Step 3
133        let content_size = if let Some(s) = size {
134            s
135        } else {
136            (data_size as GPUSize64)
137                .checked_sub(data_offset)
138                .ok_or(Error::Operation(None))?
139        };
140
141        // Step 4
142        let valid = data_offset + content_size <= data_size as u64 &&
143            (content_size * sizeof_element as u64)
144                .is_multiple_of(wgpu_types::COPY_BUFFER_ALIGNMENT);
145        if !valid {
146            return Err(Error::Operation(None));
147        }
148
149        // Step 5&6
150        let byte_start = (data_offset as usize) * sizeof_element;
151        let byte_end = ((data_offset + content_size) as usize) * sizeof_element;
152        let contents = match &data {
153            BufferSource::ArrayBufferView(data) => {
154                // SAFETY: The subslice is immediately copied into GenericSharedMemory,
155                // hence there is no opportunity for the slice to invalidated.
156                GenericSharedMemory::from_bytes(unsafe { &data.as_slice()[byte_start..byte_end] })
157            },
158            BufferSource::ArrayBuffer(data) => {
159                // SAFETY: The subslice is immediately copied into GenericSharedMemory,
160                // hence there is no opportunity for the slice to invalidated.
161                GenericSharedMemory::from_bytes(unsafe { &data.as_slice()[byte_start..byte_end] })
162            },
163        };
164        if let Err(e) = self.channel.0.send(WebGPURequest::WriteBuffer {
165            device_id: self.device.borrow().as_ref().unwrap().id().0,
166            queue_id: self.queue.0,
167            buffer_id: buffer.id().0,
168            buffer_offset,
169            data: contents,
170        }) {
171            warn!("Failed to send WriteBuffer({:?}) ({})", buffer.id(), e);
172            return Err(Error::Operation(None));
173        }
174
175        Ok(())
176    }
177
178    /// <https://gpuweb.github.io/gpuweb/#dom-gpuqueue-writetexture>
179    fn WriteTexture(
180        &self,
181        destination: &GPUTexelCopyTextureInfo,
182        data: BufferSource,
183        data_layout: &GPUTexelCopyBufferLayout,
184        size: GPUExtent3D,
185    ) -> Fallible<()> {
186        let (bytes, len) = match data {
187            BufferSource::ArrayBufferView(d) => (d.to_vec(), d.len() as u64),
188            BufferSource::ArrayBuffer(d) => (d.to_vec(), d.len() as u64),
189        };
190        let valid = data_layout.offset <= len;
191
192        if !valid {
193            return Err(Error::Operation(None));
194        }
195
196        let texture_cv = destination.try_convert()?;
197        let texture_layout = data_layout.convert();
198        let write_size = (&size).try_convert()?;
199        let final_data = GenericSharedMemory::from_bytes(&bytes);
200
201        if let Err(e) = self.channel.0.send(WebGPURequest::WriteTexture {
202            device_id: self.device.borrow().as_ref().unwrap().id().0,
203            queue_id: self.queue.0,
204            texture_cv,
205            data_layout: texture_layout,
206            size: write_size,
207            data: final_data,
208        }) {
209            warn!(
210                "Failed to send WriteTexture({:?}) ({})",
211                destination.texture.id().0,
212                e
213            );
214            return Err(Error::Operation(None));
215        }
216
217        Ok(())
218    }
219
220    #[expect(
221        clippy::nonminimal_bool,
222        reason = "Following the spec steps more closely"
223    )]
224    /// <https://gpuweb.github.io/gpuweb/#dom-gpuqueue-copyexternalimagetotexture>
225    fn CopyExternalImageToTexture(
226        &self,
227        cx: &mut JSContext,
228        source: &GPUCopyExternalImageSourceInfo,
229        destination: &GPUCopyExternalImageDestInfo,
230        copy_size: GPUExtent3D,
231    ) -> Fallible<()> {
232        // 1. ? validate GPUOrigin2D shape(source.origin).
233        let source_origin = source.origin.try_convert()?;
234        // 2. ? validate GPUOrigin3D shape(destination.origin).
235        let destination_tex_info = destination.parent.try_convert()?;
236        // 3. ? validate GPUExtent3D shape(copySize).
237        let copy_size = copy_size.try_convert()?;
238        // 4. Let sourceImage be source.source.
239        let source_image = &source.source;
240        // 5. If sourceImage is not origin-clean, throw a SecurityError and return.
241        let is_origin_clean = match source_image {
242            GPUCopyExternalImageSource::ImageBitmap(inner) => inner.origin_is_clean(),
243            GPUCopyExternalImageSource::ImageData(_) => true,
244            GPUCopyExternalImageSource::HTMLImageElement(inner) => {
245                inner.same_origin(&GlobalScope::entry().origin())
246            },
247            GPUCopyExternalImageSource::HTMLVideoElement(inner) => inner.origin_is_clean(),
248            GPUCopyExternalImageSource::HTMLCanvasElement(inner) => inner.origin_is_clean(),
249            GPUCopyExternalImageSource::OffscreenCanvas(inner) => inner.origin_is_clean(),
250        };
251        if !is_origin_clean {
252            return Err(Error::Security(Some(
253                "Image source is not origin clean!".to_string(),
254            )));
255        }
256        // 6. If any of the following requirements are unmet, throw an OperationError and return.
257        let (source_image_width, source_image_height) = match source_image {
258            GPUCopyExternalImageSource::ImageBitmap(inner) => (inner.Width(), inner.Height()),
259            GPUCopyExternalImageSource::ImageData(inner) => (inner.Width(), inner.Height()),
260            GPUCopyExternalImageSource::HTMLImageElement(inner) => (inner.Width(), inner.Height()),
261            GPUCopyExternalImageSource::HTMLVideoElement(inner) => (inner.Width(), inner.Height()),
262            GPUCopyExternalImageSource::HTMLCanvasElement(inner) => (inner.Width(), inner.Height()),
263            GPUCopyExternalImageSource::OffscreenCanvas(inner) => {
264                (inner.Width() as u32, inner.Height() as u32)
265            },
266        };
267        // source.origin.x + copySize.width must be ≤ the width of sourceImage.
268        if !(source_origin.x + copy_size.width <= source_image_width) {
269            return Err(Error::Operation(Some(
270                "Source origin x + copy width exceeds source image width".to_string(),
271            )));
272        }
273        // source.origin.y + copySize.height must be ≤ the height of sourceImage.
274        if !(source_origin.y + copy_size.height <= source_image_height) {
275            return Err(Error::Operation(Some(
276                "Source origin y + copy height exceeds source image height".to_string(),
277            )));
278        }
279        // copySize.depthOrArrayLayers must be ≤ 1.
280        if !(copy_size.depth_or_array_layers <= 1) {
281            return Err(Error::Operation(Some(
282                "Copy depth or array layers must be less than or equal to 1".to_string(),
283            )));
284        }
285        // 7. Let usability be ? check the usability of the image argument(source).
286        // with usable variant we also send the snapshot
287        let usable_snapshot = match source_image {
288            GPUCopyExternalImageSource::ImageBitmap(bitmap) => {
289                // If image's [[Detached]] internal slot value is set to true, then throw an "InvalidStateError" DOMException.
290                Some(bitmap.bitmap_data().clone().ok_or_else(|| {
291                    Error::InvalidState(Some("ImageBitmap is detached".to_string()))
292                })?)
293            },
294            GPUCopyExternalImageSource::ImageData(data) => {
295                // If image's [[Detached]] internal slot value is set to true, then throw an "InvalidStateError" DOMException.
296                if data.is_detached(cx) {
297                    return Err(Error::InvalidState(Some(
298                        "ImageData is detached".to_string(),
299                    )));
300                }
301                Some(data.get_snapshot())
302            },
303            GPUCopyExternalImageSource::HTMLImageElement(inner) => {
304                if inner.is_usable()? {
305                    inner.get_raster_image_data()
306                } else {
307                    None
308                }
309            },
310            GPUCopyExternalImageSource::HTMLVideoElement(inner) => {
311                if inner.is_usable() {
312                    inner.get_current_frame_data()
313                } else {
314                    None
315                }
316            },
317            GPUCopyExternalImageSource::HTMLCanvasElement(inner) => {
318                // If image has either a horizontal dimension or a vertical dimension equal to zero, then throw an "InvalidStateError" DOMException.
319                if inner.is_valid() {
320                    inner.get_image_data()
321                } else {
322                    return Err(Error::InvalidState(Some(
323                        "Canvas has zero area".to_string(),
324                    )));
325                }
326            },
327            GPUCopyExternalImageSource::OffscreenCanvas(inner) => {
328                // If image has either a horizontal dimension or a vertical dimension equal to zero, then throw an "InvalidStateError" DOMException.
329                if inner.Width() == 0 || inner.Height() == 0 {
330                    return Err(Error::InvalidState(Some(
331                        "Canvas has zero area".to_string(),
332                    )));
333                } else {
334                    inner.get_image_data()
335                }
336            },
337        };
338        // this is out ouf spec, but we currently do not support more
339        let texture_descriptor = destination.parent.texture.wgpu_texture_descriptor();
340        let target_snapshot_format =
341            match texture_descriptor.format {
342                wgpu_types::TextureFormat::Bgra8Unorm |
343                wgpu_types::TextureFormat::Bgra8UnormSrgb => SnapshotPixelFormat::BGRA,
344                wgpu_types::TextureFormat::Rgba8Unorm |
345                wgpu_types::TextureFormat::Rgba8UnormSrgb => SnapshotPixelFormat::RGBA,
346                _ => {
347                    return Err(Error::Operation(Some(
348                        "Unsupported texture format for copy".to_string(),
349                    )));
350                },
351            };
352        let usable_snapshot = usable_snapshot.map(|mut snapshot| {
353            if source.flipY {
354                pixels::flip_y_rgba8_image_inplace(snapshot.size(), snapshot.as_raw_bytes_mut());
355            }
356            snapshot.transform(
357                SnapshotAlphaMode::Transparent {
358                    premultiplied: destination.premultipliedAlpha,
359                },
360                target_snapshot_format,
361            );
362            snapshot.to_shared()
363        });
364        // 8. Issue the subsequent steps on the Device timeline of this.
365        if let Err(e) = self
366            .channel
367            .0
368            .send(WebGPURequest::CopyExternalImageToTexture {
369                device_id: self.device.borrow().as_ref().unwrap().id().0,
370                queue_id: self.queue.0,
371                usable_source: usable_snapshot,
372                destination: destination_tex_info,
373                dest_tex_descriptor: texture_descriptor,
374                copy_size,
375            })
376        {
377            warn!(
378                "Failed to send CopyExternalImageToTexture({:?}) ({e})",
379                destination.parent.texture.id().0
380            );
381            return Err(Error::Operation(None));
382        }
383        Ok(())
384    }
385
386    /// <https://gpuweb.github.io/gpuweb/#dom-gpuqueue-onsubmittedworkdone>
387    fn OnSubmittedWorkDone(&self, cx: &mut JSContext) -> Rc<Promise> {
388        let global = self.global();
389        let promise = Promise::new(cx, &global);
390        let task_manager = global.task_manager();
391        let task_source = task_manager.dom_manipulation_task_source();
392        let callback = callback_promise(&promise, self, task_source);
393
394        if let Err(e) = self
395            .channel
396            .0
397            .send(WebGPURequest::QueueOnSubmittedWorkDone {
398                sender: callback,
399                queue_id: self.queue.0,
400            })
401        {
402            warn!("QueueOnSubmittedWorkDone failed with {e}")
403        }
404        promise
405    }
406}
407
408impl RoutedPromiseListener<()> for GPUQueue {
409    fn handle_response(
410        &self,
411        cx: &mut js::context::JSContext,
412        _response: (),
413        promise: &Rc<Promise>,
414    ) {
415        promise.resolve_native(cx, &());
416    }
417}