Files
tubestation/dom/webgpu/Buffer.cpp
Nicolas Silva dbd3c4297a Bug 1777535 - Track the buffer mapAsync promise. r=jimb
Per spec (and discussion with someone on the chromium side where spec is vague), the correct behavior should be:
 - MapAsync validation happens on the device timeline, so we should reject the promise in mapAsync on the content side if we run into an internal error not described by the spec.
 - Unmap immediately rejects all pending mapping promises on the content side (there can be multiple of them since we have to catch that error on the device timeline).

This patch tracks a single mapping promise at a time and immediately rejects on the content side any subseqent mapping
request made until unmap is called. This means our current implementation deviates slightly from the current state of
the spec in that:
 - The promise is rejected earlier on the content timeline,
 - If the first request fails, all subsequent requests will fail until either unmap or when the content side receives and processes the rejected
   promise, whereas Dawn's implementation would allow the first valid request to succed.

There was some confusion around the the use of uint64_t and size_t which probably originated at point where this code was working differently. This patch uses uint64_t (=BufferAddress) more consistently removing the need for some of the casting and overflow checks.
One notable change in the overall logic is that SetMapped is now called when the buffer is actually in the mapped state (before this patch it was called as soon as the buffer had a pending map request).

Depends on D151618

Differential Revision: https://phabricator.services.mozilla.com/D151619
2022-08-10 15:55:05 +00:00

314 lines
9.4 KiB
C++

/* -*- 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_ROOT_NATIVE(Buffer, AddRef)
NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(Buffer, Release)
NS_IMPL_CYCLE_COLLECTION_CLASS(Buffer)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Buffer)
tmp->Cleanup();
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::Shmem&& aShmem)
: ChildOf(aParent), mId(aId), mSize(aSize), mUsage(aUsage), mShmem(aShmem) {
mozilla::HoldJSObjects(this);
}
Buffer::~Buffer() {
Cleanup();
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::Shmem());
return buffer.forget();
}
RefPtr<WebGPUChild> actor = aDevice->GetBridge();
ipc::Shmem shmem;
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;
}
const auto& size = checked.value();
if (!actor->AllocUnsafeShmem(size, &shmem)) {
aRv.ThrowAbortError(
nsPrintfCString("Unable to allocate shmem of size %" PRIuPTR, size));
return nullptr;
}
// zero out memory
memset(shmem.get<uint8_t>(), 0, size);
}
MaybeShmem maybeShmem = mozilla::null_t();
if (shmem.IsReadable()) {
maybeShmem = shmem;
}
RawId id = actor->DeviceCreateBuffer(aDeviceId, aDesc, std::move(maybeShmem));
RefPtr<Buffer> buffer =
new Buffer(aDevice, id, aDesc.mSize, aDesc.mUsage, std::move(shmem));
if (aDesc.mMappedAtCreation) {
buffer->SetMapped(0, aDesc.mSize,
!(aDesc.mUsage & dom::GPUBufferUsage_Binding::MAP_READ));
}
return buffer.forget();
}
bool Buffer::Mappable() const {
return (mUsage & (dom::GPUBufferUsage_Binding::MAP_WRITE |
dom::GPUBufferUsage_Binding::MAP_READ)) != 0;
}
void Buffer::Cleanup() {
AbortMapRequest();
if (mValid && mParent) {
mValid = false;
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(mParent->GetOwnerGlobal())) {
IgnoredErrorResult rv;
UnmapArrayBuffers(jsapi.cx(), rv);
}
}
auto bridge = mParent->GetBridge();
if (bridge && bridge->IsOpen()) {
// Tell the parent side the about the imminent disparition of the shmem
// *before* deallocating it.
bridge->SendBufferDestroy(mId);
}
}
}
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 (mParent->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 = mParent->MapBufferAsync(mId, aMode, aOffset, size, aRv);
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();
}
void Buffer::GetMappedRange(JSContext* aCx, uint64_t aOffset,
const dom::Optional<uint64_t>& aSize,
JS::Rooted<JSObject*>* aObject, ErrorResult& aRv) {
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()) {
aRv.ThrowRangeError("Invalid mapped range");
return;
}
if (!mMapped || !mShmem.IsReadable()) {
aRv.ThrowInvalidStateError("Buffer is not mapped");
return;
}
if (checkedMinBufferSize.value() > mShmem.Size<uint8_t>()) {
aRv.ThrowOperationError("Mapped range exceeds buffer size");
return;
}
auto* const arrayBuffer = mParent->CreateExternalArrayBuffer(
aCx, checkedOffset.value(), checkedSize.value(), mShmem);
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 handle on this side. The parent side will deallocate it.
mShmem = ipc::Shmem();
}
mParent->UnmapBuffer(mId, mMapped->mWritable);
mMapped.reset();
}
void Buffer::Destroy() {
AbortMapRequest();
// TODO: we don't have to implement it right now, but it's used by the
// examples
}
} // namespace mozilla::webgpu