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