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 #[no_trace]
53 channel: WebGPU,
54 device: MutNullableDom<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: MutNullableDom::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 cx,
79 Box::new(GPUQueue::new_inherited(channel, queue)),
80 global,
81 GPUQueueWrap::<D>,
82 )
83 }
84}
85
86impl<D: Equivalence> GPUQueue<D> {
87 pub(crate) fn set_device(&self, device: &GPUDevice<D>) {
88 self.device.set(Some(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 fn Label(&self) -> USVString {
109 self.label.borrow().clone()
110 }
111
112 fn SetLabel(&self, no_gc: &NoGC, value: USVString) {
114 *self.label.safe_borrow_mut(no_gc) = value;
115 }
116
117 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.get().unwrap().id().0,
124 queue_id: self.queue.0,
125 command_buffers,
126 })
127 .unwrap();
128 }
129
130 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 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 let data_size: usize = data_len / sizeof_element;
149 debug_assert_eq!(data_len % sizeof_element, 0);
150 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 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 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.get().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 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.get().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 fn CopyExternalImageToTexture(
246 &self,
247 cx: &mut JSContext,
248 source: &GPUCopyExternalImageSourceInfo<D>,
249 destination: &GPUCopyExternalImageDestInfo<D>,
250 copy_size: GPUExtent3D,
251 ) -> Fallible<()> {
252 let source_origin = source.origin.try_convert()?;
254 let destination_tex_info = destination.parent.try_convert()?;
256 let copy_size = copy_size.try_convert()?;
258 let source_image = &source.source;
260 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 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) => {
281 (inner.Width(cx), inner.Height(cx))
282 },
283 GPUCopyExternalImageSource::HTMLVideoElement(inner) => (inner.Width(), inner.Height()),
284 GPUCopyExternalImageSource::HTMLCanvasElement(inner) => (inner.Width(), inner.Height()),
285 GPUCopyExternalImageSource::OffscreenCanvas(inner) => {
286 (inner.Width() as u32, inner.Height() as u32)
287 },
288 };
289 if !(source_origin.x + copy_size.width <= source_image_width) {
291 return Err(Error::Operation(Some(
292 "Source origin x + copy width exceeds source image width".to_string(),
293 )));
294 }
295 if !(source_origin.y + copy_size.height <= source_image_height) {
297 return Err(Error::Operation(Some(
298 "Source origin y + copy height exceeds source image height".to_string(),
299 )));
300 }
301 if !(copy_size.depth_or_array_layers <= 1) {
303 return Err(Error::Operation(Some(
304 "Copy depth or array layers must be less than or equal to 1".to_string(),
305 )));
306 }
307 let usable_snapshot = match source_image {
310 GPUCopyExternalImageSource::ImageBitmap(bitmap) => {
311 Some(bitmap.bitmap_data().clone().ok_or_else(|| {
313 Error::InvalidState(Some("ImageBitmap is detached".to_string()))
314 })?)
315 },
316 GPUCopyExternalImageSource::ImageData(data) => {
317 if data.is_detached(cx) {
319 return Err(Error::InvalidState(Some(
320 "ImageData is detached".to_string(),
321 )));
322 }
323 Some(data.get_snapshot(cx.no_gc()))
324 },
325 GPUCopyExternalImageSource::HTMLImageElement(inner) => {
326 if inner.is_usable()? {
327 inner.get_raster_image_data()
328 } else {
329 None
330 }
331 },
332 GPUCopyExternalImageSource::HTMLVideoElement(inner) => {
333 if inner.is_usable() {
334 inner.get_current_frame_data()
335 } else {
336 None
337 }
338 },
339 GPUCopyExternalImageSource::HTMLCanvasElement(inner) => {
340 if inner.is_valid() {
342 inner.get_image_data()
343 } else {
344 return Err(Error::InvalidState(Some(
345 "Canvas has zero area".to_string(),
346 )));
347 }
348 },
349 GPUCopyExternalImageSource::OffscreenCanvas(inner) => {
350 if inner.Width() == 0 || inner.Height() == 0 {
352 return Err(Error::InvalidState(Some(
353 "Canvas has zero area".to_string(),
354 )));
355 } else {
356 inner.get_image_data()
357 }
358 },
359 };
360 let texture_descriptor = destination.parent.texture.wgpu_texture_descriptor();
362 let target_snapshot_format = match texture_descriptor.format {
363 TextureFormat::Bgra8Unorm | TextureFormat::Bgra8UnormSrgb => SnapshotPixelFormat::BGRA,
364 TextureFormat::Rgba8Unorm | TextureFormat::Rgba8UnormSrgb => SnapshotPixelFormat::RGBA,
365 _ => {
366 return Err(Error::Operation(Some(
367 "Unsupported texture format for copy".to_string(),
368 )));
369 },
370 };
371 let usable_snapshot = usable_snapshot.map(|mut snapshot| {
372 if source.flipY {
373 pixels::flip_y_rgba8_image_inplace(snapshot.size(), snapshot.as_raw_bytes_mut());
374 }
375 snapshot.transform(
376 SnapshotAlphaMode::Transparent {
377 premultiplied: destination.premultipliedAlpha,
378 },
379 target_snapshot_format,
380 );
381 snapshot.into_shared()
382 });
383 if let Err(e) = self
385 .channel
386 .0
387 .send(WebGPURequest::CopyExternalImageToTexture {
388 device_id: self.device.get().unwrap().id().0,
389 queue_id: self.queue.0,
390 usable_source: usable_snapshot,
391 destination: destination_tex_info,
392 dest_tex_descriptor: texture_descriptor,
393 copy_size,
394 })
395 {
396 warn!(
397 "Failed to send CopyExternalImageToTexture({:?}) ({e})",
398 destination.parent.texture.id().0
399 );
400 return Err(Error::Operation(Some(
401 "Failed to copy external image to texture".into(),
402 )));
403 }
404 Ok(())
405 }
406
407 fn OnSubmittedWorkDone(
409 &self,
410 cx: &mut JSContext,
411 ) -> <D::Promise as PromiseHelpers<D>>::StackRoot {
412 let global = self.global_from_reflector();
413 let promise = D::Promise::new(cx, &global);
414 let callback = promise.callback_promise_dom_manipulation_task_source(self);
415
416 if let Err(e) = self
417 .channel
418 .0
419 .send(WebGPURequest::QueueOnSubmittedWorkDone {
420 sender: callback,
421 queue_id: self.queue.0,
422 })
423 {
424 warn!("QueueOnSubmittedWorkDone failed with {e}")
425 }
426 promise
427 }
428}
429
430impl<D: Equivalence> RoutedPromiseListener<D, ()> for GPUQueue<D> {
431 fn handle_response(
432 &self,
433 cx: &mut js::context::JSContext,
434 _response: (),
435 promise: &<D::Promise as PromiseHelpers<D>>::StackRoot,
436 ) {
437 promise.resolve_native(cx, &());
438 }
439}