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