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