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