1use 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};
18use script_webgpu::gpuconvert::{WebGPUConvert, WebGPUTryConvert};
19use servo_base::generic_channel::GenericSharedMemory;
20use webgpu_traits::{WebGPU, WebGPUQueue, WebGPURequest};
21
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(
72 cx,
73 Box::new(GPUQueue::new_inherited(channel, queue)),
74 global,
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 fn Label(&self) -> USVString {
92 self.label.borrow().clone()
93 }
94
95 fn SetLabel(&self, no_gc: &NoGC, value: USVString) {
97 *self.label.safe_borrow_mut(no_gc) = value;
98 }
99
100 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 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 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 let data_size: usize = data_len / sizeof_element;
132 debug_assert_eq!(data_len % sizeof_element, 0);
133 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(Some(
140 "Overflow occured when calculating `contentsSize`".into(),
141 )))?
142 };
143
144 if !(data_offset + content_size <= data_size as u64) {
146 return Err(Error::Operation(Some(
147 "`dataOffset` + `contentsSize` is greater than `dataSize`".into(),
148 )));
149 }
150
151 if !((content_size * sizeof_element as u64)
152 .is_multiple_of(wgpu_types::COPY_BUFFER_ALIGNMENT))
153 {
154 return Err(Error::Operation(Some(
155 "`contentSize` as bytes is not a multiple of 4 bytes".into(),
156 )));
157 }
158
159 let byte_start = (data_offset as usize) * sizeof_element;
161 let byte_end = ((data_offset + content_size) as usize) * sizeof_element;
162 let contents = GenericSharedMemory::from_bytes(
163 &get_buffer_source_slice(&data, cx.no_gc())[byte_start..byte_end],
164 );
165 if let Err(e) = self.channel.0.send(WebGPURequest::WriteBuffer {
166 device_id: self.device.borrow().as_ref().unwrap().id().0,
167 queue_id: self.queue.0,
168 buffer_id: buffer.id().0,
169 buffer_offset,
170 data: contents,
171 }) {
172 warn!("Failed to send WriteBuffer({:?}) ({})", buffer.id(), e);
173 return Err(Error::Operation(Some(
174 "Failed to write buffer to GPU".into(),
175 )));
176 }
177
178 Ok(())
179 }
180
181 fn WriteTexture(
183 &self,
184 cx: &mut JSContext,
185 destination: &GPUTexelCopyTextureInfo,
186 data: BufferSource,
187 data_layout: &GPUTexelCopyBufferLayout,
188 size: GPUExtent3D,
189 ) -> Fallible<()> {
190 let bytes = get_buffer_source_slice(&data, cx.no_gc());
191 let len = bytes.len() as u64;
192
193 if !(data_layout.offset <= len) {
194 return Err(Error::Operation(Some(
195 "`dataLayout`'s offset is greater than texture buffer length".into(),
196 )));
197 }
198
199 let texture_cv = destination.try_convert()?;
200 let texture_layout = data_layout.convert();
201 let write_size = (&size).try_convert()?;
202 let final_data = GenericSharedMemory::from_bytes(bytes);
203
204 if let Err(e) = self.channel.0.send(WebGPURequest::WriteTexture {
205 device_id: self.device.borrow().as_ref().unwrap().id().0,
206 queue_id: self.queue.0,
207 texture_cv,
208 data_layout: texture_layout,
209 size: write_size,
210 data: final_data,
211 }) {
212 warn!(
213 "Failed to send WriteTexture({:?}) ({})",
214 destination.texture.id().0,
215 e
216 );
217 return Err(Error::Operation(Some(
218 "Failed to write to GPUTexture".into(),
219 )));
220 }
221
222 Ok(())
223 }
224
225 #[expect(
226 clippy::nonminimal_bool,
227 reason = "Following the spec steps more closely"
228 )]
229 fn CopyExternalImageToTexture(
231 &self,
232 cx: &mut JSContext,
233 source: &GPUCopyExternalImageSourceInfo,
234 destination: &GPUCopyExternalImageDestInfo,
235 copy_size: GPUExtent3D,
236 ) -> Fallible<()> {
237 let source_origin = source.origin.try_convert()?;
239 let destination_tex_info = destination.parent.try_convert()?;
241 let copy_size = copy_size.try_convert()?;
243 let source_image = &source.source;
245 let is_origin_clean = match source_image {
247 GPUCopyExternalImageSource::ImageBitmap(inner) => inner.origin_is_clean(),
248 GPUCopyExternalImageSource::ImageData(_) => true,
249 GPUCopyExternalImageSource::HTMLImageElement(inner) => {
250 inner.same_origin(&GlobalScope::entry().origin())
251 },
252 GPUCopyExternalImageSource::HTMLVideoElement(inner) => inner.origin_is_clean(),
253 GPUCopyExternalImageSource::HTMLCanvasElement(inner) => inner.origin_is_clean(),
254 GPUCopyExternalImageSource::OffscreenCanvas(inner) => inner.origin_is_clean(),
255 };
256 if !is_origin_clean {
257 return Err(Error::Security(Some(
258 "Image source is not origin clean!".to_string(),
259 )));
260 }
261 let (source_image_width, source_image_height) = match source_image {
263 GPUCopyExternalImageSource::ImageBitmap(inner) => (inner.Width(), inner.Height()),
264 GPUCopyExternalImageSource::ImageData(inner) => (inner.Width(), inner.Height()),
265 GPUCopyExternalImageSource::HTMLImageElement(inner) => (inner.Width(), inner.Height()),
266 GPUCopyExternalImageSource::HTMLVideoElement(inner) => (inner.Width(), inner.Height()),
267 GPUCopyExternalImageSource::HTMLCanvasElement(inner) => (inner.Width(), inner.Height()),
268 GPUCopyExternalImageSource::OffscreenCanvas(inner) => {
269 (inner.Width() as u32, inner.Height() as u32)
270 },
271 };
272 if !(source_origin.x + copy_size.width <= source_image_width) {
274 return Err(Error::Operation(Some(
275 "Source origin x + copy width exceeds source image width".to_string(),
276 )));
277 }
278 if !(source_origin.y + copy_size.height <= source_image_height) {
280 return Err(Error::Operation(Some(
281 "Source origin y + copy height exceeds source image height".to_string(),
282 )));
283 }
284 if !(copy_size.depth_or_array_layers <= 1) {
286 return Err(Error::Operation(Some(
287 "Copy depth or array layers must be less than or equal to 1".to_string(),
288 )));
289 }
290 let usable_snapshot = match source_image {
293 GPUCopyExternalImageSource::ImageBitmap(bitmap) => {
294 Some(bitmap.bitmap_data().clone().ok_or_else(|| {
296 Error::InvalidState(Some("ImageBitmap is detached".to_string()))
297 })?)
298 },
299 GPUCopyExternalImageSource::ImageData(data) => {
300 if data.is_detached(cx) {
302 return Err(Error::InvalidState(Some(
303 "ImageData is detached".to_string(),
304 )));
305 }
306 Some(data.get_snapshot(cx.no_gc()))
307 },
308 GPUCopyExternalImageSource::HTMLImageElement(inner) => {
309 if inner.is_usable()? {
310 inner.get_raster_image_data()
311 } else {
312 None
313 }
314 },
315 GPUCopyExternalImageSource::HTMLVideoElement(inner) => {
316 if inner.is_usable() {
317 inner.get_current_frame_data()
318 } else {
319 None
320 }
321 },
322 GPUCopyExternalImageSource::HTMLCanvasElement(inner) => {
323 if inner.is_valid() {
325 inner.get_image_data()
326 } else {
327 return Err(Error::InvalidState(Some(
328 "Canvas has zero area".to_string(),
329 )));
330 }
331 },
332 GPUCopyExternalImageSource::OffscreenCanvas(inner) => {
333 if inner.Width() == 0 || inner.Height() == 0 {
335 return Err(Error::InvalidState(Some(
336 "Canvas has zero area".to_string(),
337 )));
338 } else {
339 inner.get_image_data()
340 }
341 },
342 };
343 let texture_descriptor = destination.parent.texture.wgpu_texture_descriptor();
345 let target_snapshot_format =
346 match texture_descriptor.format {
347 wgpu_types::TextureFormat::Bgra8Unorm |
348 wgpu_types::TextureFormat::Bgra8UnormSrgb => SnapshotPixelFormat::BGRA,
349 wgpu_types::TextureFormat::Rgba8Unorm |
350 wgpu_types::TextureFormat::Rgba8UnormSrgb => SnapshotPixelFormat::RGBA,
351 _ => {
352 return Err(Error::Operation(Some(
353 "Unsupported texture format for copy".to_string(),
354 )));
355 },
356 };
357 let usable_snapshot = usable_snapshot.map(|mut snapshot| {
358 if source.flipY {
359 pixels::flip_y_rgba8_image_inplace(snapshot.size(), snapshot.as_raw_bytes_mut());
360 }
361 snapshot.transform(
362 SnapshotAlphaMode::Transparent {
363 premultiplied: destination.premultipliedAlpha,
364 },
365 target_snapshot_format,
366 );
367 snapshot.to_shared()
368 });
369 if let Err(e) = self
371 .channel
372 .0
373 .send(WebGPURequest::CopyExternalImageToTexture {
374 device_id: self.device.borrow().as_ref().unwrap().id().0,
375 queue_id: self.queue.0,
376 usable_source: usable_snapshot,
377 destination: destination_tex_info,
378 dest_tex_descriptor: texture_descriptor,
379 copy_size,
380 })
381 {
382 warn!(
383 "Failed to send CopyExternalImageToTexture({:?}) ({e})",
384 destination.parent.texture.id().0
385 );
386 return Err(Error::Operation(Some(
387 "Failed to copy external image to texture".into(),
388 )));
389 }
390 Ok(())
391 }
392
393 fn OnSubmittedWorkDone(&self, cx: &mut JSContext) -> Rc<Promise> {
395 let global = self.global();
396 let promise = Promise::new(cx, &global);
397 let task_manager = global.task_manager();
398 let task_source = task_manager.dom_manipulation_task_source();
399 let callback = callback_promise(&promise, self, task_source);
400
401 if let Err(e) = self
402 .channel
403 .0
404 .send(WebGPURequest::QueueOnSubmittedWorkDone {
405 sender: callback,
406 queue_id: self.queue.0,
407 })
408 {
409 warn!("QueueOnSubmittedWorkDone failed with {e}")
410 }
411 promise
412 }
413}
414
415impl RoutedPromiseListener<()> for GPUQueue {
416 fn handle_response(
417 &self,
418 cx: &mut js::context::JSContext,
419 _response: (),
420 promise: &Rc<Promise>,
421 ) {
422 promise.resolve_native(cx, &());
423 }
424}