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::routed_promise::RoutedPromiseListener;
24use script_bindings::trace::RootedTraceableBox;
25use servo_base::generic_channel::GenericSharedMemory;
26use webgpu_traits::{
27    BufferAccessError, BufferAddress, BufferDescriptor, BufferUpdate, BufferUsages,
28    COPY_BUFFER_ALIGNMENT, HostMap, MAP_ALIGNMENT, Mapping, WebGPU, WebGPUBuffer, WebGPURequest,
29};
30
31use crate::datablock::DataBlock;
32use crate::dom::bindings::root::{Dom, DomRoot};
33use crate::dom::bindings::str::USVString;
34use crate::gpuconvert::WebGPUConvert;
35use crate::traits::{Equivalence, WebGPUGlobalTrait, WebGPUPromise, WebGPUPromiseCallbackTrait};
36
37#[derive(JSTraceable, MallocSizeOf)]
38#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
39pub(crate) struct ActiveBufferMapping {
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        data: Option<GenericSharedMemory>,
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: data
67                .map(DataBlock::new_from_shared_memory)
68                .unwrap_or_else(|| DataBlock::new_zeroed(size)),
69            mode,
70            range,
71        }))
72    }
73
74    #[cfg_attr(
75        crown,
76        expect(
77            crown::unrooted_must_root,
78            reason = "No GC can happen when this is called"
79        )
80    )]
81    pub(crate) fn consume(self) -> (GenericSharedMemory, GPUMapModeFlags, Range<u64>) {
82        let ActiveBufferMapping { data, mode, range } = self;
83        (data.consume(), mode, range)
84    }
85}
86
87#[derive(JSTraceable, MallocSizeOf)]
88pub(crate) struct DroppableGPUBuffer {
89    #[no_trace]
90    channel: WebGPU,
91    #[no_trace]
92    buffer: WebGPUBuffer,
93}
94
95impl Drop for DroppableGPUBuffer {
96    fn drop(&mut self) {
97        if let Err(e) = self
98            .channel
99            .0
100            .send(WebGPURequest::DropBuffer(self.buffer.0))
101        {
102            error!(
103                "Failed to send WebGPURequest::DropBuffer({:?}) ({}) - Potential leak",
104                self.buffer.0, e
105            );
106        }
107    }
108}
109
110#[dom_struct]
111pub struct GPUBuffer<D: DomTypes> {
112    reflector_: Reflector,
113    droppable: DroppableGPUBuffer,
114    label: DomRefCell<USVString>,
115    device: Dom<D::GPUDevice>,
116    /// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-size>
117    size: GPUSize64,
118    /// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-usage>
119    usage: GPUFlagsConstant,
120    /// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-pending_map-slot>
121    pending_map: DomRefCell<Option<<D::Promise as PromiseHelpers<D>>::HeapTraced>>,
122    /// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-mapping-slot>
123    mapping: DomRefCell<Option<ActiveBufferMapping>>,
124}
125
126impl<D> GPUBuffer<D>
127where
128    D: Equivalence,
129    D::Promise: PromiseHelpers<D>,
130{
131    fn new_inherited(
132        channel: WebGPU,
133        buffer: WebGPUBuffer,
134        device: &D::GPUDevice,
135        size: GPUSize64,
136        usage: GPUFlagsConstant,
137        mapping: Option<RootedTraceableBox<ActiveBufferMapping>>,
138        label: USVString,
139    ) -> Self {
140        Self {
141            reflector_: Reflector::new(),
142            droppable: DroppableGPUBuffer { channel, buffer },
143            label: DomRefCell::new(label),
144            device: Dom::from_ref(device),
145            pending_map: DomRefCell::new(None),
146            size,
147            usage,
148            mapping: DomRefCell::new(mapping.map(|mapping| *mapping.into_box())),
149        }
150    }
151
152    #[expect(clippy::too_many_arguments)]
153    pub(crate) fn new(
154        cx: &mut js::context::JSContext,
155        global: &D::GlobalScope,
156        channel: WebGPU,
157        buffer: WebGPUBuffer,
158        device: &D::GPUDevice,
159        size: GPUSize64,
160        usage: GPUFlagsConstant,
161        mapping: Option<RootedTraceableBox<ActiveBufferMapping>>,
162        label: USVString,
163    ) -> DomRoot<Self> {
164        reflect_dom_object_with_wrap::<D, _, _>(
165            cx,
166            Box::new(GPUBuffer::new_inherited(
167                channel, buffer, device, size, usage, mapping, label,
168            )),
169            global,
170            GPUBufferWrap::<D>,
171        )
172    }
173}
174
175impl<D> GPUBuffer<D>
176where
177    D: Equivalence,
178    <D::Promise as PromiseHelpers<D>>::StackRoot: WebGPUPromise<D>,
179{
180    pub fn id(&self) -> WebGPUBuffer {
181        self.droppable.buffer
182    }
183
184    /// <https://gpuweb.github.io/gpuweb/#dom-gpudevice-createbuffer>
185    pub(crate) fn create(
186        cx: &mut js::context::JSContext,
187        device: &D::GPUDevice,
188        descriptor: &GPUBufferDescriptor,
189    ) -> Fallible<DomRoot<GPUBuffer<D>>> {
190        let desc = BufferDescriptor {
191            label: (&descriptor.parent).convert(),
192            size: descriptor.size as BufferAddress,
193            usage: BufferUsages::from_bits_retain(descriptor.usage),
194            mapped_at_creation: descriptor.mappedAtCreation,
195        };
196        let id = <D::GPUDevice as DomGlobalGeneric<D>>::global_from_reflector(device)
197            .global_wgpu_id_hub()
198            .create_buffer_id();
199
200        device
201            .channel()
202            .0
203            .send(WebGPURequest::CreateBuffer {
204                device_id: device.id().0,
205                buffer_id: id,
206                descriptor: desc,
207            })
208            .expect("Failed to create WebGPU buffer");
209
210        let buffer = WebGPUBuffer(id);
211        let mapping = if descriptor.mappedAtCreation {
212            Some(ActiveBufferMapping::new(
213                GPUMapModeConstants::WRITE,
214                0..descriptor.size,
215                None,
216            )?)
217        } else {
218            None
219        };
220
221        let global = <D::GPUDevice as DomGlobalGeneric<D>>::global_from_reflector(device);
222        Ok(GPUBuffer::new(
223            cx,
224            &*global,
225            device.channel(),
226            buffer,
227            device,
228            descriptor.size,
229            descriptor.usage,
230            mapping,
231            descriptor.parent.label.clone(),
232        ))
233    }
234}
235
236impl<D> GPUBufferMethods<D> for GPUBuffer<D>
237where
238    D: Equivalence,
239    <D::Promise as PromiseHelpers<D>>::StackRoot: WebGPUPromise<D>,
240{
241    /// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-unmap>
242    fn Unmap(&self, cx: &mut js::context::JSContext) {
243        // Step 1: If this.[[pending_map]] is not null:
244        // 1.2 Set this.[[pending_map]] to null.
245        let promise = self.pending_map.safe_borrow_mut(cx).take();
246        if let Some(promise) = promise {
247            // 1.1 Reject this.[[pending_map]] with an AbortError.
248            promise.reject_error(cx, Error::Abort(Some("No pending map".into())));
249        }
250        // Step 2: If this.[[mapping]] is null:
251        let mut rooted_mapping = RootedTraceableBox::new(self.mapping.safe_borrow_mut(cx).take());
252        let mapping = if let Some(mapping) = rooted_mapping.as_mut() {
253            mapping
254        } else {
255            // 2.1 Return.
256            return;
257        };
258
259        // Step 3: For each ArrayBuffer ab in this.[[mapping]].views:
260        // 3.1 Perform DetachArrayBuffer(ab, "WebGPUBufferMapping").
261        mapping.data.clear_views(cx);
262        let (data, mode, range) = rooted_mapping.into_box().unwrap().consume();
263        // Step 4: Let bufferUpdate be null.
264        // Step 5: If this.[[mapping]].mode contains WRITE:
265        // Set bufferUpdate to { data: this.[[mapping]].data, offset: this.[[mapping]].range[0] }.
266        let buffer_update = if mode & GPUMapModeConstants::WRITE > 0 {
267            BufferUpdate::Write(data, range)
268        } else {
269            // we still send the shared memory handle back so we do not need to send it at map_async
270            // otherwise we would need to have two flows for read and write, which is more complicated
271            BufferUpdate::Read(data)
272        };
273        if let Err(e) = self.droppable.channel.0.send(WebGPURequest::UnmapBuffer {
274            buffer_id: self.id().0,
275            buffer_update,
276        }) {
277            warn!(
278                "Failed to send Buffer unmap ({:?}) ({})",
279                self.droppable.buffer.0, e
280            );
281        }
282    }
283
284    /// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-destroy>
285    fn Destroy(&self, cx: &mut JSContext) {
286        // Step 1
287        self.Unmap(cx);
288        // Step 2
289        if let Err(e) = self
290            .droppable
291            .channel
292            .0
293            .send(WebGPURequest::DestroyBuffer(self.droppable.buffer.0))
294        {
295            warn!(
296                "Failed to send WebGPURequest::DestroyBuffer({:?}) ({})",
297                self.droppable.buffer.0, e
298            );
299        };
300    }
301
302    /// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-mapasync>
303    fn MapAsync(
304        &self,
305        cx: &mut CurrentRealm<'_>,
306        mode: u32,
307        offset: GPUSize64,
308        size: Option<GPUSize64>,
309    ) -> <D::Promise as PromiseHelpers<D>>::StackRoot {
310        // Step 3: Let p be a new promise.
311        let p = D::Promise::new_in_realm(cx);
312        // Step 2: If this.mapState is not "unmapped":
313        if self.MapState() != GPUBufferMapState::Unmapped {
314            // 2.1 Issue the early-reject steps on the Device timeline of this.[[device]].
315            self.device
316                .dispatch_error(webgpu_traits::Error::Validation(String::from(
317                    "Buffer is not in unmapped state",
318                )));
319            // 2.2 Return a promise rejected with OperationError.
320            p.reject_error(
321                cx,
322                Error::Operation(Some("Buffer is not in unmapped state".into())),
323            );
324            return p;
325        }
326        // Step 4: Set this.[[pending_map]] to p.
327        *self.pending_map.safe_borrow_mut(cx) = Some(p.to_traced());
328        // Step 5: Issue the validation steps on the Device timeline of this.[[device]].
329        let host_map = match mode {
330            GPUMapModeConstants::READ => HostMap::Read,
331            GPUMapModeConstants::WRITE => HostMap::Write,
332            _ => {
333                self.device
334                    .dispatch_error(webgpu_traits::Error::Validation(String::from(
335                        "Invalid MapModeFlags",
336                    )));
337                self.map_failure(cx, &p);
338                return p;
339            },
340        };
341
342        let callback = p.callback_promise_dom_manipulation_task_source(self);
343        if let Err(e) = self
344            .droppable
345            .channel
346            .0
347            .send(WebGPURequest::BufferMapAsync {
348                callback,
349                buffer_id: self.droppable.buffer.0,
350                device_id: self.device.id().0,
351                host_map,
352                offset,
353                size,
354                buffer_size: self.size,
355            })
356        {
357            warn!(
358                "Failed to send BufferMapAsync ({:?}) ({})",
359                self.droppable.buffer.0, e
360            );
361            self.map_failure(cx, &p);
362            return p;
363        }
364        // Step 6: Return p.
365        p
366    }
367
368    /// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-getmappedrange>
369    fn GetMappedRange(
370        &self,
371        cx: &mut js::context::JSContext,
372        offset: GPUSize64,
373        size: Option<GPUSize64>,
374    ) -> Fallible<RootedTraceableBox<HeapArrayBuffer>> {
375        // 1. If size is missing:
376        // Let rangeSize be max(0, this.size - offset).
377        // Otherwise, let rangeSize be size.
378        let range_size = size.unwrap_or(self.size.saturating_sub(offset));
379        // Step 2: If any of the following conditions are unsatisfied, throw an OperationError and return.
380
381        // this.[[mapping]] is not null.
382        let mut mapping = self
383            .mapping
384            .safe_borrow_mut(cx)
385            .take()
386            .map(RootedTraceableBox::new)
387            .ok_or(Error::Operation(Some("No active buffer map".into())))?;
388
389        // offset is a multiple of 8.
390        if !(offset.is_multiple_of(MAP_ALIGNMENT)) {
391            self.mapping
392                .safe_borrow_mut(cx)
393                .replace(*mapping.into_box());
394
395            return Err(Error::Operation(Some(
396                "`offset` is not a multiple of 8".into(),
397            )));
398        }
399
400        // rangeSize is a multiple of 4.
401        if !range_size.is_multiple_of(COPY_BUFFER_ALIGNMENT) {
402            self.mapping
403                .safe_borrow_mut(cx)
404                .replace(*mapping.into_box());
405
406            return Err(Error::Operation(Some(
407                "`rangeSize` is not a multiple of 4".into(),
408            )));
409        }
410
411        // offset ≥ this.[[mapping]].range[0].
412        if !(offset >= mapping.range.start) {
413            self.mapping
414                .safe_borrow_mut(cx)
415                .replace(*mapping.into_box());
416
417            return Err(Error::Operation(Some(
418                "`offset` is greater than `[[mapping]].range[0]`".into(),
419            )));
420        }
421
422        // offset + rangeSize ≤ this.[[mapping]].range[1].
423        if !(offset + range_size <= mapping.range.end) {
424            self.mapping
425                .safe_borrow_mut(cx)
426                .replace(*mapping.into_box());
427
428            return Err(Error::Operation(Some(
429                "`offset` + `rangeSize` is less than or equal to `[[mapping]].range[1]`".into(),
430            )));
431        }
432
433        // [offset, offset + rangeSize) does not overlap another range in this.[[mapping]].views.
434
435        // Step 4: Let view be ! create an ArrayBuffer of size rangeSize,
436        // but with its pointer mutably referencing the content of data at offset (offset - [[mapping]].range[0]).
437
438        // Step 6: Append view to this.[[mapping]].views.
439
440        let result = mapping
441            .data
442            .view(cx, offset as usize..(offset as usize + range_size as usize))
443            .map(|view| view.array_buffer())
444            .map_err(|()| {
445                Error::Operation(Some(
446                    "Mapped range overlaps with others or is out of bounds.".into(),
447                ))
448            });
449
450        self.mapping
451            .safe_borrow_mut(cx)
452            .replace(*mapping.into_box());
453
454        // Step 7: Return view.
455        result
456    }
457
458    /// <https://gpuweb.github.io/gpuweb/#dom-gpuobjectbase-label>
459    fn Label(&self) -> USVString {
460        self.label.borrow().clone()
461    }
462
463    /// <https://gpuweb.github.io/gpuweb/#dom-gpuobjectbase-label>
464    fn SetLabel(&self, no_gc: &NoGC, value: USVString) {
465        *self.label.safe_borrow_mut(no_gc) = value;
466    }
467
468    /// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-size>
469    fn Size(&self) -> GPUSize64 {
470        self.size
471    }
472
473    /// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-usage>
474    fn Usage(&self) -> GPUFlagsConstant {
475        self.usage
476    }
477
478    /// <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-mapstate>
479    fn MapState(&self) -> GPUBufferMapState {
480        // Step 1&2&3
481        if self.mapping.borrow().is_some() {
482            GPUBufferMapState::Mapped
483        } else if self.pending_map.borrow().is_some() {
484            GPUBufferMapState::Pending
485        } else {
486            GPUBufferMapState::Unmapped
487        }
488    }
489}
490
491impl<D> GPUBuffer<D>
492where
493    D: Equivalence,
494{
495    pub fn map_failure(
496        &self,
497        cx: &mut JSContext,
498        p: &<D::Promise as PromiseHelpers<D>>::StackRoot,
499    ) {
500        // Step 1
501        if self.pending_map.borrow().as_deref() != Some(p) {
502            assert!(p.is_rejected());
503            return;
504        }
505        // Step 2
506        assert!(p.is_pending());
507        // Step 3
508        self.pending_map.safe_borrow_mut(cx).take();
509        // Step 4
510        let is_lost = self.device.is_lost();
511        if is_lost {
512            p.reject_error(cx, Error::Abort(Some("GPUDevice is lost".into())));
513        } else {
514            p.reject_error(cx, Error::Operation(Some("Failed to map GPUBuffer".into())));
515        }
516    }
517
518    pub fn map_success(
519        &self,
520        cx: &mut js::context::JSContext,
521        p: &<D::Promise as PromiseHelpers<D>>::StackRoot,
522        Mapping { data, mode, range }: Mapping,
523    ) {
524        // Step 1
525        if self.pending_map.borrow().as_deref() != Some(p) {
526            assert!(p.is_rejected());
527            return;
528        }
529
530        // Step 2
531        assert!(p.is_pending());
532
533        // Step 4
534        let mapping = ActiveBufferMapping::new(
535            match mode {
536                HostMap::Read => GPUMapModeConstants::READ,
537                HostMap::Write => GPUMapModeConstants::WRITE,
538            },
539            range,
540            Some(data),
541        );
542
543        match mapping {
544            Err(error) => {
545                *self.pending_map.safe_borrow_mut(cx) = None;
546                p.reject_error(cx, error);
547            },
548            Ok(mapping) => {
549                // Step 6
550                self.mapping
551                    .safe_borrow_mut(cx)
552                    .replace(*mapping.into_box());
553                // Step 7
554                self.pending_map.safe_borrow_mut(cx).take();
555                p.resolve_native(cx, &());
556            },
557        }
558    }
559}
560
561impl<D: Equivalence> RoutedPromiseListener<D, Result<Mapping, BufferAccessError>> for GPUBuffer<D> {
562    fn handle_response(
563        &self,
564        cx: &mut js::context::JSContext,
565        response: Result<Mapping, BufferAccessError>,
566        promise: &<D::Promise as PromiseHelpers<D>>::StackRoot,
567    ) {
568        match response {
569            Ok(mapping) => self.map_success(cx, promise, mapping),
570            Err(_) => self.map_failure(cx, promise),
571        }
572    }
573}