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::error::{Error, Fallible};
29use script_bindings::interfaces::{GlobalScopeHelpers, PromiseHelpers};
30use script_bindings::reflector::{DomGlobalGeneric, Reflector, reflect_dom_object_with_wrap};
31use script_bindings::root::DomRoot;
32use servo_base::generic_channel::GenericSharedMemory;
33use webgpu_traits::{COPY_BUFFER_ALIGNMENT, TextureFormat, WebGPU, WebGPUQueue, WebGPURequest};
34
35use crate::JSTraceable;
36use crate::dom::bindings::root::Dom;
37use crate::dom::bindings::str::USVString;
38use crate::gpubuffer::GPUBuffer;
39use crate::gpucommandbuffer::GPUCommandBuffer;
40use crate::gpuconvert::{WebGPUConvert, WebGPUTryConvert};
41use crate::gpudevice::GPUDevice;
42use crate::traits::{
43    Equivalence, HtmlCanvasElementTrait, HtmlImageElementTrait, ImageBitmapTrait, ImageDataTrait,
44    OffscreenCanvasTrait, OriginIsCleanTrait, WebGPUHTMLVideoTrait, WebGPUPromise,
45    WebGPUPromiseCallbackTrait, WebGPURootedPromiseTrait,
46};
47
48#[dom_struct]
49pub struct GPUQueue<D: DomTypes> {
50    reflector_: Reflector,
51    #[ignore_malloc_size_of = "defined in webgpu"]
52    #[no_trace]
53    channel: WebGPU,
54    device: DomRefCell<Option<Dom<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: DomRefCell::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            Box::new(GPUQueue::new_inherited(channel, queue)),
79            global,
80            cx,
81            GPUQueueWrap::<D>,
82        )
83    }
84}
85
86impl<D: Equivalence> GPUQueue<D> {
87    pub(crate) fn set_device(&self, no_gc: &NoGC, device: &GPUDevice<D>) {
88        *self.device.safe_borrow_mut(no_gc) = Some(Dom::from_ref(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.borrow().as_ref().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.borrow().as_ref().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.borrow().as_ref().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) => (inner.Width(), inner.Height()),
281            GPUCopyExternalImageSource::HTMLVideoElement(inner) => (inner.Width(), inner.Height()),
282            GPUCopyExternalImageSource::HTMLCanvasElement(inner) => (inner.Width(), inner.Height()),
283            GPUCopyExternalImageSource::OffscreenCanvas(inner) => {
284                (inner.Width() as u32, inner.Height() as u32)
285            },
286        };
287        // source.origin.x + copySize.width must be ≤ the width of sourceImage.
288        if !(source_origin.x + copy_size.width <= source_image_width) {
289            return Err(Error::Operation(Some(
290                "Source origin x + copy width exceeds source image width".to_string(),
291            )));
292        }
293        // source.origin.y + copySize.height must be ≤ the height of sourceImage.
294        if !(source_origin.y + copy_size.height <= source_image_height) {
295            return Err(Error::Operation(Some(
296                "Source origin y + copy height exceeds source image height".to_string(),
297            )));
298        }
299        // copySize.depthOrArrayLayers must be ≤ 1.
300        if !(copy_size.depth_or_array_layers <= 1) {
301            return Err(Error::Operation(Some(
302                "Copy depth or array layers must be less than or equal to 1".to_string(),
303            )));
304        }
305        // 7. Let usability be ? check the usability of the image argument(source).
306        // with usable variant we also send the snapshot
307        let usable_snapshot = match source_image {
308            GPUCopyExternalImageSource::ImageBitmap(bitmap) => {
309                // If image's [[Detached]] internal slot value is set to true, then throw an "InvalidStateError" DOMException.
310                Some(bitmap.bitmap_data().clone().ok_or_else(|| {
311                    Error::InvalidState(Some("ImageBitmap is detached".to_string()))
312                })?)
313            },
314            GPUCopyExternalImageSource::ImageData(data) => {
315                // If image's [[Detached]] internal slot value is set to true, then throw an "InvalidStateError" DOMException.
316                if data.is_detached(cx) {
317                    return Err(Error::InvalidState(Some(
318                        "ImageData is detached".to_string(),
319                    )));
320                }
321                Some(data.get_snapshot(cx.no_gc()))
322            },
323            GPUCopyExternalImageSource::HTMLImageElement(inner) => {
324                if inner.is_usable()? {
325                    inner.get_raster_image_data()
326                } else {
327                    None
328                }
329            },
330            GPUCopyExternalImageSource::HTMLVideoElement(inner) => {
331                if inner.is_usable() {
332                    inner.get_current_frame_data()
333                } else {
334                    None
335                }
336            },
337            GPUCopyExternalImageSource::HTMLCanvasElement(inner) => {
338                // If image has either a horizontal dimension or a vertical dimension equal to zero, then throw an "InvalidStateError" DOMException.
339                if inner.is_valid() {
340                    inner.get_image_data()
341                } else {
342                    return Err(Error::InvalidState(Some(
343                        "Canvas has zero area".to_string(),
344                    )));
345                }
346            },
347            GPUCopyExternalImageSource::OffscreenCanvas(inner) => {
348                // If image has either a horizontal dimension or a vertical dimension equal to zero, then throw an "InvalidStateError" DOMException.
349                if inner.Width() == 0 || inner.Height() == 0 {
350                    return Err(Error::InvalidState(Some(
351                        "Canvas has zero area".to_string(),
352                    )));
353                } else {
354                    inner.get_image_data()
355                }
356            },
357        };
358        // this is out ouf spec, but we currently do not support more
359        let texture_descriptor = destination.parent.texture.wgpu_texture_descriptor();
360        let target_snapshot_format = match texture_descriptor.format {
361            TextureFormat::Bgra8Unorm | TextureFormat::Bgra8UnormSrgb => SnapshotPixelFormat::BGRA,
362            TextureFormat::Rgba8Unorm | TextureFormat::Rgba8UnormSrgb => SnapshotPixelFormat::RGBA,
363            _ => {
364                return Err(Error::Operation(Some(
365                    "Unsupported texture format for copy".to_string(),
366                )));
367            },
368        };
369        let usable_snapshot = usable_snapshot.map(|mut snapshot| {
370            if source.flipY {
371                pixels::flip_y_rgba8_image_inplace(snapshot.size(), snapshot.as_raw_bytes_mut());
372            }
373            snapshot.transform(
374                SnapshotAlphaMode::Transparent {
375                    premultiplied: destination.premultipliedAlpha,
376                },
377                target_snapshot_format,
378            );
379            snapshot.to_shared()
380        });
381        // 8. Issue the subsequent steps on the Device timeline of this.
382        if let Err(e) = self
383            .channel
384            .0
385            .send(WebGPURequest::CopyExternalImageToTexture {
386                device_id: self.device.borrow().as_ref().unwrap().id().0,
387                queue_id: self.queue.0,
388                usable_source: usable_snapshot,
389                destination: destination_tex_info,
390                dest_tex_descriptor: texture_descriptor,
391                copy_size,
392            })
393        {
394            warn!(
395                "Failed to send CopyExternalImageToTexture({:?}) ({e})",
396                destination.parent.texture.id().0
397            );
398            return Err(Error::Operation(Some(
399                "Failed to copy external image to texture".into(),
400            )));
401        }
402        Ok(())
403    }
404
405    /// <https://gpuweb.github.io/gpuweb/#dom-gpuqueue-onsubmittedworkdone>
406    fn OnSubmittedWorkDone(
407        &self,
408        cx: &mut JSContext,
409    ) -> <D::Promise as PromiseHelpers<D>>::StackRoot {
410        let global = self.global_from_reflector();
411        let promise = <D::Promise as PromiseHelpers<D>>::StackRoot::new_rooted(cx, &global);
412        let callback = promise.callback_promise_dom_manipulation_task_source(self);
413
414        if let Err(e) = self
415            .channel
416            .0
417            .send(WebGPURequest::QueueOnSubmittedWorkDone {
418                sender: callback,
419                queue_id: self.queue.0,
420            })
421        {
422            warn!("QueueOnSubmittedWorkDone failed with {e}")
423        }
424        promise
425    }
426}