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