Skip to main content

script_webgpu/
gpubuffer.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::ops::Range;
6use std::rc::Rc;
7
8use dom_struct::dom_struct;
9use js::context::{JSContext, NoGC};
10use js::realm::CurrentRealm;
11use js::typedarray::HeapArrayBuffer;
12use jstraceable_derive::JSTraceable;
13use log::{error, warn};
14use malloc_size_of_derive::MallocSizeOf;
15use script_bindings::DomTypes;
16use script_bindings::cell::DomRefCell;
17use script_bindings::codegen::GenericBindings::WebGPUBinding::{
18    GPUBufferDescriptor, GPUBufferMapState, GPUBufferMethods, GPUBufferWrap, GPUFlagsConstant,
19    GPUMapModeConstants, GPUMapModeFlags, GPUSize64,
20};
21use script_bindings::error::{Error, Fallible};
22use script_bindings::interfaces::PromiseHelpers;
23use script_bindings::reflector::{DomGlobalGeneric, Reflector, reflect_dom_object_with_wrap};
24use script_bindings::trace::RootedTraceableBox;
25use servo_base::generic_channel::GenericSharedMemory;
26use webgpu_traits::{Mapping, WebGPU, WebGPUBuffer, WebGPURequest};
27use wgpu_core::device::HostMap;
28
29use crate::datablock::DataBlock;
30use crate::dom::bindings::root::{Dom, DomRoot};
31use crate::dom::bindings::str::USVString;
32use crate::gpuconvert::WebGPUConvert;
33use crate::traits::{GPUDeviceTrait, WebGPUGlobalTrait, WebGPUPromiseTrait};
34
35#[derive(JSTraceable, MallocSizeOf)]
36#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
37pub(crate) struct ActiveBufferMapping {
38    // TODO(sagudev): Use GenericSharedMemory when https://github.com/servo/ipc-channel/pull/356 lands
39    /// <https://gpuweb.github.io/gpuweb/#active-buffer-mapping-data>
40    /// <https://gpuweb.github.io/gpuweb/#active-buffer-mapping-views>
41    pub(crate) data: DataBlock,
42    /// <https://gpuweb.github.io/gpuweb/#active-buffer-mapping-mode>
43    mode: GPUMapModeFlags,
44    /// <https://gpuweb.github.io/gpuweb/#active-buffer-mapping-range>
45    range: Range<u64>,
46}
47
48impl ActiveBufferMapping {
49    /// <https://gpuweb.github.io/gpuweb/#abstract-opdef-initialize-an-active-buffer-mapping>
50    pub(crate) fn new(
51        mode: GPUMapModeFlags,
52        range: Range<u64>,
53    ) -> Fallible<RootedTraceableBox<Self>> {
54        // Step 1
55        let size = range.end - range.start;
56        // Step 2
57        if size > (1 << 53) - 1 {
58            return Err(Error::Range(c"Over MAX_SAFE_INTEGER".to_owned()));
59        }
60        let size: usize = size
61            .try_into()
62            .map_err(|_| Error::Range(c"Over usize".to_owned()))?;
63        Ok(RootedTraceableBox::new(Self {
64            data: DataBlock::new_zeroed(size),
65            mode,
66            range,
67        }))
68    }
69}
70
71#[derive(JSTraceable, MallocSizeOf)]
72pub struct DroppableGPUBuffer {
73    #[no_trace]
74    channel: WebGPU,
75    #[no_trace]
76    buffer: WebGPUBuffer,
77}
78
79impl Drop for DroppableGPUBuffer {
80    fn drop(&mut self) {
81        if let Err(e) = self
82            .channel
83            .0
84            .send(WebGPURequest::DropBuffer(self.buffer.0))
85        {
86            error!(
87                "Failed to send WebGPURequest::DropBuffer({:?}) ({}) - Potential leak",
88                self.buffer.0, e
89            );
90        }
91    }
92}
93
94#[dom_struct]
95pub struct GPUBuffer<D: DomTypes> {
96    reflector_: Reflector,
97    droppable: DroppableGPUBuffer,
98    label: DomRefCell<USVString>,
99    device: Dom<D::GPUDevice>,
100    /// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-size>
101    size: GPUSize64,
102    /// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-usage>
103    usage: GPUFlagsConstant,
104    /// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-pending_map-slot>
105    #[conditional_malloc_size_of]
106    pending_map: DomRefCell<Option<Rc<D::Promise>>>,
107    /// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-mapping-slot>
108    mapping: DomRefCell<Option<ActiveBufferMapping>>,
109}
110
111impl<D> GPUBuffer<D>
112where
113    D: DomTypes<GPUBuffer = GPUBuffer<D>>,
114    D::Promise: PromiseHelpers<D>,
115{
116    fn new_inherited(
117        channel: WebGPU,
118        buffer: WebGPUBuffer,
119        device: &D::GPUDevice,
120        size: GPUSize64,
121        usage: GPUFlagsConstant,
122        mapping: Option<RootedTraceableBox<ActiveBufferMapping>>,
123        label: USVString,
124    ) -> Self {
125        Self {
126            reflector_: Reflector::new(),
127            droppable: DroppableGPUBuffer { channel, buffer },
128            label: DomRefCell::new(label),
129            device: Dom::from_ref(device),
130            pending_map: DomRefCell::new(None),
131            size,
132            usage,
133            mapping: DomRefCell::new(mapping.map(|mapping| *mapping.into_box())),
134        }
135    }
136
137    #[allow(clippy::too_many_arguments)]
138    pub(crate) fn new(
139        cx: &mut js::context::JSContext,
140        global: &D::GlobalScope,
141        channel: WebGPU,
142        buffer: WebGPUBuffer,
143        device: &D::GPUDevice,
144        size: GPUSize64,
145        usage: GPUFlagsConstant,
146        mapping: Option<RootedTraceableBox<ActiveBufferMapping>>,
147        label: USVString,
148    ) -> DomRoot<Self> {
149        reflect_dom_object_with_wrap::<D, _, _>(
150            Box::new(GPUBuffer::new_inherited(
151                channel, buffer, device, size, usage, mapping, label,
152            )),
153            global,
154            cx,
155            GPUBufferWrap::<D>,
156        )
157    }
158}
159
160impl<D> GPUBuffer<D>
161where
162    D: DomTypes<GPUBuffer = GPUBuffer<D>>,
163    D::GPUDevice: DomGlobalGeneric<D> + GPUDeviceTrait<D>,
164    D::GlobalScope: WebGPUGlobalTrait,
165    D::Promise: PromiseHelpers<D>,
166{
167    pub fn id(&self) -> WebGPUBuffer {
168        self.droppable.buffer
169    }
170
171    /// <https://gpuweb.github.io/gpuweb/#dom-gpudevice-createbuffer>
172    pub fn create(
173        cx: &mut js::context::JSContext,
174        device: &D::GPUDevice,
175        descriptor: &GPUBufferDescriptor,
176    ) -> Fallible<DomRoot<GPUBuffer<D>>> {
177        let desc = wgpu_types::BufferDescriptor {
178            label: (&descriptor.parent).convert(),
179            size: descriptor.size as wgpu_types::BufferAddress,
180            usage: wgpu_types::BufferUsages::from_bits_retain(descriptor.usage),
181            mapped_at_creation: descriptor.mappedAtCreation,
182        };
183        let id = <D::GPUDevice as DomGlobalGeneric<D>>::global_from_reflector(device)
184            .global_wgpu_id_hub()
185            .create_buffer_id();
186
187        device
188            .channel()
189            .0
190            .send(WebGPURequest::CreateBuffer {
191                device_id: device.id().0,
192                buffer_id: id,
193                descriptor: desc,
194            })
195            .expect("Failed to create WebGPU buffer");
196
197        let buffer = WebGPUBuffer(id);
198        let mapping = if descriptor.mappedAtCreation {
199            Some(ActiveBufferMapping::new(
200                GPUMapModeConstants::WRITE,
201                0..descriptor.size,
202            )?)
203        } else {
204            None
205        };
206
207        let global = <D::GPUDevice as DomGlobalGeneric<D>>::global_from_reflector(device);
208        Ok(GPUBuffer::new(
209            cx,
210            &*global,
211            device.channel(),
212            buffer,
213            device,
214            descriptor.size,
215            descriptor.usage,
216            mapping,
217            descriptor.parent.label.clone(),
218        ))
219    }
220}
221
222impl<D> GPUBufferMethods<D> for GPUBuffer<D>
223where
224    D: DomTypes<GPUBuffer = GPUBuffer<D>>,
225    D::Promise: PromiseHelpers<D> + WebGPUPromiseTrait<D> + PartialEq,
226    D::GPUDevice: GPUDeviceTrait<D>,
227    D::GPUDevice: DomGlobalGeneric<D> + GPUDeviceTrait<D>,
228    D::GlobalScope: WebGPUGlobalTrait,
229    D::Promise: PromiseHelpers<D>,
230{
231    /// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-unmap>
232    fn Unmap(&self, cx: &mut js::context::JSContext) {
233        // Step 1
234        let promise = self.pending_map.safe_borrow_mut(cx).take();
235        if let Some(promise) = promise {
236            promise.reject_error(cx, Error::Abort(Some("No pending map".into())));
237        }
238        // Step 2
239        let mut mapping = RootedTraceableBox::new(self.mapping.safe_borrow_mut(cx).take());
240        let mapping = if let Some(mapping) = mapping.as_mut() {
241            mapping
242        } else {
243            return;
244        };
245
246        // Step 3
247        mapping.data.clear_views(cx);
248        // Step 5&7
249        if let Err(e) = self.droppable.channel.0.send(WebGPURequest::UnmapBuffer {
250            buffer_id: self.id().0,
251            mapping: if mapping.mode >= GPUMapModeConstants::WRITE {
252                Some(Mapping {
253                    data: GenericSharedMemory::from_bytes(mapping.data.data()),
254                    range: mapping.range.clone(),
255                    mode: HostMap::Write,
256                })
257            } else {
258                None
259            },
260        }) {
261            warn!(
262                "Failed to send Buffer unmap ({:?}) ({})",
263                self.droppable.buffer.0, e
264            );
265        }
266    }
267
268    /// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-destroy>
269    fn Destroy(&self, cx: &mut JSContext) {
270        // Step 1
271        self.Unmap(cx);
272        // Step 2
273        if let Err(e) = self
274            .droppable
275            .channel
276            .0
277            .send(WebGPURequest::DestroyBuffer(self.droppable.buffer.0))
278        {
279            warn!(
280                "Failed to send WebGPURequest::DestroyBuffer({:?}) ({})",
281                self.droppable.buffer.0, e
282            );
283        };
284    }
285
286    /// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-mapasync>
287    fn MapAsync(
288        &self,
289        cx: &mut CurrentRealm<'_>,
290        mode: u32,
291        offset: GPUSize64,
292        size: Option<GPUSize64>,
293    ) -> Rc<D::Promise> {
294        let promise = D::Promise::new_in_realm(cx);
295        // Step 2
296        if self.pending_map.borrow().is_some() {
297            promise.reject_error(
298                cx,
299                Error::Operation(Some("There is already an active map".into())),
300            );
301            return promise;
302        }
303        // Step 4
304        *self.pending_map.safe_borrow_mut(cx) = Some(promise.clone());
305        // Step 5
306        let host_map = match mode {
307            GPUMapModeConstants::READ => HostMap::Read,
308            GPUMapModeConstants::WRITE => HostMap::Write,
309            _ => {
310                self.device
311                    .dispatch_error(webgpu_traits::Error::Validation(String::from(
312                        "Invalid MapModeFlags",
313                    )));
314                self.map_failure(cx, &promise);
315                return promise;
316            },
317        };
318
319        let callback = D::Promise::callback_promise_gpubuffer(&promise, self);
320        if let Err(e) = self
321            .droppable
322            .channel
323            .0
324            .send(WebGPURequest::BufferMapAsync {
325                callback,
326                buffer_id: self.droppable.buffer.0,
327                device_id: self.device.id().0,
328                host_map,
329                offset,
330                size,
331            })
332        {
333            warn!(
334                "Failed to send BufferMapAsync ({:?}) ({})",
335                self.droppable.buffer.0, e
336            );
337            self.map_failure(cx, &promise);
338            return promise;
339        }
340        // Step 6
341        promise
342    }
343
344    /// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-getmappedrange>
345    fn GetMappedRange(
346        &self,
347        cx: &mut js::context::JSContext,
348        offset: GPUSize64,
349        size: Option<GPUSize64>,
350    ) -> Fallible<RootedTraceableBox<HeapArrayBuffer>> {
351        let range_size = if let Some(s) = size {
352            s
353        } else {
354            self.size.saturating_sub(offset)
355        };
356        // Step 2: validation
357        let mut mapping = self
358            .mapping
359            .safe_borrow_mut(cx)
360            .take()
361            .map(RootedTraceableBox::new)
362            .ok_or(Error::Operation(Some("No active buffer map".into())))?;
363
364        let valid = offset.is_multiple_of(wgpu_types::MAP_ALIGNMENT) &&
365            range_size % wgpu_types::COPY_BUFFER_ALIGNMENT == 0 &&
366            offset >= mapping.range.start &&
367            offset + range_size <= mapping.range.end;
368        if !valid {
369            self.mapping
370                .safe_borrow_mut(cx)
371                .replace(*mapping.into_box());
372            return Err(Error::Operation(Some(
373                "Buffer Mapping is not active".into(),
374            )));
375        }
376
377        // Step 4
378        // only mapping.range is mapped with mapping.range.start at 0
379        // so we need to rebase range to mapped.range
380        let rebased_offset = (offset - mapping.range.start) as usize;
381        let result = mapping
382            .data
383            .view(cx, rebased_offset..rebased_offset + range_size as usize)
384            .map(|view| view.array_buffer())
385            .map_err(|()| {
386                Error::Operation(Some(
387                    "Mapped range overlaps with others or is out of bounds.".into(),
388                ))
389            });
390
391        self.mapping
392            .safe_borrow_mut(cx)
393            .replace(*mapping.into_box());
394        result
395    }
396
397    /// <https://gpuweb.github.io/gpuweb/#dom-gpuobjectbase-label>
398    fn Label(&self) -> USVString {
399        self.label.borrow().clone()
400    }
401
402    /// <https://gpuweb.github.io/gpuweb/#dom-gpuobjectbase-label>
403    fn SetLabel(&self, no_gc: &NoGC, value: USVString) {
404        *self.label.safe_borrow_mut(no_gc) = value;
405    }
406
407    /// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-size>
408    fn Size(&self) -> GPUSize64 {
409        self.size
410    }
411
412    /// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-usage>
413    fn Usage(&self) -> GPUFlagsConstant {
414        self.usage
415    }
416
417    /// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-mapstate>
418    fn MapState(&self) -> GPUBufferMapState {
419        // Step 1&2&3
420        if self.mapping.borrow().is_some() {
421            GPUBufferMapState::Mapped
422        } else if self.pending_map.borrow().is_some() {
423            GPUBufferMapState::Pending
424        } else {
425            GPUBufferMapState::Unmapped
426        }
427    }
428}
429
430impl<D> GPUBuffer<D>
431where
432    D: DomTypes,
433    D::Promise: PromiseHelpers<D> + PartialEq,
434    D::GPUDevice: GPUDeviceTrait<D>,
435{
436    pub fn map_failure(&self, cx: &mut JSContext, p: &Rc<D::Promise>) {
437        // Step 1
438        if self.pending_map.borrow().as_ref() != Some(p) {
439            assert!(p.is_rejected());
440            return;
441        }
442        // Step 2
443        assert!(p.is_pending());
444        // Step 3
445        self.pending_map.safe_borrow_mut(cx).take();
446        // Step 4
447        let is_lost = self.device.is_lost();
448        if is_lost {
449            p.reject_error(cx, Error::Abort(Some("GPUDevice is lost".into())));
450        } else {
451            p.reject_error(cx, Error::Operation(Some("Mapping failure".into())));
452        }
453    }
454
455    pub fn map_success(
456        &self,
457        cx: &mut js::context::JSContext,
458        p: &Rc<D::Promise>,
459        wgpu_mapping: Mapping,
460    ) {
461        // Step 1
462        if self.pending_map.borrow().as_ref() != Some(p) {
463            assert!(p.is_rejected());
464            return;
465        }
466
467        // Step 2
468        assert!(p.is_pending());
469
470        // Step 4
471        let mapping = ActiveBufferMapping::new(
472            match wgpu_mapping.mode {
473                HostMap::Read => GPUMapModeConstants::READ,
474                HostMap::Write => GPUMapModeConstants::WRITE,
475            },
476            wgpu_mapping.range,
477        );
478
479        match mapping {
480            Err(error) => {
481                *self.pending_map.safe_borrow_mut(cx) = None;
482                p.reject_error(cx, error);
483            },
484            Ok(mut mapping) => {
485                // Step 5
486                mapping.data.load(&wgpu_mapping.data);
487                // Step 6
488                self.mapping
489                    .safe_borrow_mut(cx)
490                    .replace(*mapping.into_box());
491                // Step 7
492                self.pending_map.safe_borrow_mut(cx).take();
493                p.resolve_native(cx, &());
494            },
495        }
496    }
497}