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::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_with_cx(
72            Box::new(GPUQueue::new_inherited(channel, queue)),
73            global,
74            cx,
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(None))?
140        };
141
142        // Step 4
143        let valid = data_offset + content_size <= data_size as u64 &&
144            (content_size * sizeof_element as u64)
145                .is_multiple_of(wgpu_types::COPY_BUFFER_ALIGNMENT);
146        if !valid {
147            return Err(Error::Operation(None));
148        }
149
150        // Step 5&6
151        let byte_start = (data_offset as usize) * sizeof_element;
152        let byte_end = ((data_offset + content_size) as usize) * sizeof_element;
153        let contents = GenericSharedMemory::from_bytes(
154            &get_buffer_source_slice(&data, cx.no_gc())[byte_start..byte_end],
155        );
156        if let Err(e) = self.channel.0.send(WebGPURequest::WriteBuffer {
157            device_id: self.device.borrow().as_ref().unwrap().id().0,
158            queue_id: self.queue.0,
159            buffer_id: buffer.id().0,
160            buffer_offset,
161            data: contents,
162        }) {
163            warn!("Failed to send WriteBuffer({:?}) ({})", buffer.id(), e);
164            return Err(Error::Operation(None));
165        }
166
167        Ok(())
168    }
169
170    /// <https://gpuweb.github.io/gpuweb/#dom-gpuqueue-writetexture>
171    fn WriteTexture(
172        &self,
173        cx: &mut JSContext,
174        destination: &GPUTexelCopyTextureInfo,
175        data: BufferSource,
176        data_layout: &GPUTexelCopyBufferLayout,
177        size: GPUExtent3D,
178    ) -> Fallible<()> {
179        let bytes = get_buffer_source_slice(&data, cx.no_gc());
180        let len = bytes.len() as u64;
181        let valid = data_layout.offset <= len;
182
183        if !valid {
184            return Err(Error::Operation(None));
185        }
186
187        let texture_cv = destination.try_convert()?;
188        let texture_layout = data_layout.convert();
189        let write_size = (&size).try_convert()?;
190        let final_data = GenericSharedMemory::from_bytes(bytes);
191
192        if let Err(e) = self.channel.0.send(WebGPURequest::WriteTexture {
193            device_id: self.device.borrow().as_ref().unwrap().id().0,
194            queue_id: self.queue.0,
195            texture_cv,
196            data_layout: texture_layout,
197            size: write_size,
198            data: final_data,
199        }) {
200            warn!(
201                "Failed to send WriteTexture({:?}) ({})",
202                destination.texture.id().0,
203                e
204            );
205            return Err(Error::Operation(None));
206        }
207
208        Ok(())
209    }
210
211    #[expect(
212        clippy::nonminimal_bool,
213        reason = "Following the spec steps more closely"
214    )]
215    /// <https://gpuweb.github.io/gpuweb/#dom-gpuqueue-copyexternalimagetotexture>
216    fn CopyExternalImageToTexture(
217        &self,
218        cx: &mut JSContext,
219        source: &GPUCopyExternalImageSourceInfo,
220        destination: &GPUCopyExternalImageDestInfo,
221        copy_size: GPUExtent3D,
222    ) -> Fallible<()> {
223        // 1. ? validate GPUOrigin2D shape(source.origin).
224        let source_origin = source.origin.try_convert()?;
225        // 2. ? validate GPUOrigin3D shape(destination.origin).
226        let destination_tex_info = destination.parent.try_convert()?;
227        // 3. ? validate GPUExtent3D shape(copySize).
228        let copy_size = copy_size.try_convert()?;
229        // 4. Let sourceImage be source.source.
230        let source_image = &source.source;
231        // 5. If sourceImage is not origin-clean, throw a SecurityError and return.
232        let is_origin_clean = match source_image {
233            GPUCopyExternalImageSource::ImageBitmap(inner) => inner.origin_is_clean(),
234            GPUCopyExternalImageSource::ImageData(_) => true,
235            GPUCopyExternalImageSource::HTMLImageElement(inner) => {
236                inner.same_origin(&GlobalScope::entry().origin())
237            },
238            GPUCopyExternalImageSource::HTMLVideoElement(inner) => inner.origin_is_clean(),
239            GPUCopyExternalImageSource::HTMLCanvasElement(inner) => inner.origin_is_clean(),
240            GPUCopyExternalImageSource::OffscreenCanvas(inner) => inner.origin_is_clean(),
241        };
242        if !is_origin_clean {
243            return Err(Error::Security(Some(
244                "Image source is not origin clean!".to_string(),
245            )));
246        }
247        // 6. If any of the following requirements are unmet, throw an OperationError and return.
248        let (source_image_width, source_image_height) = match source_image {
249            GPUCopyExternalImageSource::ImageBitmap(inner) => (inner.Width(), inner.Height()),
250            GPUCopyExternalImageSource::ImageData(inner) => (inner.Width(), inner.Height()),
251            GPUCopyExternalImageSource::HTMLImageElement(inner) => (inner.Width(), inner.Height()),
252            GPUCopyExternalImageSource::HTMLVideoElement(inner) => (inner.Width(), inner.Height()),
253            GPUCopyExternalImageSource::HTMLCanvasElement(inner) => (inner.Width(), inner.Height()),
254            GPUCopyExternalImageSource::OffscreenCanvas(inner) => {
255                (inner.Width() as u32, inner.Height() as u32)
256            },
257        };
258        // source.origin.x + copySize.width must be ≤ the width of sourceImage.
259        if !(source_origin.x + copy_size.width <= source_image_width) {
260            return Err(Error::Operation(Some(
261                "Source origin x + copy width exceeds source image width".to_string(),
262            )));
263        }
264        // source.origin.y + copySize.height must be ≤ the height of sourceImage.
265        if !(source_origin.y + copy_size.height <= source_image_height) {
266            return Err(Error::Operation(Some(
267                "Source origin y + copy height exceeds source image height".to_string(),
268            )));
269        }
270        // copySize.depthOrArrayLayers must be ≤ 1.
271        if !(copy_size.depth_or_array_layers <= 1) {
272            return Err(Error::Operation(Some(
273                "Copy depth or array layers must be less than or equal to 1".to_string(),
274            )));
275        }
276        // 7. Let usability be ? check the usability of the image argument(source).
277        // with usable variant we also send the snapshot
278        let usable_snapshot = match source_image {
279            GPUCopyExternalImageSource::ImageBitmap(bitmap) => {
280                // If image's [[Detached]] internal slot value is set to true, then throw an "InvalidStateError" DOMException.
281                Some(bitmap.bitmap_data().clone().ok_or_else(|| {
282                    Error::InvalidState(Some("ImageBitmap is detached".to_string()))
283                })?)
284            },
285            GPUCopyExternalImageSource::ImageData(data) => {
286                // If image's [[Detached]] internal slot value is set to true, then throw an "InvalidStateError" DOMException.
287                if data.is_detached(cx) {
288                    return Err(Error::InvalidState(Some(
289                        "ImageData is detached".to_string(),
290                    )));
291                }
292                Some(data.get_snapshot(cx.no_gc()))
293            },
294            GPUCopyExternalImageSource::HTMLImageElement(inner) => {
295                if inner.is_usable()? {
296                    inner.get_raster_image_data()
297                } else {
298                    None
299                }
300            },
301            GPUCopyExternalImageSource::HTMLVideoElement(inner) => {
302                if inner.is_usable() {
303                    inner.get_current_frame_data()
304                } else {
305                    None
306                }
307            },
308            GPUCopyExternalImageSource::HTMLCanvasElement(inner) => {
309                // If image has either a horizontal dimension or a vertical dimension equal to zero, then throw an "InvalidStateError" DOMException.
310                if inner.is_valid() {
311                    inner.get_image_data()
312                } else {
313                    return Err(Error::InvalidState(Some(
314                        "Canvas has zero area".to_string(),
315                    )));
316                }
317            },
318            GPUCopyExternalImageSource::OffscreenCanvas(inner) => {
319                // If image has either a horizontal dimension or a vertical dimension equal to zero, then throw an "InvalidStateError" DOMException.
320                if inner.Width() == 0 || inner.Height() == 0 {
321                    return Err(Error::InvalidState(Some(
322                        "Canvas has zero area".to_string(),
323                    )));
324                } else {
325                    inner.get_image_data()
326                }
327            },
328        };
329        // this is out ouf spec, but we currently do not support more
330        let texture_descriptor = destination.parent.texture.wgpu_texture_descriptor();
331        let target_snapshot_format =
332            match texture_descriptor.format {
333                wgpu_types::TextureFormat::Bgra8Unorm |
334                wgpu_types::TextureFormat::Bgra8UnormSrgb => SnapshotPixelFormat::BGRA,
335                wgpu_types::TextureFormat::Rgba8Unorm |
336                wgpu_types::TextureFormat::Rgba8UnormSrgb => SnapshotPixelFormat::RGBA,
337                _ => {
338                    return Err(Error::Operation(Some(
339                        "Unsupported texture format for copy".to_string(),
340                    )));
341                },
342            };
343        let usable_snapshot = usable_snapshot.map(|mut snapshot| {
344            if source.flipY {
345                pixels::flip_y_rgba8_image_inplace(snapshot.size(), snapshot.as_raw_bytes_mut());
346            }
347            snapshot.transform(
348                SnapshotAlphaMode::Transparent {
349                    premultiplied: destination.premultipliedAlpha,
350                },
351                target_snapshot_format,
352            );
353            snapshot.to_shared()
354        });
355        // 8. Issue the subsequent steps on the Device timeline of this.
356        if let Err(e) = self
357            .channel
358            .0
359            .send(WebGPURequest::CopyExternalImageToTexture {
360                device_id: self.device.borrow().as_ref().unwrap().id().0,
361                queue_id: self.queue.0,
362                usable_source: usable_snapshot,
363                destination: destination_tex_info,
364                dest_tex_descriptor: texture_descriptor,
365                copy_size,
366            })
367        {
368            warn!(
369                "Failed to send CopyExternalImageToTexture({:?}) ({e})",
370                destination.parent.texture.id().0
371            );
372            return Err(Error::Operation(None));
373        }
374        Ok(())
375    }
376
377    /// <https://gpuweb.github.io/gpuweb/#dom-gpuqueue-onsubmittedworkdone>
378    fn OnSubmittedWorkDone(&self, cx: &mut JSContext) -> Rc<Promise> {
379        let global = self.global();
380        let promise = Promise::new(cx, &global);
381        let task_manager = global.task_manager();
382        let task_source = task_manager.dom_manipulation_task_source();
383        let callback = callback_promise(&promise, self, task_source);
384
385        if let Err(e) = self
386            .channel
387            .0
388            .send(WebGPURequest::QueueOnSubmittedWorkDone {
389                sender: callback,
390                queue_id: self.queue.0,
391            })
392        {
393            warn!("QueueOnSubmittedWorkDone failed with {e}")
394        }
395        promise
396    }
397}
398
399impl RoutedPromiseListener<()> for GPUQueue {
400    fn handle_response(
401        &self,
402        cx: &mut js::context::JSContext,
403        _response: (),
404        promise: &Rc<Promise>,
405    ) {
406        promise.resolve_native(cx, &());
407    }
408}