Bug 1936278 - Prevent search mode chiclet from being dismissed when clicking in page...
[gecko.git] / dom / webgpu / Buffer.cpp
blob3b2cf9f39f60f46e9e65d54d7816d4fe2f506873
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->Cleanup();
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->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);
48 mShmem =
49 std::make_shared<ipc::WritableSharedMemoryMapping>(std::move(aShmem));
50 MOZ_ASSERT(mParent);
53 Buffer::~Buffer() {
54 Cleanup();
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 = 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);
97 // zero out memory
98 memset(mapping.Bytes().data(), 0, size);
102 if (size == 0) {
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");
117 return nullptr;
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() {
139 if (!mValid) {
140 return;
142 mValid = false;
144 AbortMapRequest();
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);
155 mMapped.reset();
157 GetDevice().UntrackBuffer(this);
159 auto bridge = GetDevice().GetBridge();
160 if (!bridge) {
161 return;
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,
172 bool aWritable) {
173 MOZ_ASSERT(!mMapped);
174 MOZ_RELEASE_ASSERT(aOffset <= mSize);
175 MOZ_RELEASE_ASSERT(aSize <= mSize - aOffset);
177 mMapped.emplace();
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,
185 ErrorResult& aRv) {
186 RefPtr<dom::Promise> promise = dom::Promise::Create(GetParentObject(), aRv);
187 if (NS_WARN_IF(aRv.Failed())) {
188 return nullptr;
191 if (GetDevice().IsLost()) {
192 promise->MaybeRejectWithOperationError("Device Lost");
193 return promise.forget();
196 if (mMapRequest) {
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;
207 } else {
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
210 // zero.
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) {
226 return;
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(),
239 success.writable());
240 promise->MaybeResolve(0);
241 break;
243 case BufferMapResult::TBufferMapError: {
244 auto& error = aResult.get_BufferMapError();
245 self->RejectMapRequest(promise, error.message());
246 break;
248 default: {
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) {
261 Unused << aContents;
262 auto shm = static_cast<std::shared_ptr<ipc::WritableSharedMemoryMapping>*>(
263 aUserData);
264 delete shm;
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();
288 } else {
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()) {
293 rangeSize = 0;
297 // 2. If any of the following conditions are unsatisfied, throw an
298 // `OperationError` and stop.
300 // - `this.[[mapping]]` is not `null`.
301 if (!mMapped) {
302 aRv.ThrowOperationError("Buffer is not mapped");
303 return;
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");
311 return;
314 // - `rangeSize` is a multiple of `4`.
315 if (rangeSize.value() % 4 != 0) {
316 aRv.ThrowOperationError("GetMappedRange size is not a multiple of 4");
317 return;
320 // - `offset ≥ this.[[mapping]].range[0]`.
321 if (offset.value() < mMapped->mOffset) {
322 aRv.ThrowOperationError(
323 "GetMappedRange offset starts before buffer's mapped range");
324 return;
327 // - `offset + rangeSize ≤ this.[[mapping]].range[1]`.
329 // Perform the addition in `CheckedInt`, treating overflow as a validation
330 // error.
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");
336 return;
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");
346 return;
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)));
374 if (!view) {
375 aRv.NoteJSContextException(aCx);
376 return;
379 aObject->set(view);
380 mMapped->mViews.AppendElement(
381 MappedView({checkedOffset, rangeEnd, *aObject}));
384 void Buffer::UnmapArrayBuffers(JSContext* aCx, ErrorResult& aRv) {
385 MOZ_ASSERT(mMapped);
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();
397 AbortMapRequest();
399 if (NS_WARN_IF(!detachedArrayBuffers)) {
400 aRv.NoteJSContextException(aCx);
401 return;
405 void Buffer::RejectMapRequest(dom::Promise* aPromise, nsACString& message) {
406 if (mMapRequest == aPromise) {
407 mMapRequest = nullptr;
410 aPromise->MaybeRejectWithOperationError(message);
413 void Buffer::AbortMapRequest() {
414 if (mMapRequest) {
415 mMapRequest->MaybeRejectWithAbortError("Buffer unmapped");
417 mMapRequest = nullptr;
420 void Buffer::Unmap(JSContext* aCx, ErrorResult& aRv) {
421 if (!mMapped) {
422 return;
425 UnmapArrayBuffers(aCx, aRv);
427 bool hasMapFlags = mUsage & (dom::GPUBufferUsage_Binding::MAP_WRITE |
428 dom::GPUBufferUsage_Binding::MAP_READ);
430 if (!hasMapFlags) {
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,
439 mMapped->mWritable);
442 mMapped.reset();
445 void Buffer::Destroy(JSContext* aCx, ErrorResult& aRv) {
446 if (mMapped) {
447 Unmap(aCx, 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
454 // examples
457 dom::GPUBufferMapState Buffer::MapState() const {
458 // Implementation reference:
459 // <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-mapstate>.
461 if (mMapped) {
462 return dom::GPUBufferMapState::Mapped;
464 if (mMapRequest) {
465 return dom::GPUBufferMapState::Pending;
467 return dom::GPUBufferMapState::Unmapped;
470 } // namespace mozilla::webgpu