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