summaryrefslogtreecommitdiffstats
path: root/dom/webgpu/Buffer.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/webgpu/Buffer.cpp')
-rw-r--r--dom/webgpu/Buffer.cpp348
1 files changed, 348 insertions, 0 deletions
diff --git a/dom/webgpu/Buffer.cpp b/dom/webgpu/Buffer.cpp
new file mode 100644
index 0000000000..9841a9b6d4
--- /dev/null
+++ b/dom/webgpu/Buffer.cpp
@@ -0,0 +1,348 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/WebGPUBinding.h"
+#include "Buffer.h"
+
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/HoldDropJSObjects.h"
+#include "mozilla/ipc/Shmem.h"
+#include "ipc/WebGPUChild.h"
+#include "js/ArrayBuffer.h"
+#include "js/RootingAPI.h"
+#include "nsContentUtils.h"
+#include "nsWrapperCache.h"
+#include "Device.h"
+
+namespace mozilla::webgpu {
+
+GPU_IMPL_JS_WRAP(Buffer)
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(Buffer)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Buffer)
+ tmp->Drop();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Buffer)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParent)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(Buffer)
+ NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
+ if (tmp->mMapped) {
+ for (uint32_t i = 0; i < tmp->mMapped->mArrayBuffers.Length(); ++i) {
+ NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(
+ mMapped->mArrayBuffers[i])
+ }
+ }
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+Buffer::Buffer(Device* const aParent, RawId aId, BufferAddress aSize,
+ uint32_t aUsage, ipc::WritableSharedMemoryMapping&& aShmem)
+ : ChildOf(aParent), mId(aId), mSize(aSize), mUsage(aUsage) {
+ mozilla::HoldJSObjects(this);
+ mShmem =
+ std::make_shared<ipc::WritableSharedMemoryMapping>(std::move(aShmem));
+ MOZ_ASSERT(mParent);
+}
+
+Buffer::~Buffer() {
+ Drop();
+ mozilla::DropJSObjects(this);
+}
+
+already_AddRefed<Buffer> Buffer::Create(Device* aDevice, RawId aDeviceId,
+ const dom::GPUBufferDescriptor& aDesc,
+ ErrorResult& aRv) {
+ if (aDevice->IsLost()) {
+ RefPtr<Buffer> buffer = new Buffer(aDevice, 0, aDesc.mSize, 0,
+ ipc::WritableSharedMemoryMapping());
+ return buffer.forget();
+ }
+
+ RefPtr<WebGPUChild> actor = aDevice->GetBridge();
+
+ auto handle = ipc::UnsafeSharedMemoryHandle();
+ auto mapping = ipc::WritableSharedMemoryMapping();
+
+ bool hasMapFlags = aDesc.mUsage & (dom::GPUBufferUsage_Binding::MAP_WRITE |
+ dom::GPUBufferUsage_Binding::MAP_READ);
+ if (hasMapFlags || aDesc.mMappedAtCreation) {
+ const auto checked = CheckedInt<size_t>(aDesc.mSize);
+ if (!checked.isValid()) {
+ aRv.ThrowRangeError("Mappable size is too large");
+ return nullptr;
+ }
+ size_t size = checked.value();
+
+ auto maybeShmem = ipc::UnsafeSharedMemoryHandle::CreateAndMap(size);
+
+ if (maybeShmem.isNothing()) {
+ aRv.ThrowAbortError(
+ nsPrintfCString("Unable to allocate shmem of size %" PRIuPTR, size));
+ return nullptr;
+ }
+
+ handle = std::move(maybeShmem.ref().first);
+ mapping = std::move(maybeShmem.ref().second);
+
+ MOZ_RELEASE_ASSERT(mapping.Size() >= size);
+
+ // zero out memory
+ memset(mapping.Bytes().data(), 0, size);
+ }
+
+ RawId id = actor->DeviceCreateBuffer(aDeviceId, aDesc, std::move(handle));
+
+ RefPtr<Buffer> buffer =
+ new Buffer(aDevice, id, aDesc.mSize, aDesc.mUsage, std::move(mapping));
+ if (aDesc.mMappedAtCreation) {
+ // Mapped at creation's raison d'ĂȘtre is write access, since the buffer is
+ // being created and there isn't anything interesting to read in it yet.
+ bool writable = true;
+ buffer->SetMapped(0, aDesc.mSize, writable);
+ }
+
+ return buffer.forget();
+}
+
+void Buffer::Drop() {
+ AbortMapRequest();
+
+ if (mMapped && !mMapped->mArrayBuffers.IsEmpty()) {
+ // The array buffers could live longer than us and our shmem, so make sure
+ // we clear the external buffer bindings.
+ dom::AutoJSAPI jsapi;
+ if (jsapi.Init(GetDevice().GetOwnerGlobal())) {
+ IgnoredErrorResult rv;
+ UnmapArrayBuffers(jsapi.cx(), rv);
+ }
+ }
+ mMapped.reset();
+
+ if (mValid && !GetDevice().IsLost()) {
+ GetDevice().GetBridge()->SendBufferDrop(mId);
+ }
+ mValid = false;
+}
+
+void Buffer::SetMapped(BufferAddress aOffset, BufferAddress aSize,
+ bool aWritable) {
+ MOZ_ASSERT(!mMapped);
+ MOZ_RELEASE_ASSERT(aOffset <= mSize);
+ MOZ_RELEASE_ASSERT(aSize <= mSize - aOffset);
+
+ mMapped.emplace();
+ mMapped->mWritable = aWritable;
+ mMapped->mOffset = aOffset;
+ mMapped->mSize = aSize;
+}
+
+already_AddRefed<dom::Promise> Buffer::MapAsync(
+ uint32_t aMode, uint64_t aOffset, const dom::Optional<uint64_t>& aSize,
+ ErrorResult& aRv) {
+ RefPtr<dom::Promise> promise = dom::Promise::Create(GetParentObject(), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ if (GetDevice().IsLost()) {
+ promise->MaybeRejectWithOperationError("Device Lost");
+ return promise.forget();
+ }
+
+ if (mMapRequest) {
+ promise->MaybeRejectWithOperationError("Buffer mapping is already pending");
+ return promise.forget();
+ }
+
+ BufferAddress size = 0;
+ if (aSize.WasPassed()) {
+ size = aSize.Value();
+ } else if (aOffset <= mSize) {
+ // Default to passing the reminder of the buffer after the provided offset.
+ size = mSize - aOffset;
+ } else {
+ // The provided offset is larger than the buffer size.
+ // The parent side will handle the error, we can let the requested size be
+ // zero.
+ }
+
+ RefPtr<Buffer> self(this);
+
+ auto mappingPromise =
+ GetDevice().GetBridge()->SendBufferMap(mId, aMode, aOffset, size);
+ MOZ_ASSERT(mappingPromise);
+
+ mMapRequest = promise;
+
+ mappingPromise->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [promise, self](BufferMapResult&& aResult) {
+ // Unmap might have been called while the result was on the way back.
+ if (promise->State() != dom::Promise::PromiseState::Pending) {
+ return;
+ }
+
+ switch (aResult.type()) {
+ case BufferMapResult::TBufferMapSuccess: {
+ auto& success = aResult.get_BufferMapSuccess();
+ self->mMapRequest = nullptr;
+ self->SetMapped(success.offset(), success.size(),
+ success.writable());
+ promise->MaybeResolve(0);
+ break;
+ }
+ case BufferMapResult::TBufferMapError: {
+ auto& error = aResult.get_BufferMapError();
+ self->RejectMapRequest(promise, error.message());
+ break;
+ }
+ default: {
+ MOZ_CRASH("unreachable");
+ }
+ }
+ },
+ [promise](const ipc::ResponseRejectReason&) {
+ promise->MaybeRejectWithAbortError("Internal communication error!");
+ });
+
+ return promise.forget();
+}
+
+static void ExternalBufferFreeCallback(void* aContents, void* aUserData) {
+ Unused << aContents;
+ auto shm = static_cast<std::shared_ptr<ipc::WritableSharedMemoryMapping>*>(
+ aUserData);
+ delete shm;
+}
+
+void Buffer::GetMappedRange(JSContext* aCx, uint64_t aOffset,
+ const dom::Optional<uint64_t>& aSize,
+ JS::Rooted<JSObject*>* aObject, ErrorResult& aRv) {
+ if (!mMapped) {
+ aRv.ThrowInvalidStateError("Buffer is not mapped");
+ return;
+ }
+
+ const auto checkedOffset = CheckedInt<size_t>(aOffset);
+ const auto checkedSize = aSize.WasPassed()
+ ? CheckedInt<size_t>(aSize.Value())
+ : CheckedInt<size_t>(mSize) - aOffset;
+ const auto checkedMinBufferSize = checkedOffset + checkedSize;
+
+ if (!checkedOffset.isValid() || !checkedSize.isValid() ||
+ !checkedMinBufferSize.isValid() || aOffset < mMapped->mOffset ||
+ checkedMinBufferSize.value() > mMapped->mOffset + mMapped->mSize) {
+ aRv.ThrowRangeError("Invalid range");
+ return;
+ }
+
+ auto offset = checkedOffset.value();
+ auto size = checkedSize.value();
+ auto span = mShmem->Bytes().Subspan(offset, size);
+
+ std::shared_ptr<ipc::WritableSharedMemoryMapping>* userData =
+ new std::shared_ptr<ipc::WritableSharedMemoryMapping>(mShmem);
+ auto* const arrayBuffer = JS::NewExternalArrayBuffer(
+ aCx, size, span.data(), &ExternalBufferFreeCallback, userData);
+
+ if (!arrayBuffer) {
+ aRv.NoteJSContextException(aCx);
+ return;
+ }
+
+ aObject->set(arrayBuffer);
+ mMapped->mArrayBuffers.AppendElement(*aObject);
+}
+
+void Buffer::UnmapArrayBuffers(JSContext* aCx, ErrorResult& aRv) {
+ MOZ_ASSERT(mMapped);
+
+ bool detachedArrayBuffers = true;
+ for (const auto& arrayBuffer : mMapped->mArrayBuffers) {
+ JS::Rooted<JSObject*> rooted(aCx, arrayBuffer);
+ if (!JS::DetachArrayBuffer(aCx, rooted)) {
+ detachedArrayBuffers = false;
+ }
+ };
+
+ mMapped->mArrayBuffers.Clear();
+
+ AbortMapRequest();
+
+ if (NS_WARN_IF(!detachedArrayBuffers)) {
+ aRv.NoteJSContextException(aCx);
+ return;
+ }
+}
+
+void Buffer::RejectMapRequest(dom::Promise* aPromise, nsACString& message) {
+ if (mMapRequest == aPromise) {
+ mMapRequest = nullptr;
+ }
+
+ aPromise->MaybeRejectWithOperationError(message);
+}
+
+void Buffer::AbortMapRequest() {
+ if (mMapRequest) {
+ mMapRequest->MaybeRejectWithAbortError("Buffer unmapped");
+ }
+ mMapRequest = nullptr;
+}
+
+void Buffer::Unmap(JSContext* aCx, ErrorResult& aRv) {
+ if (!mMapped) {
+ return;
+ }
+
+ UnmapArrayBuffers(aCx, aRv);
+
+ bool hasMapFlags = mUsage & (dom::GPUBufferUsage_Binding::MAP_WRITE |
+ dom::GPUBufferUsage_Binding::MAP_READ);
+
+ if (!hasMapFlags) {
+ // We get here if the buffer was mapped at creation without map flags.
+ // It won't be possible to map the buffer again so we can get rid of
+ // our shmem on this side.
+ mShmem = std::make_shared<ipc::WritableSharedMemoryMapping>();
+ }
+
+ if (!GetDevice().IsLost()) {
+ GetDevice().GetBridge()->SendBufferUnmap(GetDevice().mId, mId,
+ mMapped->mWritable);
+ }
+
+ mMapped.reset();
+}
+
+void Buffer::Destroy(JSContext* aCx, ErrorResult& aRv) {
+ if (mMapped) {
+ Unmap(aCx, aRv);
+ }
+
+ if (!GetDevice().IsLost()) {
+ GetDevice().GetBridge()->SendBufferDestroy(mId);
+ }
+ // TODO: we don't have to implement it right now, but it's used by the
+ // examples
+}
+
+dom::GPUBufferMapState Buffer::MapState() const {
+ // Implementation reference:
+ // <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-mapstate>.
+
+ if (mMapped) {
+ return dom::GPUBufferMapState::Mapped;
+ }
+ if (mMapRequest) {
+ return dom::GPUBufferMapState::Pending;
+ }
+ return dom::GPUBufferMapState::Unmapped;
+}
+
+} // namespace mozilla::webgpu