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