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