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