Skip to main content

webgpu/
canvas_context.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
5//! Main process implementation of [GPUCanvasContext](https://www.w3.org/TR/webgpu/#canvas-context)
6
7use std::ptr::NonNull;
8use std::sync::{Arc, Mutex};
9
10use arrayvec::ArrayVec;
11use euclid::default::Size2D;
12use log::warn;
13use paint_api::{
14    CrossProcessPaintApi, ExternalImageSource, SerializableImageData, WebRenderExternalImageApi,
15};
16use pixels::{SharedSnapshot, Snapshot, SnapshotAlphaMode, SnapshotPixelFormat};
17use rustc_hash::FxHashMap;
18use servo_base::Epoch;
19use servo_base::generic_channel::GenericSender;
20use webgpu_traits::{
21    ContextConfiguration, PRESENTATION_BUFFER_COUNT, PendingTexture, WebGPUContextId, WebGPUMsg,
22};
23use webrender_api::units::DeviceIntSize;
24use webrender_api::{
25    ExternalImageData, ExternalImageId, ExternalImageType, ImageDescriptor, ImageDescriptorFlags,
26    ImageFormat, ImageKey,
27};
28use wgpu_core::device::HostMap;
29use wgpu_core::global::Global;
30use wgpu_core::id::{
31    self, BufferId, CommandBufferId, CommandEncoderId, DeviceId, QueueId, TextureId,
32};
33use wgpu_core::resource::{
34    BufferAccessError, BufferDescriptor, BufferMapOperation, CreateBufferError,
35};
36use wgpu_types::{
37    BufferUsages, COPY_BYTES_PER_ROW_ALIGNMENT, CommandBufferDescriptor, CommandEncoderDescriptor,
38    Extent3d, Origin3d, TexelCopyBufferInfo, TexelCopyBufferLayout, TexelCopyTextureInfo,
39    TextureAspect,
40};
41
42pub type WebGpuExternalImageMap = Arc<Mutex<FxHashMap<WebGPUContextId, ContextData>>>;
43
44const fn image_data(context_id: WebGPUContextId) -> ExternalImageData {
45    ExternalImageData {
46        id: ExternalImageId(context_id.0),
47        channel_index: 0,
48        image_type: ExternalImageType::Buffer,
49        normalized_uvs: false,
50    }
51}
52
53/// Allocated buffer on GPU device
54#[derive(Clone, Copy, Debug)]
55struct Buffer {
56    device_id: DeviceId,
57    queue_id: QueueId,
58    size: u64,
59}
60
61impl Buffer {
62    /// Returns true if buffer is compatible with provided configuration
63    fn has_compatible_config(&self, config: &ContextConfiguration) -> bool {
64        config.device_id == self.device_id && self.size == config.buffer_size()
65    }
66}
67
68/// Mapped GPUBuffer
69#[derive(Debug)]
70struct MappedBuffer {
71    buffer: Buffer,
72    data: NonNull<u8>,
73    len: u64,
74    image_size: Size2D<u32>,
75    image_format: ImageFormat,
76    is_opaque: bool,
77}
78
79// Mapped buffer can be shared between safely (it's read-only)
80unsafe impl Send for MappedBuffer {}
81
82impl MappedBuffer {
83    const fn slice(&'_ self) -> &'_ [u8] {
84        // Safety: Pointer is from wgpu, and we only use it here
85        unsafe { std::slice::from_raw_parts(self.data.as_ptr(), self.len as usize) }
86    }
87
88    fn stride(&self) -> u32 {
89        (self.image_size.width * self.image_format.bytes_per_pixel() as u32)
90            .next_multiple_of(COPY_BYTES_PER_ROW_ALIGNMENT)
91    }
92}
93
94#[derive(Debug)]
95enum StagingBufferState {
96    /// The Initial state: the buffer has yet to be created with only an
97    /// id reserved for it.
98    Unassigned,
99    /// The buffer is allocated in the WGPU Device and is ready to be used.
100    Available(Buffer),
101    /// `mapAsync` is currently running on the buffer.
102    Mapping(Buffer),
103    /// The buffer is currently mapped.
104    Mapped(MappedBuffer),
105}
106
107/// A staging buffer used for texture to buffer to CPU copy operations.
108#[derive(Debug)]
109struct StagingBuffer {
110    global: Arc<Global>,
111    buffer_id: BufferId,
112    state: StagingBufferState,
113}
114
115// [`StagingBuffer`] only used for reading (never for writing)
116// so it is safe to share between threads.
117unsafe impl Sync for StagingBuffer {}
118
119impl StagingBuffer {
120    fn new(global: Arc<Global>, buffer_id: BufferId) -> Self {
121        Self {
122            global,
123            buffer_id,
124            state: StagingBufferState::Unassigned,
125        }
126    }
127
128    const fn is_mapped(&self) -> bool {
129        matches!(self.state, StagingBufferState::Mapped(..))
130    }
131
132    /// Return true if buffer can be used directly with provided config
133    /// without any additional work
134    fn is_available_and_has_compatible_config(&self, config: &ContextConfiguration) -> bool {
135        let StagingBufferState::Available(buffer) = &self.state else {
136            return false;
137        };
138        buffer.has_compatible_config(config)
139    }
140
141    /// Return true if buffer is not mapping or being mapped
142    const fn needs_assignment(&self) -> bool {
143        matches!(
144            self.state,
145            StagingBufferState::Unassigned | StagingBufferState::Available(_)
146        )
147    }
148
149    /// Make buffer available by unmapping / destroying it and then recreating it if needed.
150    fn ensure_available(&mut self, config: &ContextConfiguration) -> Result<(), CreateBufferError> {
151        let recreate = match &self.state {
152            StagingBufferState::Unassigned => true,
153            StagingBufferState::Available(buffer) |
154            StagingBufferState::Mapping(buffer) |
155            StagingBufferState::Mapped(MappedBuffer { buffer, .. }) => {
156                if buffer.has_compatible_config(config) {
157                    let _ = self.global.buffer_unmap(self.buffer_id);
158                    false
159                } else {
160                    self.global.buffer_drop(self.buffer_id);
161                    true
162                }
163            },
164        };
165        if recreate {
166            let buffer_size = config.buffer_size();
167            let (_, error) = self.global.device_create_buffer(
168                config.device_id,
169                &BufferDescriptor {
170                    label: None,
171                    size: buffer_size,
172                    usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
173                    mapped_at_creation: false,
174                },
175                Some(self.buffer_id),
176            );
177            if let Some(error) = error {
178                return Err(error);
179            };
180            self.state = StagingBufferState::Available(Buffer {
181                device_id: config.device_id,
182                queue_id: config.queue_id,
183                size: buffer_size,
184            });
185        }
186        Ok(())
187    }
188
189    /// Makes buffer available and prepares command encoder
190    /// that will copy texture to this staging buffer.
191    ///
192    /// Caller must submit command buffer to queue.
193    fn prepare_load_texture_command_buffer(
194        &mut self,
195        texture_id: TextureId,
196        encoder_id: CommandEncoderId,
197        command_buffer_id: CommandBufferId,
198        config: &ContextConfiguration,
199    ) -> Result<CommandBufferId, Box<dyn std::error::Error>> {
200        self.ensure_available(config)?;
201        let StagingBufferState::Available(buffer) = &self.state else {
202            unreachable!("Should be made available by `ensure_available`")
203        };
204        let device_id = buffer.device_id;
205        let command_descriptor = CommandEncoderDescriptor { label: None };
206        let (encoder_id, error) = self.global.device_create_command_encoder(
207            device_id,
208            &command_descriptor,
209            Some(encoder_id),
210        );
211        if let Some(error) = error {
212            return Err(error.into());
213        };
214        let buffer_info = TexelCopyBufferInfo {
215            buffer: self.buffer_id,
216            layout: TexelCopyBufferLayout {
217                offset: 0,
218                bytes_per_row: Some(config.stride()),
219                rows_per_image: None,
220            },
221        };
222        let texture_info = TexelCopyTextureInfo {
223            texture: texture_id,
224            mip_level: 0,
225            origin: Origin3d::ZERO,
226            aspect: TextureAspect::All,
227        };
228        let copy_size = Extent3d {
229            width: config.size.width,
230            height: config.size.height,
231            depth_or_array_layers: 1,
232        };
233        self.global.command_encoder_copy_texture_to_buffer(
234            encoder_id,
235            &texture_info,
236            &buffer_info,
237            &copy_size,
238        )?;
239        let (command_buffer_id, error) = self.global.command_encoder_finish(
240            encoder_id,
241            &CommandBufferDescriptor::default(),
242            Some(command_buffer_id),
243        );
244        if let Some((_, error)) = error {
245            return Err(error.into());
246        };
247        Ok(command_buffer_id)
248    }
249
250    /// Unmaps the buffer or cancels a mapping operation if one is in progress.
251    fn unmap(&mut self) {
252        match self.state {
253            StagingBufferState::Unassigned | StagingBufferState::Available(_) => {},
254            StagingBufferState::Mapping(buffer) |
255            StagingBufferState::Mapped(MappedBuffer { buffer, .. }) => {
256                let _ = self.global.buffer_unmap(self.buffer_id);
257                self.state = StagingBufferState::Available(buffer)
258            },
259        }
260    }
261
262    /// Obtain a snapshot from this buffer if is mapped or return `None` if it is not mapped.
263    fn snapshot(&self) -> Option<Snapshot> {
264        let StagingBufferState::Mapped(mapped) = &self.state else {
265            return None;
266        };
267        let format = match mapped.image_format {
268            ImageFormat::RGBA8 => SnapshotPixelFormat::RGBA,
269            ImageFormat::BGRA8 => SnapshotPixelFormat::BGRA,
270            _ => unreachable!("GPUCanvasContext does not support other formats per spec"),
271        };
272        let alpha_mode = if mapped.is_opaque {
273            SnapshotAlphaMode::AsOpaque {
274                premultiplied: false,
275            }
276        } else {
277            SnapshotAlphaMode::Transparent {
278                premultiplied: true,
279            }
280        };
281        let padded_byte_width = mapped.stride();
282        let data = mapped.slice();
283        let bytes_per_pixel = mapped.image_format.bytes_per_pixel() as usize;
284        let mut result_unpadded =
285            Vec::<u8>::with_capacity(mapped.image_size.area() as usize * bytes_per_pixel);
286        for row in 0..mapped.image_size.height {
287            let start = (row * padded_byte_width).try_into().ok()?;
288            result_unpadded
289                .extend(&data[start..start + mapped.image_size.width as usize * bytes_per_pixel]);
290        }
291        let mut snapshot =
292            Snapshot::from_vec(mapped.image_size, format, alpha_mode, result_unpadded);
293        if mapped.is_opaque {
294            snapshot.transform(SnapshotAlphaMode::Opaque, snapshot.format())
295        }
296        Some(snapshot)
297    }
298}
299
300impl Drop for StagingBuffer {
301    fn drop(&mut self) {
302        match self.state {
303            StagingBufferState::Unassigned => {},
304            StagingBufferState::Available(_) |
305            StagingBufferState::Mapping(_) |
306            StagingBufferState::Mapped(_) => {
307                self.global.buffer_drop(self.buffer_id);
308            },
309        }
310    }
311}
312
313pub struct WebGpuExternalImages {
314    pub image_map: WebGpuExternalImageMap,
315    pub locked_ids: FxHashMap<WebGPUContextId, PresentationStagingBuffer>,
316}
317
318impl WebGpuExternalImages {
319    pub fn new(image_map: WebGpuExternalImageMap) -> Self {
320        Self {
321            image_map,
322            locked_ids: Default::default(),
323        }
324    }
325}
326
327impl WebRenderExternalImageApi for WebGpuExternalImages {
328    fn lock(&mut self, id: u64) -> (ExternalImageSource<'_>, Size2D<i32>) {
329        let id = WebGPUContextId(id);
330        let presentation = {
331            let mut webgpu_contexts = self.image_map.lock().unwrap();
332            webgpu_contexts
333                .get_mut(&id)
334                .and_then(|context_data| context_data.presentation.clone())
335        };
336        let Some(presentation) = presentation else {
337            return (ExternalImageSource::Invalid, Size2D::zero());
338        };
339        self.locked_ids.insert(id, presentation);
340        let presentation = self.locked_ids.get(&id).unwrap();
341        let StagingBufferState::Mapped(mapped_buffer) = &presentation.staging_buffer.state else {
342            unreachable!("Presentation staging buffer should be mapped")
343        };
344        let size = mapped_buffer.image_size;
345        (
346            ExternalImageSource::RawData(mapped_buffer.slice()),
347            size.cast().cast_unit(),
348        )
349    }
350
351    fn unlock(&mut self, id: u64) {
352        let id = WebGPUContextId(id);
353        let Some(presentation) = self.locked_ids.remove(&id) else {
354            return;
355        };
356        let mut webgpu_contexts = self.image_map.lock().unwrap();
357        if let Some(context_data) = webgpu_contexts.get_mut(&id) {
358            // We use this to return staging buffer if a newer one exists.
359            presentation.maybe_destroy(context_data);
360        } else {
361            // This will not free this buffer id in script,
362            // but that's okay because we still have many free ids.
363            drop(presentation);
364        }
365    }
366}
367
368/// Staging buffer currently used for presenting the epoch.
369///
370/// Users should [`ContextData::replace_presentation`] when done.
371#[derive(Clone)]
372pub struct PresentationStagingBuffer {
373    epoch: Epoch,
374    staging_buffer: Arc<StagingBuffer>,
375}
376
377impl PresentationStagingBuffer {
378    fn new(epoch: Epoch, staging_buffer: StagingBuffer) -> Self {
379        Self {
380            epoch,
381            staging_buffer: Arc::new(staging_buffer),
382        }
383    }
384
385    /// If the internal staging buffer is not shared,
386    /// unmap it and call [`ContextData::return_staging_buffer`] with it.
387    fn maybe_destroy(self, context_data: &mut ContextData) {
388        if let Some(mut staging_buffer) = Arc::into_inner(self.staging_buffer) {
389            staging_buffer.unmap();
390            context_data.return_staging_buffer(staging_buffer);
391        }
392    }
393}
394
395/// The embedder process-side representation of what is the `GPUCanvasContext` in script.
396pub struct ContextData {
397    /// The [`ImageKey`] of the WebRender image associated with this context.
398    image_key: Option<ImageKey>,
399    /// The current size of this context.
400    size: DeviceIntSize,
401    /// Staging buffers that are not actively used.
402    ///
403    /// Staging buffer here are either [`StagingBufferState::Unassigned`] or [`StagingBufferState::Available`].
404    /// They are removed from here when they are in process of being mapped or are already mapped.
405    inactive_staging_buffers: ArrayVec<StagingBuffer, PRESENTATION_BUFFER_COUNT>,
406    /// The [`PresentationStagingBuffer`] of the most recent presentation. This will
407    /// be `None` directly after initialization, as clearing is handled completely in
408    /// the `ScriptThread`.
409    presentation: Option<PresentationStagingBuffer>,
410    /// Next epoch to be used
411    next_epoch: Epoch,
412}
413
414impl ContextData {
415    fn new(
416        global: &Arc<Global>,
417        buffer_ids: ArrayVec<id::BufferId, PRESENTATION_BUFFER_COUNT>,
418        size: DeviceIntSize,
419    ) -> Self {
420        Self {
421            image_key: None,
422            size,
423            inactive_staging_buffers: buffer_ids
424                .iter()
425                .map(|buffer_id| StagingBuffer::new(global.clone(), *buffer_id))
426                .collect(),
427            presentation: None,
428            next_epoch: Epoch(1),
429        }
430    }
431
432    /// Returns `None` if no staging buffer is unused or failure when making it available
433    fn get_or_make_available_buffer(
434        &'_ mut self,
435        config: &ContextConfiguration,
436    ) -> Option<StagingBuffer> {
437        self.inactive_staging_buffers
438            .iter()
439            // Try to get first preallocated GPUBuffer.
440            .position(|staging_buffer| {
441                staging_buffer.is_available_and_has_compatible_config(config)
442            })
443            // Fall back to the first inactive staging buffer.
444            .or_else(|| {
445                self.inactive_staging_buffers
446                    .iter()
447                    .position(|staging_buffer| staging_buffer.needs_assignment())
448            })
449            // Or just the use first one.
450            .or_else(|| {
451                if self.inactive_staging_buffers.is_empty() {
452                    None
453                } else {
454                    Some(0)
455                }
456            })
457            .and_then(|index| {
458                let mut staging_buffer = self.inactive_staging_buffers.remove(index);
459                if staging_buffer.ensure_available(config).is_ok() {
460                    Some(staging_buffer)
461                } else {
462                    // If we fail to make it available, return it to the list of inactive staging buffers.
463                    self.inactive_staging_buffers.push(staging_buffer);
464                    None
465                }
466            })
467    }
468
469    /// Destroy the context that this [`ContextData`] represents,
470    /// freeing all of its buffers, and deleting the associated WebRender image.
471    fn destroy(
472        mut self,
473        script_sender: &GenericSender<WebGPUMsg>,
474        paint_api: &CrossProcessPaintApi,
475    ) {
476        // This frees the id in the `ScriptThread`.
477        for staging_buffer in self.inactive_staging_buffers {
478            if let Err(error) = script_sender.send(WebGPUMsg::FreeBuffer(staging_buffer.buffer_id))
479            {
480                warn!(
481                    "Unable to send FreeBuffer({:?}) ({error})",
482                    staging_buffer.buffer_id
483                );
484            };
485        }
486        if let Some(image_key) = self.image_key.take() {
487            paint_api.delete_image(image_key);
488        }
489    }
490
491    /// Advance the [`Epoch`] and return the new one.
492    fn next_epoch(&mut self) -> Epoch {
493        let epoch = self.next_epoch;
494        self.next_epoch.next();
495        epoch
496    }
497
498    /// If the given [`PresentationStagingBuffer`] is for a newer presentation, replace the existing
499    /// one. Deallocate the older one by calling [`Self::return_staging_buffer`] on it.
500    fn replace_presentation(&mut self, presentation: PresentationStagingBuffer) {
501        let stale_presentation = if presentation.epoch >=
502            self.presentation
503                .as_ref()
504                .map(|p| p.epoch)
505                .unwrap_or_default()
506        {
507            self.presentation.replace(presentation)
508        } else {
509            Some(presentation)
510        };
511        if let Some(stale_presentation) = stale_presentation {
512            stale_presentation.maybe_destroy(self);
513        }
514    }
515
516    fn clear_presentation(&mut self) {
517        if let Some(stale_presentation) = self.presentation.take() {
518            stale_presentation.maybe_destroy(self);
519        }
520    }
521
522    fn return_staging_buffer(&mut self, staging_buffer: StagingBuffer) {
523        self.inactive_staging_buffers.push(staging_buffer)
524    }
525}
526
527impl crate::WGPU {
528    pub(crate) fn create_context(
529        &self,
530        context_id: WebGPUContextId,
531        size: DeviceIntSize,
532        buffer_ids: ArrayVec<id::BufferId, PRESENTATION_BUFFER_COUNT>,
533    ) {
534        let context_data = ContextData::new(&self.global, buffer_ids, size);
535        assert!(
536            self.wgpu_image_map
537                .lock()
538                .unwrap()
539                .insert(context_id, context_data)
540                .is_none(),
541            "Context should be created only once!"
542        );
543    }
544
545    pub(crate) fn set_image_key(&self, context_id: WebGPUContextId, image_key: ImageKey) {
546        let mut webgpu_contexts = self.wgpu_image_map.lock().unwrap();
547        let context_data = webgpu_contexts.get_mut(&context_id).unwrap();
548
549        if let Some(old_image_key) = context_data.image_key.replace(image_key) {
550            self.paint_api.delete_image(old_image_key);
551        }
552
553        self.paint_api.add_image(
554            image_key,
555            ImageDescriptor {
556                format: ImageFormat::BGRA8,
557                size: context_data.size,
558                stride: None,
559                offset: 0,
560                flags: ImageDescriptorFlags::empty(),
561            },
562            SerializableImageData::External(image_data(context_id)),
563            false,
564        );
565    }
566
567    pub(crate) fn get_image(
568        &self,
569        context_id: WebGPUContextId,
570        pending_texture: Option<PendingTexture>,
571        sender: GenericSender<SharedSnapshot>,
572    ) {
573        let mut webgpu_contexts = self.wgpu_image_map.lock().unwrap();
574        let context_data = webgpu_contexts.get_mut(&context_id).unwrap();
575        if let Some(PendingTexture {
576            texture_id,
577            encoder_id,
578            command_buffer_id,
579            configuration,
580        }) = pending_texture
581        {
582            let Some(staging_buffer) = context_data.get_or_make_available_buffer(&configuration)
583            else {
584                warn!("Failure obtaining available staging buffer");
585                sender
586                    .send(SharedSnapshot::cleared(configuration.size))
587                    .unwrap();
588                return;
589            };
590
591            let epoch = context_data.next_epoch();
592            let wgpu_image_map = self.wgpu_image_map.clone();
593            let sender = sender;
594            drop(webgpu_contexts);
595            self.texture_download(
596                texture_id,
597                encoder_id,
598                command_buffer_id,
599                staging_buffer,
600                configuration,
601                move |staging_buffer| {
602                    let mut webgpu_contexts = wgpu_image_map.lock().unwrap();
603                    let context_data = webgpu_contexts.get_mut(&context_id).unwrap();
604                    sender
605                        .send(
606                            staging_buffer
607                                .snapshot()
608                                .as_ref()
609                                .map(Snapshot::to_shared)
610                                .unwrap_or_else(|| SharedSnapshot::cleared(configuration.size)),
611                        )
612                        .unwrap();
613                    if staging_buffer.is_mapped() {
614                        context_data.replace_presentation(PresentationStagingBuffer::new(
615                            epoch,
616                            staging_buffer,
617                        ));
618                    } else {
619                        // failure
620                        context_data.return_staging_buffer(staging_buffer);
621                    }
622                },
623            );
624        } else {
625            sender
626                .send(
627                    context_data
628                        .presentation
629                        .as_ref()
630                        .and_then(|presentation_staging_buffer| {
631                            presentation_staging_buffer.staging_buffer.snapshot()
632                        })
633                        .unwrap_or_else(Snapshot::empty)
634                        .to_shared(),
635                )
636                .unwrap();
637        }
638    }
639
640    /// Read the texture to the staging buffer, map it to CPU memory, and update the
641    /// image in WebRender when complete.
642    pub(crate) fn present(
643        &self,
644        context_id: WebGPUContextId,
645        pending_texture: Option<PendingTexture>,
646        size: Size2D<u32>,
647        canvas_epoch: Epoch,
648    ) {
649        let mut webgpu_contexts = self.wgpu_image_map.lock().unwrap();
650        let context_data = webgpu_contexts.get_mut(&context_id).unwrap();
651
652        let Some(image_key) = context_data.image_key else {
653            return;
654        };
655
656        let Some(PendingTexture {
657            texture_id,
658            encoder_id,
659            command_buffer_id,
660            configuration,
661        }) = pending_texture
662        else {
663            context_data.clear_presentation();
664            self.paint_api.update_image(
665                image_key,
666                ImageDescriptor {
667                    format: ImageFormat::BGRA8,
668                    size: size.cast_unit().cast(),
669                    stride: None,
670                    offset: 0,
671                    flags: ImageDescriptorFlags::empty(),
672                },
673                SerializableImageData::External(image_data(context_id)),
674                Some(canvas_epoch),
675            );
676            return;
677        };
678        let Some(staging_buffer) = context_data.get_or_make_available_buffer(&configuration) else {
679            warn!("Failure obtaining available staging buffer");
680            context_data.clear_presentation();
681            self.paint_api.update_image(
682                image_key,
683                configuration.into(),
684                SerializableImageData::External(image_data(context_id)),
685                Some(canvas_epoch),
686            );
687            return;
688        };
689        let epoch = context_data.next_epoch();
690        let wgpu_image_map = self.wgpu_image_map.clone();
691        let paint_api = self.paint_api.clone();
692        drop(webgpu_contexts);
693        self.texture_download(
694            texture_id,
695            encoder_id,
696            command_buffer_id,
697            staging_buffer,
698            configuration,
699            move |staging_buffer| {
700                let mut webgpu_contexts = wgpu_image_map.lock().unwrap();
701                let context_data = webgpu_contexts.get_mut(&context_id).unwrap();
702                if staging_buffer.is_mapped() {
703                    context_data.replace_presentation(PresentationStagingBuffer::new(
704                        epoch,
705                        staging_buffer,
706                    ));
707                } else {
708                    context_data.return_staging_buffer(staging_buffer);
709                    context_data.clear_presentation();
710                }
711                // update image in WR
712                paint_api.update_image(
713                    image_key,
714                    configuration.into(),
715                    SerializableImageData::External(image_data(context_id)),
716                    Some(canvas_epoch),
717                );
718            },
719        );
720    }
721
722    /// Copies data from provided texture using `encoder_id` to the provided [`StagingBuffer`].
723    ///
724    /// `callback` is guaranteed to be called.
725    ///
726    /// Returns a [`StagingBuffer`] with the [`StagingBufferState::Mapped`] state
727    /// on success or [`StagingBufferState::Available`] on failure.
728    fn texture_download(
729        &self,
730        texture_id: TextureId,
731        encoder_id: CommandEncoderId,
732        command_buffer_id: CommandBufferId,
733        mut staging_buffer: StagingBuffer,
734        config: ContextConfiguration,
735        callback: impl FnOnce(StagingBuffer) + Send + 'static,
736    ) {
737        let Ok(command_buffer_id) = staging_buffer.prepare_load_texture_command_buffer(
738            texture_id,
739            encoder_id,
740            command_buffer_id,
741            &config,
742        ) else {
743            return callback(staging_buffer);
744        };
745        let StagingBufferState::Available(buffer) = &staging_buffer.state else {
746            unreachable!("`prepare_load_texture_command_buffer` should make buffer available")
747        };
748        let buffer_id = staging_buffer.buffer_id;
749        let buffer_size = buffer.size;
750        {
751            let _guard = self.poller.lock();
752            let result = self
753                .global
754                .queue_submit(buffer.queue_id, &[command_buffer_id]);
755            if result.is_err() {
756                return callback(staging_buffer);
757            }
758        }
759        staging_buffer.state = match staging_buffer.state {
760            StagingBufferState::Available(buffer) => StagingBufferState::Mapping(buffer),
761            _ => unreachable!("`prepare_load_texture_command_buffer` should make buffer available"),
762        };
763        let map_callback = {
764            let token = self.poller.token();
765            Box::new(move |result: Result<(), BufferAccessError>| {
766                drop(token);
767                staging_buffer.state = match staging_buffer.state {
768                    StagingBufferState::Mapping(buffer) => {
769                        if let Ok((data, len)) = result.and_then(|_| {
770                            staging_buffer.global.buffer_get_mapped_range(
771                                staging_buffer.buffer_id,
772                                0,
773                                Some(buffer.size),
774                            )
775                        }) {
776                            StagingBufferState::Mapped(MappedBuffer {
777                                buffer,
778                                data,
779                                len,
780                                image_size: config.size,
781                                image_format: config.format,
782                                is_opaque: config.is_opaque,
783                            })
784                        } else {
785                            StagingBufferState::Available(buffer)
786                        }
787                    },
788                    _ => {
789                        unreachable!("Mapping buffer should have StagingBufferState::Mapping state")
790                    },
791                };
792                callback(staging_buffer);
793            })
794        };
795        let map_op = BufferMapOperation {
796            host: HostMap::Read,
797            callback: Some(map_callback),
798        };
799        // error is handled by map_callback
800        let _ = self
801            .global
802            .buffer_map_async(buffer_id, 0, Some(buffer_size), map_op);
803        self.poller.wake();
804    }
805
806    pub(crate) fn destroy_context(&mut self, context_id: WebGPUContextId) {
807        self.wgpu_image_map
808            .lock()
809            .unwrap()
810            .remove(&context_id)
811            .unwrap()
812            .destroy(&self.script_sender, &self.paint_api);
813    }
814}