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