1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #include "mozilla/dom/WebGPUBinding.h"
9 #include "mozilla/dom/Promise.h"
10 #include "mozilla/dom/ScriptSettings.h"
11 #include "mozilla/HoldDropJSObjects.h"
12 #include "mozilla/ipc/Shmem.h"
13 #include "ipc/WebGPUChild.h"
14 #include "js/ArrayBuffer.h"
15 #include "js/RootingAPI.h"
16 #include "nsContentUtils.h"
17 #include "nsWrapperCache.h"
19 #include "mozilla/webgpu/ffi/wgpu.h"
21 namespace mozilla::webgpu
{
23 GPU_IMPL_JS_WRAP(Buffer
)
25 NS_IMPL_CYCLE_COLLECTION_CLASS(Buffer
)
26 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Buffer
)
28 NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent
)
29 NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
30 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
31 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Buffer
)
32 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParent
)
33 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
34 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(Buffer
)
35 NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
37 for (uint32_t i
= 0; i
< tmp
->mMapped
->mViews
.Length(); ++i
) {
38 NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(
39 mMapped
->mViews
[i
].mArrayBuffer
)
42 NS_IMPL_CYCLE_COLLECTION_TRACE_END
44 Buffer::Buffer(Device
* const aParent
, RawId aId
, BufferAddress aSize
,
45 uint32_t aUsage
, ipc::WritableSharedMemoryMapping
&& aShmem
)
46 : ChildOf(aParent
), mId(aId
), mSize(aSize
), mUsage(aUsage
) {
47 mozilla::HoldJSObjects(this);
49 std::make_shared
<ipc::WritableSharedMemoryMapping
>(std::move(aShmem
));
55 mozilla::DropJSObjects(this);
58 already_AddRefed
<Buffer
> Buffer::Create(Device
* aDevice
, RawId aDeviceId
,
59 const dom::GPUBufferDescriptor
& aDesc
,
61 RefPtr
<WebGPUChild
> actor
= aDevice
->GetBridge();
62 RawId bufferId
= ffi::wgpu_client_make_buffer_id(actor
->GetClient());
64 if (!aDevice
->IsBridgeAlive()) {
65 // Create and return an invalid Buffer.
66 RefPtr
<Buffer
> buffer
= new Buffer(aDevice
, bufferId
, aDesc
.mSize
, 0,
67 ipc::WritableSharedMemoryMapping());
68 buffer
->mValid
= false;
69 return buffer
.forget();
72 auto handle
= ipc::UnsafeSharedMemoryHandle();
73 auto mapping
= ipc::WritableSharedMemoryMapping();
75 bool hasMapFlags
= aDesc
.mUsage
& (dom::GPUBufferUsage_Binding::MAP_WRITE
|
76 dom::GPUBufferUsage_Binding::MAP_READ
);
78 bool allocSucceeded
= false;
79 if (hasMapFlags
|| aDesc
.mMappedAtCreation
) {
80 // If shmem allocation fails, we continue and provide the parent side with
81 // an empty shmem which it will interpret as an OOM situtation.
82 const auto checked
= CheckedInt
<size_t>(aDesc
.mSize
);
83 const size_t maxSize
= WGPUMAX_BUFFER_SIZE
;
84 if (checked
.isValid()) {
85 size_t size
= checked
.value();
87 if (size
> 0 && size
< maxSize
) {
88 auto maybeShmem
= ipc::UnsafeSharedMemoryHandle::CreateAndMap(size
);
90 if (maybeShmem
.isSome()) {
91 allocSucceeded
= true;
92 handle
= std::move(maybeShmem
.ref().first
);
93 mapping
= std::move(maybeShmem
.ref().second
);
95 MOZ_RELEASE_ASSERT(mapping
.Size() >= size
);
98 memset(mapping
.Bytes().data(), 0, size
);
103 // Zero-sized buffers is a special case. We don't create a shmem since
104 // allocating the memory would not make sense, however mappable null
105 // buffers are allowed by the spec so we just pass the null handle which
106 // in practice deserializes into a null handle on the parent side and
107 // behaves like a zero-sized allocation.
108 allocSucceeded
= true;
113 // If mapped at creation and the shmem allocation failed, immediately throw
114 // a range error and don't attempt to create the buffer.
115 if (aDesc
.mMappedAtCreation
&& !allocSucceeded
) {
116 aRv
.ThrowRangeError("Allocation failed");
120 actor
->SendDeviceCreateBuffer(aDeviceId
, bufferId
, aDesc
, std::move(handle
));
122 RefPtr
<Buffer
> buffer
= new Buffer(aDevice
, bufferId
, aDesc
.mSize
,
123 aDesc
.mUsage
, std::move(mapping
));
124 buffer
->SetLabel(aDesc
.mLabel
);
126 if (aDesc
.mMappedAtCreation
) {
127 // Mapped at creation's raison d'être is write access, since the buffer is
128 // being created and there isn't anything interesting to read in it yet.
129 bool writable
= true;
130 buffer
->SetMapped(0, aDesc
.mSize
, writable
);
133 aDevice
->TrackBuffer(buffer
.get());
135 return buffer
.forget();
138 void Buffer::Cleanup() {
146 if (mMapped
&& !mMapped
->mViews
.IsEmpty()) {
147 // The array buffers could live longer than us and our shmem, so make sure
148 // we clear the external buffer bindings.
149 dom::AutoJSAPI jsapi
;
150 if (jsapi
.Init(GetDevice().GetOwnerGlobal())) {
151 IgnoredErrorResult rv
;
152 UnmapArrayBuffers(jsapi
.cx(), rv
);
157 GetDevice().UntrackBuffer(this);
159 auto bridge
= GetDevice().GetBridge();
164 if (bridge
->CanSend()) {
165 bridge
->SendBufferDrop(mId
);
168 wgpu_client_free_buffer_id(bridge
->GetClient(), mId
);
171 void Buffer::SetMapped(BufferAddress aOffset
, BufferAddress aSize
,
173 MOZ_ASSERT(!mMapped
);
174 MOZ_RELEASE_ASSERT(aOffset
<= mSize
);
175 MOZ_RELEASE_ASSERT(aSize
<= mSize
- aOffset
);
178 mMapped
->mWritable
= aWritable
;
179 mMapped
->mOffset
= aOffset
;
180 mMapped
->mSize
= aSize
;
183 already_AddRefed
<dom::Promise
> Buffer::MapAsync(
184 uint32_t aMode
, uint64_t aOffset
, const dom::Optional
<uint64_t>& aSize
,
186 RefPtr
<dom::Promise
> promise
= dom::Promise::Create(GetParentObject(), aRv
);
187 if (NS_WARN_IF(aRv
.Failed())) {
191 if (GetDevice().IsLost()) {
192 promise
->MaybeRejectWithOperationError("Device Lost");
193 return promise
.forget();
197 promise
->MaybeRejectWithOperationError("Buffer mapping is already pending");
198 return promise
.forget();
201 BufferAddress size
= 0;
202 if (aSize
.WasPassed()) {
203 size
= aSize
.Value();
204 } else if (aOffset
<= mSize
) {
205 // Default to passing the reminder of the buffer after the provided offset.
206 size
= mSize
- aOffset
;
208 // The provided offset is larger than the buffer size.
209 // The parent side will handle the error, we can let the requested size be
213 RefPtr
<Buffer
> self(this);
215 auto mappingPromise
= GetDevice().GetBridge()->SendBufferMap(
216 GetDevice().mId
, mId
, aMode
, aOffset
, size
);
217 MOZ_ASSERT(mappingPromise
);
219 mMapRequest
= promise
;
221 mappingPromise
->Then(
222 GetCurrentSerialEventTarget(), __func__
,
223 [promise
, self
](BufferMapResult
&& aResult
) {
224 // Unmap might have been called while the result was on the way back.
225 if (promise
->State() != dom::Promise::PromiseState::Pending
) {
229 // mValid should be true or we should have called unmap while marking
230 // the buffer invalid, causing the promise to be rejected and the branch
231 // above to have early-returned.
232 MOZ_RELEASE_ASSERT(self
->mValid
);
234 switch (aResult
.type()) {
235 case BufferMapResult::TBufferMapSuccess
: {
236 auto& success
= aResult
.get_BufferMapSuccess();
237 self
->mMapRequest
= nullptr;
238 self
->SetMapped(success
.offset(), success
.size(),
240 promise
->MaybeResolve(0);
243 case BufferMapResult::TBufferMapError
: {
244 auto& error
= aResult
.get_BufferMapError();
245 self
->RejectMapRequest(promise
, error
.message());
249 MOZ_CRASH("unreachable");
253 [promise
](const ipc::ResponseRejectReason
&) {
254 promise
->MaybeRejectWithAbortError("Internal communication error!");
257 return promise
.forget();
260 static void ExternalBufferFreeCallback(void* aContents
, void* aUserData
) {
262 auto shm
= static_cast<std::shared_ptr
<ipc::WritableSharedMemoryMapping
>*>(
267 void Buffer::GetMappedRange(JSContext
* aCx
, uint64_t aOffset
,
268 const dom::Optional
<uint64_t>& aSize
,
269 JS::Rooted
<JSObject
*>* aObject
, ErrorResult
& aRv
) {
270 // The WebGPU spec spells out the validation we must perform, but
271 // use `CheckedInt<uint64_t>` anyway to catch our mistakes. Except
272 // where we explicitly say otherwise, invalid `CheckedInt` values
273 // should only arise when we have a bug, so just calling
274 // `CheckedInt::value` where needed should be fine (it checks with
275 // `MOZ_DIAGNOSTIC_ASSERT`).
277 // https://gpuweb.github.io/gpuweb/#dom-gpubuffer-getmappedrange
279 // Content timeline steps:
281 // 1. If `size` is missing:
282 // 1. Let `rangeSize` be `max(0, this.size - offset)`.
283 // Otherwise, let `rangeSize` be `size`.
284 const auto offset
= CheckedInt
<uint64_t>(aOffset
);
285 CheckedInt
<uint64_t> rangeSize
;
286 if (aSize
.WasPassed()) {
287 rangeSize
= aSize
.Value();
289 const auto bufferSize
= CheckedInt
<uint64_t>(mSize
);
290 // Use `CheckInt`'s underflow detection for `max(0, ...)`.
291 rangeSize
= bufferSize
- offset
;
292 if (!rangeSize
.isValid()) {
297 // 2. If any of the following conditions are unsatisfied, throw an
298 // `OperationError` and stop.
300 // - `this.[[mapping]]` is not `null`.
302 aRv
.ThrowOperationError("Buffer is not mapped");
306 // - `offset` is a multiple of 8.
308 // (`operator!=` is not available on `CheckedInt`.)
309 if (offset
.value() % 8 != 0) {
310 aRv
.ThrowOperationError("GetMappedRange offset is not a multiple of 8");
314 // - `rangeSize` is a multiple of `4`.
315 if (rangeSize
.value() % 4 != 0) {
316 aRv
.ThrowOperationError("GetMappedRange size is not a multiple of 4");
320 // - `offset ≥ this.[[mapping]].range[0]`.
321 if (offset
.value() < mMapped
->mOffset
) {
322 aRv
.ThrowOperationError(
323 "GetMappedRange offset starts before buffer's mapped range");
327 // - `offset + rangeSize ≤ this.[[mapping]].range[1]`.
329 // Perform the addition in `CheckedInt`, treating overflow as a validation
331 const auto rangeEndChecked
= offset
+ rangeSize
;
332 if (!rangeEndChecked
.isValid() ||
333 rangeEndChecked
.value() > mMapped
->mOffset
+ mMapped
->mSize
) {
334 aRv
.ThrowOperationError(
335 "GetMappedRange range extends beyond buffer's mapped range");
339 // - `[offset, offset + rangeSize)` does not overlap another range
340 // in `this.[[mapping]].views`.
341 const uint64_t rangeEnd
= rangeEndChecked
.value();
342 for (const auto& view
: mMapped
->mViews
) {
343 if (view
.mOffset
< rangeEnd
&& offset
.value() < view
.mRangeEnd
) {
344 aRv
.ThrowOperationError(
345 "GetMappedRange range overlaps with existing buffer view");
350 // 3. Let `data` be `this.[[mapping]].data`.
352 // The creation of a *pointer to* a `shared_ptr` here seems redundant but is
353 // unfortunately necessary: `JS::BufferContentsDeleter` requires that its
354 // `userData` be a `void*`, and while `shared_ptr` can't be inter-converted
355 // with `void*` (it's actually two pointers), `shared_ptr*` obviously can.
356 std::shared_ptr
<ipc::WritableSharedMemoryMapping
>* data
=
357 new std::shared_ptr
<ipc::WritableSharedMemoryMapping
>(mShmem
);
359 // 4. Let `view` be (potentially fallible operation follows) create an
360 // `ArrayBuffer` of size `rangeSize`, but with its pointer mutably
361 // referencing the content of `data` at offset `(offset -
362 // [[mapping]].range[0])`.
364 // Since `size_t` may not be the same as `uint64_t`, check, convert, and check
365 // again. `CheckedInt<size_t>(x)` produces an invalid value if `x` is not in
366 // range for `size_t` before any conversion is performed.
367 const auto checkedSize
= CheckedInt
<size_t>(rangeSize
.value()).value();
368 const auto checkedOffset
= CheckedInt
<size_t>(offset
.value()).value();
369 const auto span
= (*data
)->Bytes().Subspan(checkedOffset
, checkedSize
);
370 UniquePtr
<void, JS::BufferContentsDeleter
> contents
{
371 span
.data(), {&ExternalBufferFreeCallback
, data
}};
372 JS::Rooted
<JSObject
*> view(
373 aCx
, JS::NewExternalArrayBuffer(aCx
, checkedSize
, std::move(contents
)));
375 aRv
.NoteJSContextException(aCx
);
380 mMapped
->mViews
.AppendElement(
381 MappedView({checkedOffset
, rangeEnd
, *aObject
}));
384 void Buffer::UnmapArrayBuffers(JSContext
* aCx
, ErrorResult
& aRv
) {
387 bool detachedArrayBuffers
= true;
388 for (const auto& view
: mMapped
->mViews
) {
389 JS::Rooted
<JSObject
*> rooted(aCx
, view
.mArrayBuffer
);
390 if (!JS::DetachArrayBuffer(aCx
, rooted
)) {
391 detachedArrayBuffers
= false;
395 mMapped
->mViews
.Clear();
399 if (NS_WARN_IF(!detachedArrayBuffers
)) {
400 aRv
.NoteJSContextException(aCx
);
405 void Buffer::RejectMapRequest(dom::Promise
* aPromise
, nsACString
& message
) {
406 if (mMapRequest
== aPromise
) {
407 mMapRequest
= nullptr;
410 aPromise
->MaybeRejectWithOperationError(message
);
413 void Buffer::AbortMapRequest() {
415 mMapRequest
->MaybeRejectWithAbortError("Buffer unmapped");
417 mMapRequest
= nullptr;
420 void Buffer::Unmap(JSContext
* aCx
, ErrorResult
& aRv
) {
425 UnmapArrayBuffers(aCx
, aRv
);
427 bool hasMapFlags
= mUsage
& (dom::GPUBufferUsage_Binding::MAP_WRITE
|
428 dom::GPUBufferUsage_Binding::MAP_READ
);
431 // We get here if the buffer was mapped at creation without map flags.
432 // It won't be possible to map the buffer again so we can get rid of
433 // our shmem on this side.
434 mShmem
= std::make_shared
<ipc::WritableSharedMemoryMapping
>();
437 if (!GetDevice().IsLost()) {
438 GetDevice().GetBridge()->SendBufferUnmap(GetDevice().mId
, mId
,
445 void Buffer::Destroy(JSContext
* aCx
, ErrorResult
& aRv
) {
450 if (!GetDevice().IsLost()) {
451 GetDevice().GetBridge()->SendBufferDestroy(mId
);
453 // TODO: we don't have to implement it right now, but it's used by the
457 dom::GPUBufferMapState
Buffer::MapState() const {
458 // Implementation reference:
459 // <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-mapstate>.
462 return dom::GPUBufferMapState::Mapped
;
465 return dom::GPUBufferMapState::Pending
;
467 return dom::GPUBufferMapState::Unmapped
;
470 } // namespace mozilla::webgpu