Skip to main content

script/dom/webgpu/
gpucanvascontext.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 std::borrow::Cow;
6use std::cell::{Cell, RefCell};
7use std::rc::Rc;
8
9use arrayvec::ArrayVec;
10use dom_struct::dom_struct;
11use js::context::JSContext;
12use pixels::Snapshot;
13use script_bindings::cformat;
14use script_bindings::codegen::GenericBindings::WebGPUBinding::{
15    GPUDeviceMethods, GPUTextureFormat, GPUTextureUsageConstants,
16};
17use script_bindings::reflector::{Reflector, reflect_weak_referenceable_dom_object};
18use script_webgpu::gpuconvert::convert_texture_descriptor;
19use servo_base::{Epoch, generic_channel};
20use webgpu_traits::{
21    ContextConfiguration, PRESENTATION_BUFFER_COUNT, PendingTexture, WebGPU, WebGPUContextId,
22    WebGPURequest, id,
23};
24use webrender_api::{ImageFormat, ImageKey};
25
26use super::gputexture::GPUTexture;
27use crate::canvas_context::{CanvasContext, CanvasHelpers, HTMLCanvasElementOrOffscreenCanvas};
28use crate::dom::bindings::codegen::Bindings::GPUCanvasContextBinding::GPUCanvasContextMethods;
29use crate::dom::bindings::codegen::Bindings::WebGPUBinding::GPUTexture_Binding::GPUTextureMethods;
30use crate::dom::bindings::codegen::Bindings::WebGPUBinding::{
31    GPUCanvasAlphaMode, GPUCanvasConfiguration as RootedGPUCanvasConfiguration, GPUExtent3D,
32    GPUExtent3DDict, GPUObjectDescriptorBase, GPUTextureDescriptor, GPUTextureDimension,
33};
34use crate::dom::bindings::codegen::UnionTypes::HTMLCanvasElementOrOffscreenCanvas as RootedHTMLCanvasElementOrOffscreenCanvas;
35use crate::dom::bindings::error::{Error, Fallible};
36use crate::dom::bindings::reflector::DomGlobal;
37use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom};
38use crate::dom::bindings::str::USVString;
39use crate::dom::globalscope::GlobalScope;
40use crate::dom::htmlcanvaselement::HTMLCanvasElement;
41use crate::dom::webgpu::gpudevice::GPUDevice;
42
43/// <https://gpuweb.github.io/gpuweb/#supported-context-formats>
44fn supported_context_format(format: GPUTextureFormat) -> bool {
45    // TODO: GPUTextureFormat::Rgba16float
46    matches!(
47        format,
48        GPUTextureFormat::Bgra8unorm | GPUTextureFormat::Rgba8unorm
49    )
50}
51
52#[derive(JSTraceable, MallocSizeOf)]
53struct DroppableGPUCanvasContext {
54    #[no_trace]
55    context_id: WebGPUContextId,
56    #[no_trace]
57    channel: WebGPU,
58}
59
60impl Drop for DroppableGPUCanvasContext {
61    fn drop(&mut self) {
62        if let Err(error) = self.channel.0.send(WebGPURequest::DestroyContext {
63            context_id: self.context_id,
64        }) {
65            warn!(
66                "Failed to send DestroyContext({:?}): {error}",
67                self.context_id,
68            );
69        }
70    }
71}
72
73#[derive(JSTraceable, MallocSizeOf)]
74#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
75struct GPUCanvasConfiguration {
76    alpha_mode: GPUCanvasAlphaMode,
77    device: Dom<GPUDevice>,
78    format: GPUTextureFormat,
79    usage: u32,
80    view_formats: Vec<GPUTextureFormat>,
81}
82
83impl From<&RootedGPUCanvasConfiguration> for GPUCanvasConfiguration {
84    fn from(value: &RootedGPUCanvasConfiguration) -> GPUCanvasConfiguration {
85        GPUCanvasConfiguration {
86            alpha_mode: value.alphaMode,
87            device: value.device.as_traced(),
88            format: value.format,
89            usage: value.usage,
90            view_formats: value.viewFormats.clone(),
91        }
92    }
93}
94
95impl GPUCanvasConfiguration {
96    fn root(&self) -> RootedGPUCanvasConfiguration {
97        RootedGPUCanvasConfiguration {
98            alphaMode: self.alpha_mode,
99            device: self.device.as_rooted(),
100            format: self.format,
101            usage: self.usage,
102            viewFormats: self.view_formats.clone(),
103        }
104    }
105}
106
107#[dom_struct]
108pub(crate) struct GPUCanvasContext {
109    reflector_: Reflector,
110    /// <https://gpuweb.github.io/gpuweb/#dom-gpucanvascontext-canvas>
111    canvas: HTMLCanvasElementOrOffscreenCanvas,
112    #[ignore_malloc_size_of = "manual writing is hard"]
113    /// <https://gpuweb.github.io/gpuweb/#dom-gpucanvascontext-configuration-slot>
114    configuration: RefCell<Option<GPUCanvasConfiguration>>,
115    /// <https://gpuweb.github.io/gpuweb/#dom-gpucanvascontext-texturedescriptor-slot>
116    texture_descriptor: RefCell<Option<GPUTextureDescriptor>>,
117    /// <https://gpuweb.github.io/gpuweb/#dom-gpucanvascontext-currenttexture-slot>
118    current_texture: MutNullableDom<GPUTexture>,
119    /// Set if image is cleared
120    /// (usually done by [`GPUCanvasContext::replace_drawing_buffer`])
121    cleared: Cell<bool>,
122    droppable: DroppableGPUCanvasContext,
123}
124
125impl GPUCanvasContext {
126    #[cfg_attr(crown, expect(crown::unrooted_must_root))]
127    fn new_inherited(
128        global: &GlobalScope,
129        canvas: HTMLCanvasElementOrOffscreenCanvas,
130        channel: WebGPU,
131    ) -> Self {
132        let (sender, receiver) = generic_channel::channel().unwrap();
133        let size = canvas.size().cast().cast_unit();
134        let mut buffer_ids = ArrayVec::<id::BufferId, PRESENTATION_BUFFER_COUNT>::new();
135        for _ in 0..PRESENTATION_BUFFER_COUNT {
136            buffer_ids.push(global.wgpu_id_hub().create_buffer_id());
137        }
138        if let Err(error) = channel.0.send(WebGPURequest::CreateContext {
139            buffer_ids,
140            size,
141            sender,
142        }) {
143            warn!("Failed to send CreateContext ({error:?})");
144        }
145        let context_id = receiver.recv().unwrap();
146
147        Self {
148            reflector_: Reflector::new(),
149            canvas,
150            configuration: RefCell::new(None),
151            texture_descriptor: RefCell::new(None),
152            current_texture: MutNullableDom::default(),
153            cleared: Cell::new(true),
154            droppable: DroppableGPUCanvasContext {
155                context_id,
156                channel,
157            },
158        }
159    }
160
161    pub(crate) fn new(
162        cx: &mut JSContext,
163        global: &GlobalScope,
164        canvas: &HTMLCanvasElement,
165        channel: WebGPU,
166    ) -> DomRoot<Self> {
167        reflect_weak_referenceable_dom_object(
168            cx,
169            Rc::new(GPUCanvasContext::new_inherited(
170                global,
171                HTMLCanvasElementOrOffscreenCanvas::HTMLCanvasElement(Dom::from_ref(canvas)),
172                channel,
173            )),
174            global,
175        )
176    }
177}
178
179// Abstract ops from spec
180impl GPUCanvasContext {
181    pub(crate) fn set_image_key(&self, image_key: ImageKey) {
182        if let Err(error) = self.droppable.channel.0.send(WebGPURequest::SetImageKey {
183            context_id: self.context_id(),
184            image_key,
185        }) {
186            warn!(
187                "Failed to send WebGPURequest::Present({:?}) ({error})",
188                self.context_id()
189            );
190        }
191    }
192
193    /// <https://gpuweb.github.io/gpuweb/#abstract-opdef-updating-the-rendering-of-a-webgpu-canvas>
194    pub(crate) fn update_rendering(&self, canvas_epoch: Epoch) -> bool {
195        // Present by updating the image in WebRender. This will copy the texture into
196        // the presentation buffer and use it for presenting or send a cleared image to WebRender.
197        if let Err(error) = self.droppable.channel.0.send(WebGPURequest::Present {
198            context_id: self.context_id(),
199            pending_texture: self.pending_texture(),
200            size: self.size(),
201            canvas_epoch,
202        }) {
203            warn!(
204                "Failed to send WebGPURequest::Present({:?}) ({error})",
205                self.context_id()
206            );
207        }
208
209        // 1. Expire the current texture of context.
210        self.expire_current_texture(true);
211
212        true
213    }
214
215    /// <https://gpuweb.github.io/gpuweb/#abstract-opdef-gputexturedescriptor-for-the-canvas-and-configuration>
216    fn texture_descriptor_for_canvas_and_configuration(
217        &self,
218        configuration: &RootedGPUCanvasConfiguration,
219    ) -> GPUTextureDescriptor {
220        let size = self.size();
221        GPUTextureDescriptor {
222            size: GPUExtent3D::GPUExtent3DDict(GPUExtent3DDict {
223                width: size.width,
224                height: size.height,
225                depthOrArrayLayers: 1,
226            }),
227            format: configuration.format,
228            // We need to add `COPY_SRC` so we can copy texture to presentation buffer
229            // causes FAIL on webgpu:web_platform,canvas,configure:usage:*
230            usage: configuration.usage | GPUTextureUsageConstants::COPY_SRC,
231            viewFormats: configuration.viewFormats.clone(),
232            // All other members set to their defaults.
233            mipLevelCount: 1,
234            sampleCount: 1,
235            parent: GPUObjectDescriptorBase {
236                label: USVString::default(),
237            },
238            dimension: GPUTextureDimension::_2d,
239        }
240    }
241
242    /// <https://gpuweb.github.io/gpuweb/#abstract-opdef-expire-the-current-texture>
243    fn expire_current_texture(&self, skip_dirty: bool) {
244        // 1. If context.[[currentTexture]] is not null:
245
246        if let Some(current_texture) = self.current_texture.take() {
247            // 1.2 Set context.[[currentTexture]] to null.
248
249            // 1.1 Call context.[[currentTexture]].destroy()
250            // (without destroying context.[[drawingBuffer]])
251            // to terminate write access to the image.
252            current_texture.Destroy()
253            // we can safely destroy content here,
254            // because we already copied content when doing present
255            // or current texture is getting cleared
256        }
257        // We skip marking the canvas as dirty again if we are already
258        // in the process of updating the rendering.
259        if !skip_dirty {
260            // texture is either cleared or applied to canvas
261            self.mark_as_dirty();
262        }
263    }
264
265    /// <https://gpuweb.github.io/gpuweb/#abstract-opdef-replace-the-drawing-buffer>
266    fn replace_drawing_buffer(&self) {
267        // 1. Expire the current texture of context.
268        self.expire_current_texture(false);
269        // 2. Let configuration be context.[[configuration]].
270        // 3. Set context.[[drawingBuffer]] to
271        // a transparent black image of the same size as context.canvas
272        self.cleared.set(true);
273    }
274}
275
276// Internal helper methods
277impl GPUCanvasContext {
278    fn context_configuration(&self) -> Option<ContextConfiguration> {
279        let configuration = self.configuration.borrow();
280        let configuration = configuration.as_ref()?;
281        Some(ContextConfiguration {
282            device_id: configuration.device.id().0,
283            queue_id: configuration.device.queue_id().0,
284            format: match configuration.format {
285                GPUTextureFormat::Bgra8unorm => ImageFormat::BGRA8,
286                GPUTextureFormat::Rgba8unorm => ImageFormat::RGBA8,
287                _ => unreachable!("Configure method should set valid texture format"),
288            },
289            is_opaque: matches!(configuration.alpha_mode, GPUCanvasAlphaMode::Opaque),
290            size: self.size(),
291        })
292    }
293
294    fn pending_texture(&self) -> Option<PendingTexture> {
295        self.current_texture.get().map(|texture| PendingTexture {
296            texture_id: texture.id().0,
297            encoder_id: self.global().wgpu_id_hub().create_command_encoder_id(),
298            command_buffer_id: self.global().wgpu_id_hub().create_command_buffer_id(),
299            configuration: self
300                .context_configuration()
301                .expect("Context should be configured if there is a texture."),
302        })
303    }
304}
305
306impl CanvasContext for GPUCanvasContext {
307    type ID = WebGPUContextId;
308
309    fn context_id(&self) -> WebGPUContextId {
310        self.droppable.context_id
311    }
312
313    /// <https://gpuweb.github.io/gpuweb/#abstract-opdef-update-the-canvas-size>
314    fn resize(&self) {
315        // 1. Replace the drawing buffer of context.
316        self.replace_drawing_buffer();
317        // 2. Let configuration be context.[[configuration]]
318        let configuration = self.configuration.borrow();
319        // 3. If configuration is not null:
320        if let Some(configuration) = configuration.as_ref() {
321            // 3.1. Set context.[[textureDescriptor]] to the
322            // GPUTextureDescriptor for the canvas and configuration(canvas, configuration).
323            self.texture_descriptor.replace(Some(
324                self.texture_descriptor_for_canvas_and_configuration(&configuration.root()),
325            ));
326        }
327    }
328
329    fn reset_bitmap(&self) {
330        warn!("The GPUCanvasContext 'reset_bitmap' is not implemented yet");
331    }
332
333    /// <https://gpuweb.github.io/gpuweb/#ref-for-abstract-opdef-get-a-copy-of-the-image-contents-of-a-context%E2%91%A5>
334    fn get_image_data(&self) -> Option<Snapshot> {
335        // 1. Return a copy of the image contents of context.
336        Some(if self.cleared.get() {
337            Snapshot::cleared(self.size())
338        } else {
339            let (sender, receiver) = generic_channel::channel().unwrap();
340            self.droppable
341                .channel
342                .0
343                .send(WebGPURequest::GetImage {
344                    context_id: self.context_id(),
345                    // We need to read from the pending texture, if one exists.
346                    pending_texture: self.pending_texture(),
347                    sender,
348                })
349                .ok()?;
350            receiver.recv().ok()?.to_owned()
351        })
352    }
353
354    fn canvas(&self) -> Option<RootedHTMLCanvasElementOrOffscreenCanvas> {
355        Some(RootedHTMLCanvasElementOrOffscreenCanvas::from(&self.canvas))
356    }
357
358    fn mark_as_dirty(&self) {
359        self.canvas.mark_as_dirty();
360    }
361}
362
363impl GPUCanvasContextMethods<crate::DomTypeHolder> for GPUCanvasContext {
364    /// <https://gpuweb.github.io/gpuweb/#dom-gpucanvascontext-canvas>
365    fn Canvas(&self) -> RootedHTMLCanvasElementOrOffscreenCanvas {
366        RootedHTMLCanvasElementOrOffscreenCanvas::from(&self.canvas)
367    }
368
369    /// <https://gpuweb.github.io/gpuweb/#dom-gpucanvascontext-configure>
370    fn Configure(&self, configuration: &RootedGPUCanvasConfiguration) -> Fallible<()> {
371        // 1. Let device be configuration.device
372        let device = &configuration.device;
373
374        // 6. Let descriptor be the GPUTextureDescriptor for the canvas and configuration.
375        let descriptor = self.texture_descriptor_for_canvas_and_configuration(configuration);
376
377        // 2. Validate texture format required features of configuration.format with device.[[device]].
378        // 3. Validate texture format required features of each element of configuration.viewFormats with device.[[device]].
379        let (mut wgpu_descriptor, _) =
380            convert_texture_descriptor::<crate::DomTypeHolder>(&descriptor, device)?;
381        wgpu_descriptor.label = Some(Cow::Borrowed(
382            "dummy texture for texture descriptor validation",
383        ));
384
385        // 4. If Supported context formats does not contain configuration.format, throw a TypeError
386        if !supported_context_format(configuration.format) {
387            return Err(Error::Type(cformat!(
388                "Unsupported context format: {:?}",
389                configuration.format
390            )));
391        }
392
393        // 5. If configuration.usage includes the TRANSIENT_ATTACHMENT bit, throw a TypeError.
394        if configuration.usage & GPUTextureUsageConstants::TRANSIENT_ATTACHMENT != 0 {
395            return Err(Error::Type(
396                c"configuration.usage includes the TRANSIENT_ATTACHMENT bit".into(),
397            ));
398        }
399
400        // 7. Let this.[[configuration]] to configuration.
401        self.configuration.replace(Some(configuration.into()));
402
403        // 8. Set this.[[textureDescriptor]] to descriptor.
404        self.texture_descriptor.replace(Some(descriptor));
405
406        // 9. Replace the drawing buffer of this.
407        self.replace_drawing_buffer();
408
409        // 10. Issue the subsequent steps on the Device timeline of device.
410        // 10.1. Validate texture descriptor
411        let texture_id = self.global().wgpu_id_hub().create_texture_id();
412        self.droppable
413            .channel
414            .0
415            .send(WebGPURequest::ValidateTextureDescriptor {
416                device_id: device.id().0,
417                texture_id,
418                descriptor: wgpu_descriptor,
419            })
420            .expect("Failed to create WebGPU SwapChain");
421
422        Ok(())
423    }
424
425    /// <https://gpuweb.github.io/gpuweb/#dom-gpucanvascontext-unconfigure>
426    fn Unconfigure(&self) {
427        // 1. Set this.[[configuration]] to null.
428        self.configuration.take();
429        // 2. Set this.[[textureDescriptor]] to null.
430        self.current_texture.take();
431        // 3. Replace the drawing buffer of this.
432        self.replace_drawing_buffer();
433    }
434
435    /// <https://www.w3.org/TR/webgpu/#dom-gpucanvascontext-getconfiguration>
436    fn GetConfiguration(&self) -> Option<RootedGPUCanvasConfiguration> {
437        self.configuration
438            .borrow()
439            .as_ref()
440            .map(|configuration| configuration.root())
441    }
442
443    /// <https://gpuweb.github.io/gpuweb/#dom-gpucanvascontext-getcurrenttexture>
444    fn GetCurrentTexture(&self, cx: &mut JSContext) -> Fallible<DomRoot<GPUTexture>> {
445        // 1. If this.[[configuration]] is null, throw an InvalidStateError and return.
446        let configuration = self.configuration.borrow();
447        let Some(configuration) = configuration.as_ref() else {
448            return Err(Error::InvalidState(Some(
449                "GPUCanvasContext is not configured".into(),
450            )));
451        };
452        // 2. Assert this.[[textureDescriptor]] is not null.
453        let texture_descriptor = self.texture_descriptor.borrow();
454        let texture_descriptor = texture_descriptor.as_ref().unwrap();
455        // 3. Let device be this.[[configuration]].device.
456        let device = &configuration.device;
457        let current_texture = if let Some(current_texture) = self.current_texture.get() {
458            current_texture
459        } else {
460            // If this.[[currentTexture]] is null:
461            // 4.1. Replace the drawing buffer of this.
462            self.replace_drawing_buffer();
463            // 4.2. Set this.[[currentTexture]] to the result of calling device.createTexture() with this.[[textureDescriptor]],
464            // except with the GPUTexture’s underlying storage pointing to this.[[drawingBuffer]].
465            let current_texture = device.CreateTexture(cx, texture_descriptor)?;
466            self.current_texture.set(Some(&current_texture));
467
468            // The content of the texture is the content of the canvas.
469            self.cleared.set(false);
470
471            current_texture
472        };
473        // 6. Return this.[[currentTexture]].
474        Ok(current_texture)
475    }
476}