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