Skip to main content

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