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 if (aDevice
->IsLost()) {
62 // Create and return an invalid Buffer. This Buffer will have id 0 and
63 // won't be sent in any messages to the parent.
64 RefPtr
<Buffer
> buffer
= new Buffer(aDevice
, 0, aDesc
.mSize
, 0,
65 ipc::WritableSharedMemoryMapping());
67 // Track the invalid Buffer to ensure that ::Drop can untrack it later.
68 aDevice
->TrackBuffer(buffer
.get());
69 return buffer
.forget();
72 RefPtr
<WebGPUChild
> actor
= aDevice
->GetBridge();
74 auto handle
= ipc::UnsafeSharedMemoryHandle();
75 auto mapping
= ipc::WritableSharedMemoryMapping();
77 bool hasMapFlags
= aDesc
.mUsage
& (dom::GPUBufferUsage_Binding::MAP_WRITE
|
78 dom::GPUBufferUsage_Binding::MAP_READ
);
80 bool allocSucceeded
= false;
81 if (hasMapFlags
|| aDesc
.mMappedAtCreation
) {
82 // If shmem allocation fails, we continue and provide the parent side with
83 // an empty shmem which it will interpret as an OOM situtation.
84 const auto checked
= CheckedInt
<size_t>(aDesc
.mSize
);
85 const size_t maxSize
= WGPUMAX_BUFFER_SIZE
;
86 if (checked
.isValid()) {
87 size_t size
= checked
.value();
89 if (size
> 0 && size
< maxSize
) {
90 auto maybeShmem
= ipc::UnsafeSharedMemoryHandle::CreateAndMap(size
);
92 if (maybeShmem
.isSome()) {
93 allocSucceeded
= true;
94 handle
= std::move(maybeShmem
.ref().first
);
95 mapping
= std::move(maybeShmem
.ref().second
);
97 MOZ_RELEASE_ASSERT(mapping
.Size() >= size
);
100 memset(mapping
.Bytes().data(), 0, size
);
105 // Zero-sized buffers is a special case. We don't create a shmem since
106 // allocating the memory would not make sense, however mappable null
107 // buffers are allowed by the spec so we just pass the null handle which
108 // in practice deserializes into a null handle on the parent side and
109 // behaves like a zero-sized allocation.
110 allocSucceeded
= true;
115 // If mapped at creation and the shmem allocation failed, immediately throw
116 // a range error and don't attempt to create the buffer.
117 if (aDesc
.mMappedAtCreation
&& !allocSucceeded
) {
118 aRv
.ThrowRangeError("Allocation failed");
122 RawId id
= actor
->DeviceCreateBuffer(aDeviceId
, aDesc
, std::move(handle
));
124 RefPtr
<Buffer
> buffer
=
125 new Buffer(aDevice
, id
, aDesc
.mSize
, aDesc
.mUsage
, std::move(mapping
));
126 buffer
->SetLabel(aDesc
.mLabel
);
128 if (aDesc
.mMappedAtCreation
) {
129 // Mapped at creation's raison d'ĂȘtre is write access, since the buffer is
130 // being created and there isn't anything interesting to read in it yet.
131 bool writable
= true;
132 buffer
->SetMapped(0, aDesc
.mSize
, writable
);
135 aDevice
->TrackBuffer(buffer
.get());
137 return buffer
.forget();
140 void Buffer::Drop() {
149 if (mMapped
&& !mMapped
->mArrayBuffers
.IsEmpty()) {
150 // The array buffers could live longer than us and our shmem, so make sure
151 // we clear the external buffer bindings.
152 dom::AutoJSAPI jsapi
;
153 if (jsapi
.Init(GetDevice().GetOwnerGlobal())) {
154 IgnoredErrorResult rv
;
155 UnmapArrayBuffers(jsapi
.cx(), rv
);
160 GetDevice().UntrackBuffer(this);
162 if (GetDevice().IsBridgeAlive() && mId
) {
163 GetDevice().GetBridge()->SendBufferDrop(mId
);
167 void Buffer::SetMapped(BufferAddress aOffset
, BufferAddress aSize
,
169 MOZ_ASSERT(!mMapped
);
170 MOZ_RELEASE_ASSERT(aOffset
<= mSize
);
171 MOZ_RELEASE_ASSERT(aSize
<= mSize
- aOffset
);
174 mMapped
->mWritable
= aWritable
;
175 mMapped
->mOffset
= aOffset
;
176 mMapped
->mSize
= aSize
;
179 already_AddRefed
<dom::Promise
> Buffer::MapAsync(
180 uint32_t aMode
, uint64_t aOffset
, const dom::Optional
<uint64_t>& aSize
,
182 RefPtr
<dom::Promise
> promise
= dom::Promise::Create(GetParentObject(), aRv
);
183 if (NS_WARN_IF(aRv
.Failed())) {
187 if (GetDevice().IsLost()) {
188 promise
->MaybeRejectWithOperationError("Device Lost");
189 return promise
.forget();
193 promise
->MaybeRejectWithOperationError("Buffer mapping is already pending");
194 return promise
.forget();
197 BufferAddress size
= 0;
198 if (aSize
.WasPassed()) {
199 size
= aSize
.Value();
200 } else if (aOffset
<= mSize
) {
201 // Default to passing the reminder of the buffer after the provided offset.
202 size
= mSize
- aOffset
;
204 // The provided offset is larger than the buffer size.
205 // The parent side will handle the error, we can let the requested size be
209 RefPtr
<Buffer
> self(this);
211 auto mappingPromise
=
212 GetDevice().GetBridge()->SendBufferMap(mId
, aMode
, aOffset
, size
);
213 MOZ_ASSERT(mappingPromise
);
215 mMapRequest
= promise
;
217 mappingPromise
->Then(
218 GetCurrentSerialEventTarget(), __func__
,
219 [promise
, self
](BufferMapResult
&& aResult
) {
220 // Unmap might have been called while the result was on the way back.
221 if (promise
->State() != dom::Promise::PromiseState::Pending
) {
225 // mValid should be true or we should have called unmap while marking
226 // the buffer invalid, causing the promise to be rejected and the branch
227 // above to have early-returned.
228 MOZ_RELEASE_ASSERT(self
->mValid
);
230 switch (aResult
.type()) {
231 case BufferMapResult::TBufferMapSuccess
: {
232 auto& success
= aResult
.get_BufferMapSuccess();
233 self
->mMapRequest
= nullptr;
234 self
->SetMapped(success
.offset(), success
.size(),
236 promise
->MaybeResolve(0);
239 case BufferMapResult::TBufferMapError
: {
240 auto& error
= aResult
.get_BufferMapError();
241 self
->RejectMapRequest(promise
, error
.message());
245 MOZ_CRASH("unreachable");
249 [promise
](const ipc::ResponseRejectReason
&) {
250 promise
->MaybeRejectWithAbortError("Internal communication error!");
253 return promise
.forget();
256 static void ExternalBufferFreeCallback(void* aContents
, void* aUserData
) {
258 auto shm
= static_cast<std::shared_ptr
<ipc::WritableSharedMemoryMapping
>*>(
263 void Buffer::GetMappedRange(JSContext
* aCx
, uint64_t aOffset
,
264 const dom::Optional
<uint64_t>& aSize
,
265 JS::Rooted
<JSObject
*>* aObject
, ErrorResult
& aRv
) {
267 aRv
.ThrowInvalidStateError("Buffer is not mapped");
271 const auto checkedOffset
= CheckedInt
<size_t>(aOffset
);
272 const auto checkedSize
= aSize
.WasPassed()
273 ? CheckedInt
<size_t>(aSize
.Value())
274 : CheckedInt
<size_t>(mSize
) - aOffset
;
275 const auto checkedMinBufferSize
= checkedOffset
+ checkedSize
;
277 if (!checkedOffset
.isValid() || !checkedSize
.isValid() ||
278 !checkedMinBufferSize
.isValid() || aOffset
< mMapped
->mOffset
||
279 checkedMinBufferSize
.value() > mMapped
->mOffset
+ mMapped
->mSize
) {
280 aRv
.ThrowRangeError("Invalid range");
284 auto offset
= checkedOffset
.value();
285 auto size
= checkedSize
.value();
286 auto span
= mShmem
->Bytes().Subspan(offset
, size
);
288 std::shared_ptr
<ipc::WritableSharedMemoryMapping
>* userData
=
289 new std::shared_ptr
<ipc::WritableSharedMemoryMapping
>(mShmem
);
290 UniquePtr
<void, JS::BufferContentsDeleter
> dataPtr
{
291 span
.data(), {&ExternalBufferFreeCallback
, userData
}};
292 JS::Rooted
<JSObject
*> arrayBuffer(
293 aCx
, JS::NewExternalArrayBuffer(aCx
, size
, std::move(dataPtr
)));
295 aRv
.NoteJSContextException(aCx
);
299 aObject
->set(arrayBuffer
);
300 mMapped
->mArrayBuffers
.AppendElement(*aObject
);
303 void Buffer::UnmapArrayBuffers(JSContext
* aCx
, ErrorResult
& aRv
) {
306 bool detachedArrayBuffers
= true;
307 for (const auto& arrayBuffer
: mMapped
->mArrayBuffers
) {
308 JS::Rooted
<JSObject
*> rooted(aCx
, arrayBuffer
);
309 if (!JS::DetachArrayBuffer(aCx
, rooted
)) {
310 detachedArrayBuffers
= false;
314 mMapped
->mArrayBuffers
.Clear();
318 if (NS_WARN_IF(!detachedArrayBuffers
)) {
319 aRv
.NoteJSContextException(aCx
);
324 void Buffer::RejectMapRequest(dom::Promise
* aPromise
, nsACString
& message
) {
325 if (mMapRequest
== aPromise
) {
326 mMapRequest
= nullptr;
329 aPromise
->MaybeRejectWithOperationError(message
);
332 void Buffer::AbortMapRequest() {
334 mMapRequest
->MaybeRejectWithAbortError("Buffer unmapped");
336 mMapRequest
= nullptr;
339 void Buffer::Unmap(JSContext
* aCx
, ErrorResult
& aRv
) {
344 UnmapArrayBuffers(aCx
, aRv
);
346 bool hasMapFlags
= mUsage
& (dom::GPUBufferUsage_Binding::MAP_WRITE
|
347 dom::GPUBufferUsage_Binding::MAP_READ
);
350 // We get here if the buffer was mapped at creation without map flags.
351 // It won't be possible to map the buffer again so we can get rid of
352 // our shmem on this side.
353 mShmem
= std::make_shared
<ipc::WritableSharedMemoryMapping
>();
356 if (!GetDevice().IsLost()) {
357 GetDevice().GetBridge()->SendBufferUnmap(GetDevice().mId
, mId
,
364 void Buffer::Destroy(JSContext
* aCx
, ErrorResult
& aRv
) {
369 if (!GetDevice().IsLost()) {
370 GetDevice().GetBridge()->SendBufferDestroy(mId
);
372 // TODO: we don't have to implement it right now, but it's used by the
376 dom::GPUBufferMapState
Buffer::MapState() const {
377 // Implementation reference:
378 // <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-mapstate>.
381 return dom::GPUBufferMapState::Mapped
;
384 return dom::GPUBufferMapState::Pending
;
386 return dom::GPUBufferMapState::Unmapped
;
389 } // namespace mozilla::webgpu