tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

Buffer.cpp (14484B)


      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/. */
      5 
      6 #include "Buffer.h"
      7 
      8 #include "Device.h"
      9 #include "ipc/WebGPUChild.h"
     10 #include "js/ArrayBuffer.h"
     11 #include "js/RootingAPI.h"
     12 #include "mozilla/HoldDropJSObjects.h"
     13 #include "mozilla/dom/Promise.h"
     14 #include "mozilla/dom/ScriptSettings.h"
     15 #include "mozilla/dom/WebGPUBinding.h"
     16 #include "mozilla/ipc/Shmem.h"
     17 #include "mozilla/webgpu/ffi/wgpu.h"
     18 #include "nsContentUtils.h"
     19 #include "nsWrapperCache.h"
     20 
     21 namespace mozilla::webgpu {
     22 
     23 GPU_IMPL_JS_WRAP(Buffer)
     24 
     25 // We can't use `NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_WITH_JS_MEMBERS` since
     26 // we need to trace all nested `ArrayBuffer`s. We also need access to the
     27 // parent in the `Cleanup` step before we unlink it.
     28 NS_IMPL_CYCLE_COLLECTION_CLASS(Buffer)
     29 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Buffer)
     30  tmp->Cleanup();
     31  NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent)
     32  NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
     33 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
     34 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Buffer)
     35  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParent)
     36 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
     37 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(Buffer)
     38  NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
     39  if (tmp->mMapped) {
     40    for (uint32_t i = 0; i < tmp->mMapped->mViews.Length(); ++i) {
     41      NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(
     42          mMapped->mViews[i].mArrayBuffer)
     43    }
     44  }
     45 NS_IMPL_CYCLE_COLLECTION_TRACE_END
     46 
     47 Buffer::Buffer(Device* const aParent, RawId aId, BufferAddress aSize,
     48               uint32_t aUsage, ipc::SharedMemoryMapping&& aShmem)
     49    : ObjectBase(aParent->GetChild(), aId, ffi::wgpu_client_drop_buffer),
     50      ChildOf(aParent),
     51      mSize(aSize),
     52      mUsage(aUsage) {
     53  mozilla::HoldJSObjects(this);
     54  mShmem = std::make_shared<ipc::SharedMemoryMapping>(std::move(aShmem));
     55  MOZ_ASSERT(mParent);
     56 }
     57 
     58 Buffer::~Buffer() {
     59  Cleanup();
     60  mozilla::DropJSObjects(this);
     61 }
     62 
     63 already_AddRefed<Buffer> Buffer::Create(Device* aDevice, RawId aDeviceId,
     64                                        const dom::GPUBufferDescriptor& aDesc,
     65                                        ErrorResult& aRv) {
     66  RefPtr<WebGPUChild> child = aDevice->GetChild();
     67 
     68  ipc::MutableSharedMemoryHandle handle;
     69  ipc::SharedMemoryMapping mapping;
     70 
     71  bool hasMapFlags = aDesc.mUsage & (dom::GPUBufferUsage_Binding::MAP_WRITE |
     72                                     dom::GPUBufferUsage_Binding::MAP_READ);
     73 
     74  bool allocSucceeded = false;
     75  if (hasMapFlags || aDesc.mMappedAtCreation) {
     76    // If shmem allocation fails, we continue and provide the parent side with
     77    // an empty shmem which it will interpret as an OOM situtation.
     78    const auto checked = CheckedInt<size_t>(aDesc.mSize);
     79    const size_t maxSize = WGPUMAX_BUFFER_SIZE;
     80    if (checked.isValid()) {
     81      size_t size = checked.value();
     82 
     83      if (size > 0 && size < maxSize) {
     84        handle = ipc::shared_memory::Create(size);
     85        mapping = handle.Map();
     86        if (handle && mapping) {
     87          allocSucceeded = true;
     88 
     89          MOZ_RELEASE_ASSERT(mapping.Size() >= size);
     90 
     91          // zero out memory
     92          memset(mapping.Address(), 0, size);
     93        } else {
     94          handle = nullptr;
     95          mapping = nullptr;
     96        }
     97      }
     98 
     99      if (size == 0) {
    100        // Zero-sized buffers is a special case. We don't create a shmem since
    101        // allocating the memory would not make sense, however mappable null
    102        // buffers are allowed by the spec so we just pass the null handle which
    103        // in practice deserializes into a null handle on the parent side and
    104        // behaves like a zero-sized allocation.
    105        allocSucceeded = true;
    106      }
    107    }
    108  }
    109 
    110  // If mapped at creation and the shmem allocation failed, immediately throw
    111  // a range error and don't attempt to create the buffer.
    112  if (aDesc.mMappedAtCreation && !allocSucceeded) {
    113    aRv.ThrowRangeError("Allocation failed");
    114    return nullptr;
    115  }
    116 
    117  ffi::WGPUBufferDescriptor desc = {};
    118  webgpu::StringHelper label(aDesc.mLabel);
    119  desc.label = label.Get();
    120  desc.size = aDesc.mSize;
    121  desc.usage = aDesc.mUsage;
    122  desc.mapped_at_creation = aDesc.mMappedAtCreation;
    123 
    124  auto shmem_handle_index = child->QueueShmemHandle(std::move(handle));
    125  RawId bufferId = ffi::wgpu_client_create_buffer(child->GetClient(), aDeviceId,
    126                                                  &desc, shmem_handle_index);
    127 
    128  RefPtr<Buffer> buffer = new Buffer(aDevice, bufferId, aDesc.mSize,
    129                                     aDesc.mUsage, std::move(mapping));
    130  buffer->SetLabel(aDesc.mLabel);
    131 
    132  if (aDesc.mMappedAtCreation) {
    133    // Mapped at creation's raison d'être is write access, since the buffer is
    134    // being created and there isn't anything interesting to read in it yet.
    135    bool writable = true;
    136    buffer->SetMapped(0, aDesc.mSize, writable);
    137  }
    138 
    139  aDevice->TrackBuffer(buffer.get());
    140 
    141  return buffer.forget();
    142 }
    143 
    144 void Buffer::Cleanup() {
    145  if (!mValid) {
    146    return;
    147  }
    148  mValid = false;
    149 
    150  AbortMapRequest();
    151 
    152  if (mMapped && !mMapped->mViews.IsEmpty()) {
    153    // The array buffers could live longer than us and our shmem, so make sure
    154    // we clear the external buffer bindings.
    155    dom::AutoJSAPI jsapi;
    156    if (jsapi.Init(mParent->GetOwnerGlobal())) {
    157      IgnoredErrorResult rv;
    158      UnmapArrayBuffers(jsapi.cx(), rv);
    159    }
    160  }
    161  mMapped.reset();
    162 
    163  mParent->UntrackBuffer(this);
    164 }
    165 
    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);
    171 
    172  mMapped.emplace();
    173  mMapped->mWritable = aWritable;
    174  mMapped->mOffset = aOffset;
    175  mMapped->mSize = aSize;
    176 }
    177 
    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;
    184  }
    185 
    186  if (mMapRequest) {
    187    promise->MaybeRejectWithOperationError("Buffer mapping is already pending");
    188    return promise.forget();
    189  }
    190 
    191  BufferAddress size = 0;
    192  if (aSize.WasPassed()) {
    193    size = aSize.Value();
    194  } else if (aOffset <= mSize) {
    195    // Default to passing the reminder of the buffer after the provided offset.
    196    size = mSize - aOffset;
    197  } else {
    198    // The provided offset is larger than the buffer size.
    199    // The parent side will handle the error, we can let the requested size be
    200    // zero.
    201  }
    202 
    203  ffi::wgpu_client_buffer_map(GetClient(), mParent->GetId(), GetId(), aMode,
    204                              aOffset, size);
    205 
    206  mMapRequest = promise;
    207 
    208  auto pending_promise = WebGPUChild::PendingBufferMapPromise{
    209      RefPtr(promise),
    210      RefPtr(this),
    211  };
    212  auto& pending_promises = GetChild()->mPendingBufferMapPromises;
    213  if (auto search = pending_promises.find(GetId());
    214      search != pending_promises.end()) {
    215    search->second.push_back(std::move(pending_promise));
    216  } else {
    217    pending_promises.insert({GetId(), {std::move(pending_promise)}});
    218  }
    219 
    220  return promise.forget();
    221 }
    222 
    223 static void ExternalBufferFreeCallback(void* aContents, void* aUserData) {
    224  (void)aContents;
    225  auto shm = static_cast<std::shared_ptr<ipc::SharedMemoryMapping>*>(aUserData);
    226  delete shm;
    227 }
    228 
    229 void Buffer::GetMappedRange(JSContext* aCx, uint64_t aOffset,
    230                            const dom::Optional<uint64_t>& aSize,
    231                            JS::Rooted<JSObject*>* aObject, ErrorResult& aRv) {
    232  // The WebGPU spec spells out the validation we must perform, but
    233  // use `CheckedInt<uint64_t>` anyway to catch our mistakes. Except
    234  // where we explicitly say otherwise, invalid `CheckedInt` values
    235  // should only arise when we have a bug, so just calling
    236  // `CheckedInt::value` where needed should be fine (it checks with
    237  // `MOZ_DIAGNOSTIC_ASSERT`).
    238 
    239  // https://gpuweb.github.io/gpuweb/#dom-gpubuffer-getmappedrange
    240  //
    241  // Content timeline steps:
    242  //
    243  // 1. If `size` is missing:
    244  //    1. Let `rangeSize` be `max(0, this.size - offset)`.
    245  //    Otherwise, let `rangeSize` be `size`.
    246  const auto offset = CheckedInt<uint64_t>(aOffset);
    247  CheckedInt<uint64_t> rangeSize;
    248  if (aSize.WasPassed()) {
    249    rangeSize = aSize.Value();
    250  } else {
    251    const auto bufferSize = CheckedInt<uint64_t>(mSize);
    252    // Use `CheckInt`'s underflow detection for `max(0, ...)`.
    253    rangeSize = bufferSize - offset;
    254    if (!rangeSize.isValid()) {
    255      rangeSize = 0;
    256    }
    257  }
    258 
    259  // 2. If any of the following conditions are unsatisfied, throw an
    260  //    `OperationError` and stop.
    261  //
    262  //     - `this.[[mapping]]` is not `null`.
    263  if (!mMapped) {
    264    aRv.ThrowOperationError("Buffer is not mapped");
    265    return;
    266  }
    267 
    268  //     - `offset` is a multiple of 8.
    269  //
    270  // (`operator!=` is not available on `CheckedInt`.)
    271  if (offset.value() % 8 != 0) {
    272    aRv.ThrowOperationError("GetMappedRange offset is not a multiple of 8");
    273    return;
    274  }
    275 
    276  //     - `rangeSize` is a multiple of `4`.
    277  if (rangeSize.value() % 4 != 0) {
    278    aRv.ThrowOperationError("GetMappedRange size is not a multiple of 4");
    279    return;
    280  }
    281 
    282  //     - `offset ≥ this.[[mapping]].range[0]`.
    283  if (offset.value() < mMapped->mOffset) {
    284    aRv.ThrowOperationError(
    285        "GetMappedRange offset starts before buffer's mapped range");
    286    return;
    287  }
    288 
    289  //     - `offset + rangeSize ≤ this.[[mapping]].range[1]`.
    290  //
    291  // Perform the addition in `CheckedInt`, treating overflow as a validation
    292  // error.
    293  const auto rangeEndChecked = offset + rangeSize;
    294  if (!rangeEndChecked.isValid() ||
    295      rangeEndChecked.value() > mMapped->mOffset + mMapped->mSize) {
    296    aRv.ThrowOperationError(
    297        "GetMappedRange range extends beyond buffer's mapped range");
    298    return;
    299  }
    300 
    301  //     - `[offset, offset + rangeSize)` does not overlap another range
    302  //       in `this.[[mapping]].views`.
    303  const uint64_t rangeEnd = rangeEndChecked.value();
    304  for (const auto& view : mMapped->mViews) {
    305    if (view.mOffset < rangeEnd && offset.value() < view.mRangeEnd) {
    306      aRv.ThrowOperationError(
    307          "GetMappedRange range overlaps with existing buffer view");
    308      return;
    309    }
    310  }
    311 
    312  // 3. Let `data` be `this.[[mapping]].data`.
    313  //
    314  // The creation of a *pointer to* a `shared_ptr` here seems redundant but is
    315  // unfortunately necessary: `JS::BufferContentsDeleter` requires that its
    316  // `userData` be a `void*`, and while `shared_ptr` can't be inter-converted
    317  // with `void*` (it's actually two pointers), `shared_ptr*` obviously can.
    318  std::shared_ptr<ipc::SharedMemoryMapping>* data =
    319      new std::shared_ptr<ipc::SharedMemoryMapping>(mShmem);
    320 
    321  // 4. Let `view` be (potentially fallible operation follows) create an
    322  //    `ArrayBuffer` of size `rangeSize`, but with its pointer mutably
    323  //    referencing the content of `data` at offset `(offset -
    324  //    [[mapping]].range[0])`.
    325  //
    326  // Since `size_t` may not be the same as `uint64_t`, check, convert, and check
    327  // again. `CheckedInt<size_t>(x)` produces an invalid value if `x` is not in
    328  // range for `size_t` before any conversion is performed.
    329  const auto checkedSize = CheckedInt<size_t>(rangeSize.value()).value();
    330  const auto checkedOffset = CheckedInt<size_t>(offset.value()).value();
    331  const auto span =
    332      (*data)->DataAsSpan<uint8_t>().Subspan(checkedOffset, checkedSize);
    333  UniquePtr<void, JS::BufferContentsDeleter> contents{
    334      span.data(), {&ExternalBufferFreeCallback, data}};
    335  JS::Rooted<JSObject*> view(
    336      aCx, JS::NewExternalArrayBuffer(aCx, checkedSize, std::move(contents)));
    337  if (!view) {
    338    aRv.NoteJSContextException(aCx);
    339    return;
    340  }
    341 
    342  aObject->set(view);
    343  mMapped->mViews.AppendElement(
    344      MappedView({checkedOffset, rangeEnd, *aObject}));
    345 }
    346 
    347 void Buffer::UnmapArrayBuffers(JSContext* aCx, ErrorResult& aRv) {
    348  MOZ_ASSERT(mMapped);
    349 
    350  bool detachedArrayBuffers = true;
    351  for (const auto& view : mMapped->mViews) {
    352    JS::Rooted<JSObject*> rooted(aCx, view.mArrayBuffer);
    353    if (!JS::DetachArrayBuffer(aCx, rooted)) {
    354      detachedArrayBuffers = false;
    355    }
    356  };
    357 
    358  mMapped->mViews.Clear();
    359 
    360  AbortMapRequest();
    361 
    362  if (NS_WARN_IF(!detachedArrayBuffers)) {
    363    aRv.NoteJSContextException(aCx);
    364    return;
    365  }
    366 }
    367 
    368 void Buffer::ResolveMapRequest(dom::Promise* aPromise, BufferAddress aOffset,
    369                               BufferAddress aSize, bool aWritable) {
    370  MOZ_RELEASE_ASSERT(mMapRequest == aPromise);
    371  SetMapped(aOffset, aSize, aWritable);
    372  mMapRequest->MaybeResolveWithUndefined();
    373  mMapRequest = nullptr;
    374 }
    375 
    376 void Buffer::RejectMapRequest(dom::Promise* aPromise,
    377                              const nsACString& message) {
    378  MOZ_RELEASE_ASSERT(mMapRequest == aPromise);
    379  mMapRequest->MaybeRejectWithOperationError(message);
    380  mMapRequest = nullptr;
    381 }
    382 
    383 void Buffer::RejectMapRequestWithAbortError(dom::Promise* aPromise) {
    384  MOZ_RELEASE_ASSERT(mMapRequest == aPromise);
    385  AbortMapRequest();
    386 }
    387 
    388 void Buffer::AbortMapRequest() {
    389  if (mMapRequest) {
    390    mMapRequest->MaybeRejectWithAbortError("Buffer unmapped");
    391  }
    392  mMapRequest = nullptr;
    393 }
    394 
    395 void Buffer::Unmap(JSContext* aCx, ErrorResult& aRv) {
    396  if (!mMapped) {
    397    return;
    398  }
    399 
    400  UnmapArrayBuffers(aCx, aRv);
    401 
    402  bool hasMapFlags = mUsage & (dom::GPUBufferUsage_Binding::MAP_WRITE |
    403                               dom::GPUBufferUsage_Binding::MAP_READ);
    404 
    405  if (!hasMapFlags) {
    406    // We get here if the buffer was mapped at creation without map flags.
    407    // It won't be possible to map the buffer again so we can get rid of
    408    // our shmem on this side.
    409    mShmem = std::make_shared<ipc::SharedMemoryMapping>();
    410  }
    411 
    412  ffi::wgpu_client_buffer_unmap(GetClient(), mParent->GetId(), GetId(),
    413                                mMapped->mWritable);
    414 
    415  mMapped.reset();
    416 }
    417 
    418 void Buffer::Destroy(JSContext* aCx, ErrorResult& aRv) {
    419  if (mMapped) {
    420    Unmap(aCx, aRv);
    421  }
    422 
    423  ffi::wgpu_client_destroy_buffer(GetClient(), GetId());
    424 }
    425 
    426 dom::GPUBufferMapState Buffer::MapState() const {
    427  // Implementation reference:
    428  // <https://gpuweb.github.io/gpuweb/#dom-gpubuffer-mapstate>.
    429 
    430  if (mMapped) {
    431    return dom::GPUBufferMapState::Mapped;
    432  }
    433  if (mMapRequest) {
    434    return dom::GPUBufferMapState::Pending;
    435  }
    436  return dom::GPUBufferMapState::Unmapped;
    437 }
    438 
    439 }  // namespace mozilla::webgpu