Skip to main content

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