Bug 1883853 [wpt PR 44937] - [wdspec] fix test_set_permission_origin_unknown, a=testonly
[gecko.git] / dom / webgpu / Buffer.cpp
blobb7b689a9a0de45b2deae09441ac0076aa5e23004
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"
7 #include "Buffer.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"
18 #include "Device.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)
27 tmp->Drop();
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
36 if (tmp->mMapped) {
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);
48 mShmem =
49 std::make_shared<ipc::WritableSharedMemoryMapping>(std::move(aShmem));
50 MOZ_ASSERT(mParent);
53 Buffer::~Buffer() {
54 Drop();
55 mozilla::DropJSObjects(this);
58 already_AddRefed<Buffer> Buffer::Create(Device* aDevice, RawId aDeviceId,
59 const dom::GPUBufferDescriptor& aDesc,
60 ErrorResult& aRv) {
61 RefPtr<WebGPUChild> actor = aDevice->GetBridge();
62 RawId bufferId =
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);
98 // zero out memory
99 memset(mapping.Bytes().data(), 0, size);
103 if (size == 0) {
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");
118 return nullptr;
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() {
140 if (!mValid) {
141 return;
144 mValid = false;
146 AbortMapRequest();
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);
157 mMapped.reset();
159 GetDevice().UntrackBuffer(this);
161 if (GetDevice().IsBridgeAlive()) {
162 GetDevice().GetBridge()->SendBufferDrop(mId);
166 void Buffer::SetMapped(BufferAddress aOffset, BufferAddress aSize,
167 bool aWritable) {
168 MOZ_ASSERT(!mMapped);
169 MOZ_RELEASE_ASSERT(aOffset <= mSize);
170 MOZ_RELEASE_ASSERT(aSize <= mSize - aOffset);
172 mMapped.emplace();
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,
180 ErrorResult& aRv) {
181 RefPtr<dom::Promise> promise = dom::Promise::Create(GetParentObject(), aRv);
182 if (NS_WARN_IF(aRv.Failed())) {
183 return nullptr;
186 if (GetDevice().IsLost()) {
187 promise->MaybeRejectWithOperationError("Device Lost");
188 return promise.forget();
191 if (mMapRequest) {
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;
202 } else {
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
205 // zero.
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) {
221 return;
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(),
234 success.writable());
235 promise->MaybeResolve(0);
236 break;
238 case BufferMapResult::TBufferMapError: {
239 auto& error = aResult.get_BufferMapError();
240 self->RejectMapRequest(promise, error.message());
241 break;
243 default: {
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) {
256 Unused << aContents;
257 auto shm = static_cast<std::shared_ptr<ipc::WritableSharedMemoryMapping>*>(
258 aUserData);
259 delete shm;
262 void Buffer::GetMappedRange(JSContext* aCx, uint64_t aOffset,
263 const dom::Optional<uint64_t>& aSize,
264 JS::Rooted<JSObject*>* aObject, ErrorResult& aRv) {
265 if (!mMapped) {
266 aRv.ThrowInvalidStateError("Buffer is not mapped");
267 return;
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");
280 return;
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)));
293 if (!arrayBuffer) {
294 aRv.NoteJSContextException(aCx);
295 return;
298 aObject->set(arrayBuffer);
299 mMapped->mArrayBuffers.AppendElement(*aObject);
302 void Buffer::UnmapArrayBuffers(JSContext* aCx, ErrorResult& aRv) {
303 MOZ_ASSERT(mMapped);
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();
315 AbortMapRequest();
317 if (NS_WARN_IF(!detachedArrayBuffers)) {
318 aRv.NoteJSContextException(aCx);
319 return;
323 void Buffer::RejectMapRequest(dom::Promise* aPromise, nsACString& message) {
324 if (mMapRequest == aPromise) {
325 mMapRequest = nullptr;
328 aPromise->MaybeRejectWithOperationError(message);
331 void Buffer::AbortMapRequest() {
332 if (mMapRequest) {
333 mMapRequest->MaybeRejectWithAbortError("Buffer unmapped");
335 mMapRequest = nullptr;
338 void Buffer::Unmap(JSContext* aCx, ErrorResult& aRv) {
339 if (!mMapped) {
340 return;
343 UnmapArrayBuffers(aCx, aRv);
345 bool hasMapFlags = mUsage & (dom::GPUBufferUsage_Binding::MAP_WRITE |
346 dom::GPUBufferUsage_Binding::MAP_READ);
348 if (!hasMapFlags) {
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,
357 mMapped->mWritable);
360 mMapped.reset();
363 void Buffer::Destroy(JSContext* aCx, ErrorResult& aRv) {
364 if (mMapped) {
365 Unmap(aCx, 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
372 // examples
375 dom::GPUBufferMapState Buffer::MapState() const {
376 // Implementation reference:
377 // <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-mapstate>.
379 if (mMapped) {
380 return dom::GPUBufferMapState::Mapped;
382 if (mMapRequest) {
383 return dom::GPUBufferMapState::Pending;
385 return dom::GPUBufferMapState::Unmapped;
388 } // namespace mozilla::webgpu