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