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