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