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::error::{Error, Fallible};
29use script_bindings::interfaces::{GlobalScopeHelpers, PromiseHelpers};
30use script_bindings::reflector::{DomGlobalGeneric, Reflector, reflect_dom_object_with_wrap};
31use script_bindings::root::DomRoot;
32use servo_base::generic_channel::GenericSharedMemory;
33use webgpu_traits::{COPY_BUFFER_ALIGNMENT, TextureFormat, WebGPU, WebGPUQueue, WebGPURequest};
34
35use crate::JSTraceable;
36use crate::dom::bindings::root::Dom;
37use crate::dom::bindings::str::USVString;
38use crate::gpubuffer::GPUBuffer;
39use crate::gpucommandbuffer::GPUCommandBuffer;
40use crate::gpuconvert::{WebGPUConvert, WebGPUTryConvert};
41use crate::gpudevice::GPUDevice;
42use crate::traits::{
43 Equivalence, HtmlCanvasElementTrait, HtmlImageElementTrait, ImageBitmapTrait, ImageDataTrait,
44 OffscreenCanvasTrait, OriginIsCleanTrait, WebGPUHTMLVideoTrait, WebGPUPromise,
45 WebGPUPromiseCallbackTrait, WebGPURootedPromiseTrait,
46};
47
48#[dom_struct]
49pub struct GPUQueue<D: DomTypes> {
50 reflector_: Reflector,
51 #[ignore_malloc_size_of = "defined in webgpu"]
52 #[no_trace]
53 channel: WebGPU,
54 device: DomRefCell<Option<Dom<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: DomRefCell::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 Box::new(GPUQueue::new_inherited(channel, queue)),
79 global,
80 cx,
81 GPUQueueWrap::<D>,
82 )
83 }
84}
85
86impl<D: Equivalence> GPUQueue<D> {
87 pub(crate) fn set_device(&self, no_gc: &NoGC, device: &GPUDevice<D>) {
88 *self.device.safe_borrow_mut(no_gc) = Some(Dom::from_ref(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.borrow().as_ref().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.borrow().as_ref().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.borrow().as_ref().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) => (inner.Width(), inner.Height()),
281 GPUCopyExternalImageSource::HTMLVideoElement(inner) => (inner.Width(), inner.Height()),
282 GPUCopyExternalImageSource::HTMLCanvasElement(inner) => (inner.Width(), inner.Height()),
283 GPUCopyExternalImageSource::OffscreenCanvas(inner) => {
284 (inner.Width() as u32, inner.Height() as u32)
285 },
286 };
287 if !(source_origin.x + copy_size.width <= source_image_width) {
289 return Err(Error::Operation(Some(
290 "Source origin x + copy width exceeds source image width".to_string(),
291 )));
292 }
293 if !(source_origin.y + copy_size.height <= source_image_height) {
295 return Err(Error::Operation(Some(
296 "Source origin y + copy height exceeds source image height".to_string(),
297 )));
298 }
299 if !(copy_size.depth_or_array_layers <= 1) {
301 return Err(Error::Operation(Some(
302 "Copy depth or array layers must be less than or equal to 1".to_string(),
303 )));
304 }
305 let usable_snapshot = match source_image {
308 GPUCopyExternalImageSource::ImageBitmap(bitmap) => {
309 Some(bitmap.bitmap_data().clone().ok_or_else(|| {
311 Error::InvalidState(Some("ImageBitmap is detached".to_string()))
312 })?)
313 },
314 GPUCopyExternalImageSource::ImageData(data) => {
315 if data.is_detached(cx) {
317 return Err(Error::InvalidState(Some(
318 "ImageData is detached".to_string(),
319 )));
320 }
321 Some(data.get_snapshot(cx.no_gc()))
322 },
323 GPUCopyExternalImageSource::HTMLImageElement(inner) => {
324 if inner.is_usable()? {
325 inner.get_raster_image_data()
326 } else {
327 None
328 }
329 },
330 GPUCopyExternalImageSource::HTMLVideoElement(inner) => {
331 if inner.is_usable() {
332 inner.get_current_frame_data()
333 } else {
334 None
335 }
336 },
337 GPUCopyExternalImageSource::HTMLCanvasElement(inner) => {
338 if inner.is_valid() {
340 inner.get_image_data()
341 } else {
342 return Err(Error::InvalidState(Some(
343 "Canvas has zero area".to_string(),
344 )));
345 }
346 },
347 GPUCopyExternalImageSource::OffscreenCanvas(inner) => {
348 if inner.Width() == 0 || inner.Height() == 0 {
350 return Err(Error::InvalidState(Some(
351 "Canvas has zero area".to_string(),
352 )));
353 } else {
354 inner.get_image_data()
355 }
356 },
357 };
358 let texture_descriptor = destination.parent.texture.wgpu_texture_descriptor();
360 let target_snapshot_format = match texture_descriptor.format {
361 TextureFormat::Bgra8Unorm | TextureFormat::Bgra8UnormSrgb => SnapshotPixelFormat::BGRA,
362 TextureFormat::Rgba8Unorm | TextureFormat::Rgba8UnormSrgb => SnapshotPixelFormat::RGBA,
363 _ => {
364 return Err(Error::Operation(Some(
365 "Unsupported texture format for copy".to_string(),
366 )));
367 },
368 };
369 let usable_snapshot = usable_snapshot.map(|mut snapshot| {
370 if source.flipY {
371 pixels::flip_y_rgba8_image_inplace(snapshot.size(), snapshot.as_raw_bytes_mut());
372 }
373 snapshot.transform(
374 SnapshotAlphaMode::Transparent {
375 premultiplied: destination.premultipliedAlpha,
376 },
377 target_snapshot_format,
378 );
379 snapshot.to_shared()
380 });
381 if let Err(e) = self
383 .channel
384 .0
385 .send(WebGPURequest::CopyExternalImageToTexture {
386 device_id: self.device.borrow().as_ref().unwrap().id().0,
387 queue_id: self.queue.0,
388 usable_source: usable_snapshot,
389 destination: destination_tex_info,
390 dest_tex_descriptor: texture_descriptor,
391 copy_size,
392 })
393 {
394 warn!(
395 "Failed to send CopyExternalImageToTexture({:?}) ({e})",
396 destination.parent.texture.id().0
397 );
398 return Err(Error::Operation(Some(
399 "Failed to copy external image to texture".into(),
400 )));
401 }
402 Ok(())
403 }
404
405 fn OnSubmittedWorkDone(
407 &self,
408 cx: &mut JSContext,
409 ) -> <D::Promise as PromiseHelpers<D>>::StackRoot {
410 let global = self.global_from_reflector();
411 let promise = <D::Promise as PromiseHelpers<D>>::StackRoot::new_rooted(cx, &global);
412 let callback = promise.callback_promise_dom_manipulation_task_source(self);
413
414 if let Err(e) = self
415 .channel
416 .0
417 .send(WebGPURequest::QueueOnSubmittedWorkDone {
418 sender: callback,
419 queue_id: self.queue.0,
420 })
421 {
422 warn!("QueueOnSubmittedWorkDone failed with {e}")
423 }
424 promise
425 }
426}