| // Copyright 2019 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/modules/webgpu/gpu_buffer.h" |
| |
| #include <cinttypes> |
| #include <utility> |
| |
| #include "base/numerics/checked_math.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "gpu/command_buffer/client/webgpu_interface.h" |
| #include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h" |
| #include "third_party/blink/renderer/bindings/modules/v8/v8_gpu_buffer_descriptor.h" |
| #include "third_party/blink/renderer/bindings/modules/v8/v8_gpu_buffer_map_state.h" |
| #include "third_party/blink/renderer/core/dom/dom_exception.h" |
| #include "third_party/blink/renderer/core/execution_context/execution_context.h" |
| #include "third_party/blink/renderer/core/typed_arrays/dom_array_buffer.h" |
| #include "third_party/blink/renderer/modules/webgpu/dawn_conversions.h" |
| #include "third_party/blink/renderer/modules/webgpu/gpu.h" |
| #include "third_party/blink/renderer/modules/webgpu/gpu_adapter.h" |
| #include "third_party/blink/renderer/modules/webgpu/gpu_device.h" |
| #include "third_party/blink/renderer/modules/webgpu/gpu_queue.h" |
| #include "third_party/blink/renderer/platform/graphics/gpu/webgpu_callback.h" |
| #include "third_party/blink/renderer/platform/heap/garbage_collected.h" |
| #include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h" |
| #include "third_party/blink/renderer/platform/wtf/text/string_builder.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| // A size that if used to create a dawn_wire buffer, will guarantee we'll OOM |
| // immediately. It is an implementation detail of dawn_wire but that's tested |
| // on CQ in Dawn. Note that we set kGuaranteedBufferOOMSize to |
| // (wgpu::kWholeMapSize - 1) to ensure we never pass wgpu::kWholeMapSize from |
| // blink to wire_client. |
| constexpr uint64_t kGuaranteedBufferOOMSize = wgpu::kWholeMapSize - 1u; |
| |
| wgpu::BufferDescriptor AsDawnType(const GPUBufferDescriptor* webgpu_desc, |
| std::string* label) { |
| DCHECK(webgpu_desc); |
| DCHECK(label); |
| |
| wgpu::BufferDescriptor dawn_desc = { |
| .usage = AsDawnFlags<wgpu::BufferUsage>(webgpu_desc->usage()), |
| .size = webgpu_desc->size(), |
| .mappedAtCreation = webgpu_desc->mappedAtCreation(), |
| }; |
| *label = webgpu_desc->label().Utf8(); |
| if (!label->empty()) { |
| dawn_desc.label = label->c_str(); |
| } |
| |
| return dawn_desc; |
| } |
| |
| } // namespace |
| |
| // GPUMappedDOMArrayBuffer is returned from mappings created from |
| // GPUBuffer which point to shared memory. This memory is owned by |
| // the underlying wgpu::Buffer used to implement GPUBuffer. |
| // GPUMappedDOMArrayBuffer exists because mapped DOMArrayBuffers need |
| // to keep their owning GPUBuffer alive, or the shared memory may be |
| // freed while it is in use. It derives from DOMArrayBuffer and holds |
| // a Member<GPUBuffer> to its owner. Alternative ideas might be to keep |
| // the wgpu::Buffer alive using a custom deleter of v8::BackingStore or |
| // ArrayBufferContents. However, since these are non-GC objects, it |
| // becomes complex to handle destruction when the last reference to |
| // the wgpu::Buffer may be held either by a GC object, or a non-GC object. |
| class GPUMappedDOMArrayBuffer : public DOMArrayBuffer { |
| static constexpr char kWebGPUBufferMappingDetachKey[] = "WebGPUBufferMapping"; |
| |
| public: |
| static GPUMappedDOMArrayBuffer* Create(v8::Isolate* isolate, |
| GPUBuffer* owner, |
| ArrayBufferContents contents) { |
| auto* mapped_array_buffer = MakeGarbageCollected<GPUMappedDOMArrayBuffer>( |
| owner, std::move(contents)); |
| mapped_array_buffer->SetDetachKey(isolate, kWebGPUBufferMappingDetachKey); |
| return mapped_array_buffer; |
| } |
| |
| GPUMappedDOMArrayBuffer(GPUBuffer* owner, ArrayBufferContents contents) |
| : DOMArrayBuffer(std::move(contents)), owner_(owner) {} |
| ~GPUMappedDOMArrayBuffer() override = default; |
| |
| void DetachContents(v8::Isolate* isolate) { |
| if (IsDetached()) { |
| return; |
| } |
| NonThrowableExceptionState exception_state; |
| // Detach the array buffer by transferring the contents out and dropping |
| // them. |
| ArrayBufferContents contents; |
| bool result = DOMArrayBuffer::Transfer( |
| isolate, V8AtomicString(isolate, kWebGPUBufferMappingDetachKey), |
| contents, exception_state); |
| // TODO(crbug.com/1326210): Temporary CHECK to prevent aliased array |
| // buffers. |
| CHECK(result && IsDetached()); |
| } |
| |
| // Due to an unusual non-owning backing these array buffers can't be shared |
| // for internal use. |
| bool ShareNonSharedForInternalUse(ArrayBufferContents& result) override { |
| result.Detach(); |
| return false; |
| } |
| |
| void Trace(Visitor* visitor) const override { |
| DOMArrayBuffer::Trace(visitor); |
| visitor->Trace(owner_); |
| } |
| |
| private: |
| Member<GPUBuffer> owner_; |
| }; |
| |
| // static |
| GPUBuffer* GPUBuffer::Create(GPUDevice* device, |
| const GPUBufferDescriptor* webgpu_desc, |
| ExceptionState& exception_state) { |
| DCHECK(device); |
| |
| std::string label; |
| wgpu::BufferDescriptor dawn_desc = AsDawnType(webgpu_desc, &label); |
| |
| // Save the requested size of the buffer, for reflection and defaults. |
| uint64_t buffer_size = dawn_desc.size; |
| // If the buffer is mappable, make sure the size stays in a size_t but still |
| // guarantees that we have an OOM. |
| bool is_mappable = dawn_desc.usage & (wgpu::BufferUsage::MapRead | |
| wgpu::BufferUsage::MapWrite) || |
| dawn_desc.mappedAtCreation; |
| if (is_mappable) { |
| dawn_desc.size = std::min(dawn_desc.size, kGuaranteedBufferOOMSize); |
| } |
| |
| wgpu::Buffer wgpuBuffer = device->GetHandle().CreateBuffer(&dawn_desc); |
| // dawn_wire::client will return nullptr when mappedAtCreation == true and |
| // dawn_wire::client fails to allocate memory for initializing an active |
| // buffer mapping, which is required by latest WebGPU SPEC. |
| if (wgpuBuffer == nullptr) { |
| DCHECK(dawn_desc.mappedAtCreation); |
| exception_state.ThrowRangeError( |
| WTF::String::Format("createBuffer failed, size (%" PRIu64 |
| ") is too large for " |
| "the implementation when " |
| "mappedAtCreation == true", |
| buffer_size)); |
| return nullptr; |
| } |
| |
| GPUBuffer* buffer = MakeGarbageCollected<GPUBuffer>( |
| device, buffer_size, std::move(wgpuBuffer), webgpu_desc->label()); |
| |
| if (is_mappable) { |
| GPU* gpu = device->adapter()->gpu(); |
| gpu->TrackMappableBuffer(buffer); |
| device->TrackMappableBuffer(buffer); |
| buffer->mappable_buffer_handles_ = gpu->mappable_buffer_handles(); |
| } |
| |
| return buffer; |
| } |
| |
| GPUBuffer::GPUBuffer(GPUDevice* device, |
| uint64_t size, |
| wgpu::Buffer buffer, |
| const String& label) |
| : DawnObject<wgpu::Buffer>(device, std::move(buffer), label), size_(size) {} |
| |
| GPUBuffer::~GPUBuffer() { |
| if (mappable_buffer_handles_) { |
| mappable_buffer_handles_->erase(GetHandle()); |
| } |
| } |
| |
| void GPUBuffer::Trace(Visitor* visitor) const { |
| visitor->Trace(mapped_array_buffers_); |
| DawnObject<wgpu::Buffer>::Trace(visitor); |
| } |
| |
| ScriptPromise<IDLUndefined> GPUBuffer::mapAsync( |
| ScriptState* script_state, |
| uint32_t mode, |
| uint64_t offset, |
| ExceptionState& exception_state) { |
| return MapAsyncImpl(script_state, mode, offset, std::nullopt, |
| exception_state); |
| } |
| |
| ScriptPromise<IDLUndefined> GPUBuffer::mapAsync( |
| ScriptState* script_state, |
| uint32_t mode, |
| uint64_t offset, |
| uint64_t size, |
| ExceptionState& exception_state) { |
| return MapAsyncImpl(script_state, mode, offset, size, exception_state); |
| } |
| |
| DOMArrayBuffer* GPUBuffer::getMappedRange(ScriptState* script_state, |
| uint64_t offset, |
| ExceptionState& exception_state) { |
| return GetMappedRangeImpl(script_state, offset, std::nullopt, |
| exception_state); |
| } |
| |
| DOMArrayBuffer* GPUBuffer::getMappedRange(ScriptState* script_state, |
| uint64_t offset, |
| uint64_t size, |
| ExceptionState& exception_state) { |
| return GetMappedRangeImpl(script_state, offset, size, exception_state); |
| } |
| |
| void GPUBuffer::unmap(v8::Isolate* isolate) { |
| ResetMappingState(isolate); |
| GetHandle().Unmap(); |
| } |
| |
| void GPUBuffer::destroy(v8::Isolate* isolate) { |
| ResetMappingState(isolate); |
| GetHandle().Destroy(); |
| // Destroyed, so it can never be mapped again. Stop tracking. |
| device_->adapter()->gpu()->UntrackMappableBuffer(this); |
| device_->UntrackMappableBuffer(this); |
| // Drop the reference to the mapped buffer handles. No longer |
| // need to remove the wgpu::Buffer from this set in ~GPUBuffer. |
| mappable_buffer_handles_ = nullptr; |
| } |
| |
| uint64_t GPUBuffer::size() const { |
| return size_; |
| } |
| |
| uint32_t GPUBuffer::usage() const { |
| return static_cast<uint32_t>(GetHandle().GetUsage()); |
| } |
| |
| V8GPUBufferMapState GPUBuffer::mapState() const { |
| return FromDawnEnum(GetHandle().GetMapState()); |
| } |
| |
| ScriptPromise<IDLUndefined> GPUBuffer::MapAsyncImpl( |
| ScriptState* script_state, |
| uint32_t mode, |
| uint64_t offset, |
| std::optional<uint64_t> size, |
| ExceptionState& exception_state) { |
| // Compute the defaulted size which is "until the end of the buffer" or 0 if |
| // offset is past the end of the buffer. |
| uint64_t size_defaulted = 0; |
| if (size) { |
| size_defaulted = *size; |
| } else if (offset <= size_) { |
| size_defaulted = size_ - offset; |
| } |
| |
| // We need to convert from uint64_t to size_t. Either of these two variables |
| // are bigger or equal to the guaranteed OOM size then mapAsync should be an |
| // error so. That OOM size fits in a size_t so we can clamp size and offset |
| // with it. |
| size_t map_offset = |
| static_cast<size_t>(std::min(offset, kGuaranteedBufferOOMSize)); |
| size_t map_size = |
| static_cast<size_t>(std::min(size_defaulted, kGuaranteedBufferOOMSize)); |
| |
| auto* resolver = MakeGarbageCollected<ScriptPromiseResolver<IDLUndefined>>( |
| script_state, exception_state.GetContext()); |
| auto promise = resolver->Promise(); |
| |
| // And send the command, leaving remaining validation to Dawn. |
| auto* callback = MakeWGPUOnceCallback(resolver->WrapCallbackInScriptScope( |
| WTF::BindOnce(&GPUBuffer::OnMapAsyncCallback, WrapPersistent(this)))); |
| |
| GetHandle().MapAsync(static_cast<wgpu::MapMode>(mode), map_offset, map_size, |
| wgpu::CallbackMode::AllowSpontaneous, |
| callback->UnboundCallback(), callback->AsUserdata()); |
| |
| // WebGPU guarantees that promises are resolved in finite time so we |
| // need to ensure commands are flushed. |
| EnsureFlush(ToEventLoop(script_state)); |
| return promise; |
| } |
| |
| DOMArrayBuffer* GPUBuffer::GetMappedRangeImpl(ScriptState* script_state, |
| uint64_t offset, |
| std::optional<uint64_t> size, |
| ExceptionState& exception_state) { |
| // Compute the defaulted size which is "until the end of the buffer" or 0 if |
| // offset is past the end of the buffer. |
| uint64_t size_defaulted = 0; |
| if (size) { |
| size_defaulted = *size; |
| } else if (offset <= size_) { |
| size_defaulted = size_ - offset; |
| } |
| |
| // We need to convert from uint64_t to size_t. Either of these two variables |
| // are bigger or equal to the guaranteed OOM size then getMappedRange should |
| // be an error so. That OOM size fits in a size_t so we can clamp size and |
| // offset with it. |
| size_t range_offset = |
| static_cast<size_t>(std::min(offset, kGuaranteedBufferOOMSize)); |
| size_t range_size = |
| static_cast<size_t>(std::min(size_defaulted, kGuaranteedBufferOOMSize)); |
| |
| if (range_size > std::numeric_limits<size_t>::max() - range_offset) { |
| exception_state.ThrowDOMException( |
| DOMExceptionCode::kOperationError, |
| WTF::String::Format( |
| "getMappedRange failed, offset(%zu) + size(%zu) overflows size_t", |
| range_offset, range_size)); |
| return nullptr; |
| } |
| size_t range_end = range_offset + range_size; |
| |
| // Check if an overlapping range has already been returned. |
| // TODO: keep mapped_ranges_ sorted (e.g. std::map), and do a binary search |
| // (e.g. map.upper_bound()) to make this O(lg(n)) instead of linear. |
| // (Note: std::map is not allowed in Blink.) |
| for (const auto& overlap_candidate : mapped_ranges_) { |
| size_t candidate_start = overlap_candidate.first; |
| size_t candidate_end = overlap_candidate.second; |
| if (range_end > candidate_start && range_offset < candidate_end) { |
| exception_state.ThrowDOMException( |
| DOMExceptionCode::kOperationError, |
| WTF::String::Format("getMappedRange [%zu, %zu) overlaps with " |
| "previously returned range [%zu, %zu).", |
| range_offset, range_end, candidate_start, |
| candidate_end)); |
| return nullptr; |
| } |
| } |
| |
| // And send the command, leaving remaining validation to Dawn. |
| const void* map_data_const = |
| GetHandle().GetConstMappedRange(range_offset, range_size); |
| |
| if (!map_data_const) { |
| // Ensure that GPU process error messages are bubbled back to the renderer process. |
| EnsureFlush(ToEventLoop(script_state)); |
| exception_state.ThrowDOMException(DOMExceptionCode::kOperationError, |
| "getMappedRange failed"); |
| return nullptr; |
| } |
| |
| // The maximum size that can be mapped in JS so that we can ensure we don't |
| // create mappable buffers bigger than it. According to ECMAScript SPEC, a |
| // RangeError exception will be thrown if it is impossible to allocate an |
| // array buffer. |
| // This could eventually be upgrade to the max ArrayBuffer size instead of the |
| // max TypedArray size. See crbug.com/951196 |
| // Note that we put this check after the checks in Dawn because the latest |
| // WebGPU SPEC requires the checks on the buffer state (mapped or not) should |
| // be done before the creation of ArrayBuffer. |
| if (range_size > v8::TypedArray::kMaxByteLength) { |
| exception_state.ThrowRangeError( |
| WTF::String::Format("getMappedRange failed, size (%zu) is too large " |
| "for the implementation. max size = %zu", |
| range_size, v8::TypedArray::kMaxByteLength)); |
| return nullptr; |
| } |
| |
| // It is safe to const_cast the |data| pointer because it is a shadow |
| // copy that Dawn wire makes and does not point to the mapped GPU |
| // data. Dawn wire's copy of the data is not used outside of tests. |
| uint8_t* map_data = |
| const_cast<uint8_t*>(static_cast<const uint8_t*>(map_data_const)); |
| |
| mapped_ranges_.push_back(std::make_pair(range_offset, range_end)); |
| return CreateArrayBufferForMappedData(script_state->GetIsolate(), map_data, |
| range_size); |
| } |
| |
| void GPUBuffer::OnMapAsyncCallback( |
| ScriptPromiseResolver<IDLUndefined>* resolver, |
| wgpu::MapAsyncStatus status, |
| wgpu::StringView message) { |
| switch (status) { |
| case wgpu::MapAsyncStatus::Success: |
| resolver->Resolve(); |
| break; |
| #ifdef WGPU_BREAKING_CHANGE_INSTANCE_DROPPED_RENAME |
| case wgpu::MapAsyncStatus::CallbackCancelled: |
| #else |
| case wgpu::MapAsyncStatus::InstanceDropped: |
| #endif // WGPU_BREAKING_CHANGE_INSTANCE_DROPPED_RENAME |
| resolver->RejectWithDOMException(DOMExceptionCode::kAbortError, |
| String::FromUTF8(message)); |
| break; |
| case wgpu::MapAsyncStatus::Aborted: |
| resolver->RejectWithDOMException(DOMExceptionCode::kAbortError, |
| String::FromUTF8(message)); |
| break; |
| case wgpu::MapAsyncStatus::Error: |
| resolver->RejectWithDOMException(DOMExceptionCode::kOperationError, |
| String::FromUTF8(message)); |
| break; |
| } |
| } |
| |
| DOMArrayBuffer* GPUBuffer::CreateArrayBufferForMappedData(v8::Isolate* isolate, |
| void* data, |
| size_t data_length) { |
| DCHECK(data); |
| DCHECK_LE(static_cast<uint64_t>(data_length), v8::TypedArray::kMaxByteLength); |
| |
| ArrayBufferContents contents(v8::ArrayBuffer::NewBackingStore( |
| data, data_length, v8::BackingStore::EmptyDeleter, nullptr)); |
| GPUMappedDOMArrayBuffer* array_buffer = |
| GPUMappedDOMArrayBuffer::Create(isolate, this, contents); |
| mapped_array_buffers_.push_back(array_buffer); |
| return array_buffer; |
| } |
| |
| void GPUBuffer::ResetMappingState(v8::Isolate* isolate) { |
| mapped_ranges_.clear(); |
| DetachMappedArrayBuffers(isolate); |
| } |
| |
| void GPUBuffer::DetachMappedArrayBuffers(v8::Isolate* isolate) { |
| for (Member<GPUMappedDOMArrayBuffer>& mapped_array_buffer : |
| mapped_array_buffers_) { |
| GPUMappedDOMArrayBuffer* array_buffer = mapped_array_buffer.Release(); |
| array_buffer->DetachContents(isolate); |
| } |
| mapped_array_buffers_.clear(); |
| } |
| |
| } // namespace blink |