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
->mArrayBuffers
.Length(); ++i
) {
38 NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(
39 mMapped
->mArrayBuffers
[i
])
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();
63 ffi::wgpu_client_make_buffer_id(actor
->GetClient(), aDeviceId
);
65 if (!aDevice
->IsBridgeAlive()) {
66 // Create and return an invalid Buffer.
67 RefPtr
<Buffer
> buffer
= new Buffer(aDevice
, bufferId
, aDesc
.mSize
, 0,
68 ipc::WritableSharedMemoryMapping());
69 buffer
->mValid
= false;
70 return buffer
.forget();
73 auto handle
= ipc::UnsafeSharedMemoryHandle();
74 auto mapping
= ipc::WritableSharedMemoryMapping();
76 bool hasMapFlags
= aDesc
.mUsage
& (dom::GPUBufferUsage_Binding::MAP_WRITE
|
77 dom::GPUBufferUsage_Binding::MAP_READ
);
79 bool allocSucceeded
= false;
80 if (hasMapFlags
|| aDesc
.mMappedAtCreation
) {
81 // If shmem allocation fails, we continue and provide the parent side with
82 // an empty shmem which it will interpret as an OOM situtation.
83 const auto checked
= CheckedInt
<size_t>(aDesc
.mSize
);
84 const size_t maxSize
= WGPUMAX_BUFFER_SIZE
;
85 if (checked
.isValid()) {
86 size_t size
= checked
.value();
88 if (size
> 0 && size
< maxSize
) {
89 auto maybeShmem
= ipc::UnsafeSharedMemoryHandle::CreateAndMap(size
);
91 if (maybeShmem
.isSome()) {
92 allocSucceeded
= true;
93 handle
= std::move(maybeShmem
.ref().first
);
94 mapping
= std::move(maybeShmem
.ref().second
);
96 MOZ_RELEASE_ASSERT(mapping
.Size() >= size
);
99 memset(mapping
.Bytes().data(), 0, size
);
104 // Zero-sized buffers is a special case. We don't create a shmem since
105 // allocating the memory would not make sense, however mappable null
106 // buffers are allowed by the spec so we just pass the null handle which
107 // in practice deserializes into a null handle on the parent side and
108 // behaves like a zero-sized allocation.
109 allocSucceeded
= true;
114 // If mapped at creation and the shmem allocation failed, immediately throw
115 // a range error and don't attempt to create the buffer.
116 if (aDesc
.mMappedAtCreation
&& !allocSucceeded
) {
117 aRv
.ThrowRangeError("Allocation failed");
121 actor
->SendDeviceCreateBuffer(aDeviceId
, bufferId
, aDesc
, std::move(handle
));
123 RefPtr
<Buffer
> buffer
= new Buffer(aDevice
, bufferId
, aDesc
.mSize
,
124 aDesc
.mUsage
, std::move(mapping
));
125 buffer
->SetLabel(aDesc
.mLabel
);
127 if (aDesc
.mMappedAtCreation
) {
128 // Mapped at creation's raison d'ĂȘtre is write access, since the buffer is
129 // being created and there isn't anything interesting to read in it yet.
130 bool writable
= true;
131 buffer
->SetMapped(0, aDesc
.mSize
, writable
);
134 aDevice
->TrackBuffer(buffer
.get());
136 return buffer
.forget();
139 void Buffer::Drop() {
148 if (mMapped
&& !mMapped
->mArrayBuffers
.IsEmpty()) {
149 // The array buffers could live longer than us and our shmem, so make sure
150 // we clear the external buffer bindings.
151 dom::AutoJSAPI jsapi
;
152 if (jsapi
.Init(GetDevice().GetOwnerGlobal())) {
153 IgnoredErrorResult rv
;
154 UnmapArrayBuffers(jsapi
.cx(), rv
);
159 GetDevice().UntrackBuffer(this);
161 if (GetDevice().IsBridgeAlive()) {
162 GetDevice().GetBridge()->SendBufferDrop(mId
);
166 void Buffer::SetMapped(BufferAddress aOffset
, BufferAddress aSize
,
168 MOZ_ASSERT(!mMapped
);
169 MOZ_RELEASE_ASSERT(aOffset
<= mSize
);
170 MOZ_RELEASE_ASSERT(aSize
<= mSize
- aOffset
);
173 mMapped
->mWritable
= aWritable
;
174 mMapped
->mOffset
= aOffset
;
175 mMapped
->mSize
= aSize
;
178 already_AddRefed
<dom::Promise
> Buffer::MapAsync(
179 uint32_t aMode
, uint64_t aOffset
, const dom::Optional
<uint64_t>& aSize
,
181 RefPtr
<dom::Promise
> promise
= dom::Promise::Create(GetParentObject(), aRv
);
182 if (NS_WARN_IF(aRv
.Failed())) {
186 if (GetDevice().IsLost()) {
187 promise
->MaybeRejectWithOperationError("Device Lost");
188 return promise
.forget();
192 promise
->MaybeRejectWithOperationError("Buffer mapping is already pending");
193 return promise
.forget();
196 BufferAddress size
= 0;
197 if (aSize
.WasPassed()) {
198 size
= aSize
.Value();
199 } else if (aOffset
<= mSize
) {
200 // Default to passing the reminder of the buffer after the provided offset.
201 size
= mSize
- aOffset
;
203 // The provided offset is larger than the buffer size.
204 // The parent side will handle the error, we can let the requested size be
208 RefPtr
<Buffer
> self(this);
210 auto mappingPromise
= GetDevice().GetBridge()->SendBufferMap(
211 GetDevice().mId
, mId
, aMode
, aOffset
, size
);
212 MOZ_ASSERT(mappingPromise
);
214 mMapRequest
= promise
;
216 mappingPromise
->Then(
217 GetCurrentSerialEventTarget(), __func__
,
218 [promise
, self
](BufferMapResult
&& aResult
) {
219 // Unmap might have been called while the result was on the way back.
220 if (promise
->State() != dom::Promise::PromiseState::Pending
) {
224 // mValid should be true or we should have called unmap while marking
225 // the buffer invalid, causing the promise to be rejected and the branch
226 // above to have early-returned.
227 MOZ_RELEASE_ASSERT(self
->mValid
);
229 switch (aResult
.type()) {
230 case BufferMapResult::TBufferMapSuccess
: {
231 auto& success
= aResult
.get_BufferMapSuccess();
232 self
->mMapRequest
= nullptr;
233 self
->SetMapped(success
.offset(), success
.size(),
235 promise
->MaybeResolve(0);
238 case BufferMapResult::TBufferMapError
: {
239 auto& error
= aResult
.get_BufferMapError();
240 self
->RejectMapRequest(promise
, error
.message());
244 MOZ_CRASH("unreachable");
248 [promise
](const ipc::ResponseRejectReason
&) {
249 promise
->MaybeRejectWithAbortError("Internal communication error!");
252 return promise
.forget();
255 static void ExternalBufferFreeCallback(void* aContents
, void* aUserData
) {
257 auto shm
= static_cast<std::shared_ptr
<ipc::WritableSharedMemoryMapping
>*>(
262 void Buffer::GetMappedRange(JSContext
* aCx
, uint64_t aOffset
,
263 const dom::Optional
<uint64_t>& aSize
,
264 JS::Rooted
<JSObject
*>* aObject
, ErrorResult
& aRv
) {
266 aRv
.ThrowInvalidStateError("Buffer is not mapped");
270 const auto checkedOffset
= CheckedInt
<size_t>(aOffset
);
271 const auto checkedSize
= aSize
.WasPassed()
272 ? CheckedInt
<size_t>(aSize
.Value())
273 : CheckedInt
<size_t>(mSize
) - aOffset
;
274 const auto checkedMinBufferSize
= checkedOffset
+ checkedSize
;
276 if (!checkedOffset
.isValid() || !checkedSize
.isValid() ||
277 !checkedMinBufferSize
.isValid() || aOffset
< mMapped
->mOffset
||
278 checkedMinBufferSize
.value() > mMapped
->mOffset
+ mMapped
->mSize
) {
279 aRv
.ThrowRangeError("Invalid range");
283 auto offset
= checkedOffset
.value();
284 auto size
= checkedSize
.value();
285 auto span
= mShmem
->Bytes().Subspan(offset
, size
);
287 std::shared_ptr
<ipc::WritableSharedMemoryMapping
>* userData
=
288 new std::shared_ptr
<ipc::WritableSharedMemoryMapping
>(mShmem
);
289 UniquePtr
<void, JS::BufferContentsDeleter
> dataPtr
{
290 span
.data(), {&ExternalBufferFreeCallback
, userData
}};
291 JS::Rooted
<JSObject
*> arrayBuffer(
292 aCx
, JS::NewExternalArrayBuffer(aCx
, size
, std::move(dataPtr
)));
294 aRv
.NoteJSContextException(aCx
);
298 aObject
->set(arrayBuffer
);
299 mMapped
->mArrayBuffers
.AppendElement(*aObject
);
302 void Buffer::UnmapArrayBuffers(JSContext
* aCx
, ErrorResult
& aRv
) {
305 bool detachedArrayBuffers
= true;
306 for (const auto& arrayBuffer
: mMapped
->mArrayBuffers
) {
307 JS::Rooted
<JSObject
*> rooted(aCx
, arrayBuffer
);
308 if (!JS::DetachArrayBuffer(aCx
, rooted
)) {
309 detachedArrayBuffers
= false;
313 mMapped
->mArrayBuffers
.Clear();
317 if (NS_WARN_IF(!detachedArrayBuffers
)) {
318 aRv
.NoteJSContextException(aCx
);
323 void Buffer::RejectMapRequest(dom::Promise
* aPromise
, nsACString
& message
) {
324 if (mMapRequest
== aPromise
) {
325 mMapRequest
= nullptr;
328 aPromise
->MaybeRejectWithOperationError(message
);
331 void Buffer::AbortMapRequest() {
333 mMapRequest
->MaybeRejectWithAbortError("Buffer unmapped");
335 mMapRequest
= nullptr;
338 void Buffer::Unmap(JSContext
* aCx
, ErrorResult
& aRv
) {
343 UnmapArrayBuffers(aCx
, aRv
);
345 bool hasMapFlags
= mUsage
& (dom::GPUBufferUsage_Binding::MAP_WRITE
|
346 dom::GPUBufferUsage_Binding::MAP_READ
);
349 // We get here if the buffer was mapped at creation without map flags.
350 // It won't be possible to map the buffer again so we can get rid of
351 // our shmem on this side.
352 mShmem
= std::make_shared
<ipc::WritableSharedMemoryMapping
>();
355 if (!GetDevice().IsLost()) {
356 GetDevice().GetBridge()->SendBufferUnmap(GetDevice().mId
, mId
,
363 void Buffer::Destroy(JSContext
* aCx
, ErrorResult
& aRv
) {
368 if (!GetDevice().IsLost()) {
369 GetDevice().GetBridge()->SendBufferDestroy(mId
);
371 // TODO: we don't have to implement it right now, but it's used by the
375 dom::GPUBufferMapState
Buffer::MapState() const {
376 // Implementation reference:
377 // <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-mapstate>.
380 return dom::GPUBufferMapState::Mapped
;
383 return dom::GPUBufferMapState::Pending
;
385 return dom::GPUBufferMapState::Unmapped
;
388 } // namespace mozilla::webgpu