blob: 19b80a53817a293f5cc643bd385c19db4d11e1ab [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// 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/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/typed_arrays/dom_array_buffer.h"
#include "third_party/blink/renderer/modules/webgpu/dawn_callback.h"
#include "third_party/blink/renderer/modules/webgpu/dawn_conversions.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/heap/heap.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.
constexpr uint64_t kGuaranteedBufferOOMSize =
std::numeric_limits<size_t>::max();
WGPUBufferDescriptor AsDawnType(const GPUBufferDescriptor* webgpu_desc,
std::string* label) {
DCHECK(webgpu_desc);
DCHECK(label);
WGPUBufferDescriptor dawn_desc = {};
dawn_desc.nextInChain = nullptr;
dawn_desc.usage = AsDawnEnum<WGPUBufferUsage>(webgpu_desc->usage());
dawn_desc.size = webgpu_desc->size();
dawn_desc.mappedAtCreation = webgpu_desc->mappedAtCreation();
if (webgpu_desc->hasLabel()) {
*label = webgpu_desc->label().Utf8();
dawn_desc.label = label->c_str();
}
return dawn_desc;
}
} // namespace
// static
GPUBuffer* GPUBuffer::Create(GPUDevice* device,
const GPUBufferDescriptor* webgpu_desc) {
DCHECK(device);
std::string label;
WGPUBufferDescriptor dawn_desc = AsDawnType(webgpu_desc, &label);
// 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 & (WGPUBufferUsage_MapRead | WGPUBufferUsage_MapWrite) ||
dawn_desc.mappedAtCreation;
if (is_mappable) {
dawn_desc.size = std::min(dawn_desc.size, kGuaranteedBufferOOMSize);
}
GPUBuffer* buffer = MakeGarbageCollected<GPUBuffer>(
device, dawn_desc.size,
device->GetProcs().deviceCreateBuffer(device->GetHandle(), &dawn_desc));
buffer->setLabel(webgpu_desc->label());
return buffer;
}
GPUBuffer::GPUBuffer(GPUDevice* device,
uint64_t size,
WGPUBuffer buffer)
: DawnObject<WGPUBuffer>(device, buffer), size_(size) {
}
void GPUBuffer::Trace(Visitor* visitor) const {
visitor->Trace(mapped_array_buffers_);
DawnObject<WGPUBuffer>::Trace(visitor);
}
ScriptPromise GPUBuffer::mapAsync(ScriptState* script_state,
uint32_t mode,
uint64_t offset,
ExceptionState& exception_state) {
return MapAsyncImpl(script_state, mode, offset, base::nullopt,
exception_state);
}
ScriptPromise 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(uint64_t offset,
ExceptionState& exception_state) {
return GetMappedRangeImpl(offset, base::nullopt, exception_state);
}
DOMArrayBuffer* GPUBuffer::getMappedRange(uint64_t offset,
uint64_t size,
ExceptionState& exception_state) {
return GetMappedRangeImpl(offset, size, exception_state);
}
void GPUBuffer::unmap(ScriptState* script_state) {
ResetMappingState(script_state);
GetProcs().bufferUnmap(GetHandle());
}
void GPUBuffer::destroy(ScriptState* script_state) {
ResetMappingState(script_state);
GetProcs().bufferDestroy(GetHandle());
}
ScriptPromise GPUBuffer::MapAsyncImpl(ScriptState* script_state,
uint32_t mode,
uint64_t offset,
base::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));
ScriptPromiseResolver* resolver =
MakeGarbageCollected<ScriptPromiseResolver>(script_state);
ScriptPromise promise = resolver->Promise();
// And send the command, leaving remaining validation to Dawn.
auto* callback =
BindDawnOnceCallback(&GPUBuffer::OnMapAsyncCallback, WrapPersistent(this),
WrapPersistent(resolver));
GetProcs().bufferMapAsync(GetHandle(), mode, map_offset, map_size,
callback->UnboundCallback(),
callback->AsUserdata());
// WebGPU guarantees that promises are resolved in finite time so we
// need to ensure commands are flushed.
EnsureFlush();
return promise;
}
DOMArrayBuffer* GPUBuffer::GetMappedRangeImpl(uint64_t offset,
base::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));
// The maximum size that can be mapped in JS so that we can ensure we don't
// create mappable buffers bigger than it.
// This could eventually be upgrade to the max ArrayBuffer size instead of the
// max TypedArray size. See crbug.com/951196
if (range_size > v8::TypedArray::kMaxLength) {
exception_state.ThrowRangeError(
"getMappedRange failed, size is too large for the implementation");
return nullptr;
}
if (range_size > std::numeric_limits<size_t>::max() - range_offset) {
exception_state.ThrowDOMException(
DOMExceptionCode::kOperationError,
"getMappedRange failed, offset + size overflows size_t");
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 = GetProcs().bufferGetConstMappedRange(
GetHandle(), range_offset, range_size);
if (!map_data_const) {
// TODO: have explanatory error messages here (or just leave them to the
// asynchronous error reporting).
exception_state.ThrowDOMException(DOMExceptionCode::kOperationError,
"getMappedRange failed");
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(map_data, range_size);
}
void GPUBuffer::OnMapAsyncCallback(ScriptPromiseResolver* resolver,
WGPUBufferMapAsyncStatus status) {
switch (status) {
case WGPUBufferMapAsyncStatus_Success:
resolver->Resolve();
break;
case WGPUBufferMapAsyncStatus_Error:
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kOperationError, "Could not mapAsync"));
break;
case WGPUBufferMapAsyncStatus_Unknown:
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kOperationError, "Unknown error in mapAsync"));
break;
case WGPUBufferMapAsyncStatus_DeviceLost:
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kAbortError, "Device is lost"));
break;
case WGPUBufferMapAsyncStatus_DestroyedBeforeCallback:
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kAbortError,
"Buffer is destroyed before the mapping is resolved"));
break;
case WGPUBufferMapAsyncStatus_UnmappedBeforeCallback:
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kAbortError,
"Buffer is unmapped before the mapping is resolved"));
break;
default:
NOTREACHED();
}
}
DOMArrayBuffer* GPUBuffer::CreateArrayBufferForMappedData(void* data,
size_t data_length) {
DCHECK(data);
DCHECK_LE(static_cast<uint64_t>(data_length), v8::TypedArray::kMaxLength);
ArrayBufferContents contents(data, data_length,
v8::BackingStore::EmptyDeleter);
DOMArrayBuffer* array_buffer = DOMArrayBuffer::Create(contents);
mapped_array_buffers_.push_back(array_buffer);
return array_buffer;
}
void GPUBuffer::ResetMappingState(ScriptState* script_state) {
mapped_ranges_.clear();
for (Member<DOMArrayBuffer>& mapped_array_buffer : mapped_array_buffers_) {
v8::Isolate* isolate = script_state->GetIsolate();
DOMArrayBuffer* array_buffer = mapped_array_buffer.Release();
DCHECK(array_buffer->IsDetachable(isolate));
// Detach the array buffer by transferring the contents out and dropping
// them.
ArrayBufferContents contents;
bool did_detach = array_buffer->Transfer(isolate, contents);
// |did_detach| would be false if the buffer were already detached.
DCHECK(did_detach);
// TODO(crbug.com/1326210): Temporary CHECK to prevent aliased array
// buffers.
CHECK(array_buffer->IsDetached());
}
mapped_array_buffers_.clear();
}
} // namespace blink