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