diff --git a/.clang-format b/.clang-format index 25e8c103c5b..3f0c77fdc76 100644 --- a/.clang-format +++ b/.clang-format @@ -5,3 +5,4 @@ WhitespaceSensitiveMacros: - JSG_TS_DEFINE - JSG_STRUCT_TS_OVERRIDE - JSG_STRUCT_TS_DEFINE +PointerAlignment: Left diff --git a/src/workerd/api/global-scope.h b/src/workerd/api/global-scope.h index da42d3ea2e3..f874a116544 100644 --- a/src/workerd/api/global-scope.h +++ b/src/workerd/api/global-scope.h @@ -493,6 +493,7 @@ class ServiceWorkerGlobalScope: public WorkerGlobalScope { // WebGPU JSG_NESTED_TYPE_NAMED(api::gpu::GPUBufferUsage, GPUBufferUsage); JSG_NESTED_TYPE_NAMED(api::gpu::GPUShaderStage, GPUShaderStage); + JSG_NESTED_TYPE_NAMED(api::gpu::GPUMapMode, GPUMapMode); #endif JSG_TS_ROOT(); diff --git a/src/workerd/api/gpu/gpu-adapter-info.c++ b/src/workerd/api/gpu/gpu-adapter-info.c++ new file mode 100644 index 00000000000..5ce5cf94360 --- /dev/null +++ b/src/workerd/api/gpu/gpu-adapter-info.c++ @@ -0,0 +1,16 @@ +// Copyright (c) 2017-2022 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +#include "gpu-adapter-info.h" +#include "workerd/jsg/exception.h" + +namespace workerd::api::gpu { + +GPUAdapterInfo::GPUAdapterInfo(WGPUAdapterProperties properties) + : vendor_(kj::str(properties.vendorName)), + architecture_(kj::str(properties.architecture)), + device_(kj::str(properties.name)), + description_(kj::str(properties.driverDescription)) {} + +} // namespace workerd::api::gpu diff --git a/src/workerd/api/gpu/gpu-adapter-info.h b/src/workerd/api/gpu/gpu-adapter-info.h new file mode 100644 index 00000000000..c234f8b7570 --- /dev/null +++ b/src/workerd/api/gpu/gpu-adapter-info.h @@ -0,0 +1,33 @@ +// Copyright (c) 2017-2022 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +#pragma once + +#include +#include + +namespace workerd::api::gpu { + +class GPUAdapterInfo : public jsg::Object { +public: + explicit GPUAdapterInfo(WGPUAdapterProperties); + JSG_RESOURCE_TYPE(GPUAdapterInfo) { + JSG_READONLY_PROTOTYPE_PROPERTY(vendor, getVendor); + JSG_READONLY_PROTOTYPE_PROPERTY(architecture, getArchitecture); + JSG_READONLY_PROTOTYPE_PROPERTY(device, getDevice); + JSG_READONLY_PROTOTYPE_PROPERTY(description, getDescription); + } + +private: + kj::String vendor_; + kj::String architecture_; + kj::String device_; + kj::String description_; + kj::StringPtr getVendor() { return vendor_; }; + kj::StringPtr getArchitecture() { return architecture_; }; + kj::StringPtr getDevice() { return device_; }; + kj::StringPtr getDescription() { return description_; }; +}; + +} // namespace workerd::api::gpu diff --git a/src/workerd/api/gpu/gpu-adapter.c++ b/src/workerd/api/gpu/gpu-adapter.c++ index 59cd445f8b1..ed33a38fa86 100644 --- a/src/workerd/api/gpu/gpu-adapter.c++ +++ b/src/workerd/api/gpu/gpu-adapter.c++ @@ -3,27 +3,94 @@ // https://opensource.org/licenses/Apache-2.0 #include "gpu-adapter.h" +#include "gpu-adapter-info.h" +#include "gpu-supported-features.h" +#include "gpu-supported-limits.h" +#include "workerd/jsg/exception.h" + +#define WGPU_FOR_EACH_LIMIT(X) \ + X(maxTextureDimension1D) \ + X(maxTextureDimension2D) \ + X(maxTextureDimension3D) \ + X(maxTextureArrayLayers) \ + X(maxBindGroups) \ + X(maxBindingsPerBindGroup) \ + X(maxDynamicUniformBuffersPerPipelineLayout) \ + X(maxDynamicStorageBuffersPerPipelineLayout) \ + X(maxSampledTexturesPerShaderStage) \ + X(maxSamplersPerShaderStage) \ + X(maxStorageBuffersPerShaderStage) \ + X(maxStorageTexturesPerShaderStage) \ + X(maxUniformBuffersPerShaderStage) \ + X(maxUniformBufferBindingSize) \ + X(maxStorageBufferBindingSize) \ + X(minUniformBufferOffsetAlignment) \ + X(minStorageBufferOffsetAlignment) \ + X(maxVertexBuffers) \ + X(maxBufferSize) \ + X(maxVertexAttributes) \ + X(maxVertexBufferArrayStride) \ + X(maxInterStageShaderComponents) \ + X(maxColorAttachments) \ + X(maxColorAttachmentBytesPerSample) \ + X(maxComputeWorkgroupStorageSize) \ + X(maxComputeInvocationsPerWorkgroup) \ + X(maxComputeWorkgroupSizeX) \ + X(maxComputeWorkgroupSizeY) \ + X(maxComputeWorkgroupSizeZ) \ + X(maxComputeWorkgroupsPerDimension) namespace workerd::api::gpu { +void setLimit(wgpu::RequiredLimits& limits, kj::StringPtr name, + unsigned long long value) { + +#define COPY_LIMIT(LIMIT) \ + if (name == "#LIMIT") { \ + limits.limits.LIMIT = value; \ + return; \ + } + WGPU_FOR_EACH_LIMIT(COPY_LIMIT) +#undef COPY_LIMIT + + JSG_FAIL_REQUIRE(TypeError, "unknown limit", name); +} + +jsg::Promise> GPUAdapter::requestAdapterInfo( + jsg::Lock& js, jsg::Optional> unmaskHints) { + + WGPUAdapterProperties adapterProperties = {}; + adapter_.GetProperties(&adapterProperties); + auto info = jsg::alloc(adapterProperties); + return js.resolvedPromise(kj::mv(info)); +} + jsg::Promise> -GPUAdapter::requestDevice(jsg::Lock &js, +GPUAdapter::requestDevice(jsg::Lock& js, jsg::Optional descriptor) { wgpu::DeviceDescriptor desc{}; kj::Vector requiredFeatures; + wgpu::RequiredLimits limits; KJ_IF_MAYBE (d, descriptor) { - KJ_IF_MAYBE(label, d->label) { + KJ_IF_MAYBE (label, d->label) { desc.label = label->cStr(); } KJ_IF_MAYBE (features, d->requiredFeatures) { - for (auto &required : *features) { + for (auto& required : *features) { requiredFeatures.add(parseFeatureName(required)); } desc.requiredFeaturesCount = requiredFeatures.size(); desc.requiredFeatures = requiredFeatures.begin(); } + + KJ_IF_MAYBE (requiredLimits, d->requiredLimits) { + for (auto& f : requiredLimits->fields) { + setLimit(limits, f.name, f.value); + } + desc.requiredLimits = &limits; + } } struct UserData { @@ -35,20 +102,44 @@ GPUAdapter::requestDevice(jsg::Lock &js, adapter_.RequestDevice( &desc, [](WGPURequestDeviceStatus status, WGPUDevice cDevice, - const char *message, void *pUserData) { + const char* message, void* pUserData) { JSG_REQUIRE(status == WGPURequestDeviceStatus_Success, Error, message); - UserData &userData = *reinterpret_cast(pUserData); + UserData& userData = *reinterpret_cast(pUserData); userData.device = wgpu::Device::Acquire(cDevice); userData.requestEnded = true; }, - (void *)&userData); + (void*)&userData); KJ_ASSERT(userData.requestEnded); jsg::Ref gpuDevice = - jsg::alloc(kj::mv(userData.device)); + jsg::alloc(js, kj::mv(userData.device)); return js.resolvedPromise(kj::mv(gpuDevice)); } +jsg::Ref GPUAdapter::getFeatures() { + wgpu::Adapter adapter(adapter_.Get()); + size_t count = adapter.EnumerateFeatures(nullptr); + kj::Array features = + kj::heapArray(count); + adapter.EnumerateFeatures(&features[0]); + return jsg::alloc(kj::mv(features)); +} + +jsg::Ref GPUAdapter::getLimits() { + WGPUSupportedLimits limits{}; + JSG_REQUIRE(adapter_.GetLimits(&limits), TypeError, + "failed to get adapter limits"); + + // need to copy to the C++ version of the object + wgpu::SupportedLimits wgpuLimits{}; + +#define COPY_LIMIT(LIMIT) wgpuLimits.limits.LIMIT = limits.limits.LIMIT; + WGPU_FOR_EACH_LIMIT(COPY_LIMIT) +#undef COPY_LIMIT + + return jsg::alloc(kj::mv(wgpuLimits)); +} + } // namespace workerd::api::gpu diff --git a/src/workerd/api/gpu/gpu-adapter.h b/src/workerd/api/gpu/gpu-adapter.h index 9aa8e32bd78..880d2f5c82c 100644 --- a/src/workerd/api/gpu/gpu-adapter.h +++ b/src/workerd/api/gpu/gpu-adapter.h @@ -4,7 +4,10 @@ #pragma once +#include "gpu-adapter-info.h" #include "gpu-device.h" +#include "gpu-supported-features.h" +#include "gpu-supported-limits.h" #include "gpu-utils.h" #include #include @@ -15,12 +18,22 @@ namespace workerd::api::gpu { class GPUAdapter : public jsg::Object { public: explicit GPUAdapter(dawn::native::Adapter a) : adapter_(a){}; - JSG_RESOURCE_TYPE(GPUAdapter) { JSG_METHOD(requestDevice); } + JSG_RESOURCE_TYPE(GPUAdapter) { + JSG_METHOD(requestDevice); + JSG_METHOD(requestAdapterInfo); + JSG_READONLY_PROTOTYPE_PROPERTY(features, getFeatures); + JSG_READONLY_PROTOTYPE_PROPERTY(limits, getLimits); + } private: jsg::Promise> requestDevice(jsg::Lock &, jsg::Optional); dawn::native::Adapter adapter_; + jsg::Promise> + requestAdapterInfo(jsg::Lock &js, + jsg::Optional> unmaskHints); + jsg::Ref getFeatures(); + jsg::Ref getLimits(); }; } // namespace workerd::api::gpu diff --git a/src/workerd/api/gpu/gpu-async-runner.c++ b/src/workerd/api/gpu/gpu-async-runner.c++ new file mode 100644 index 00000000000..68a5547bd2d --- /dev/null +++ b/src/workerd/api/gpu/gpu-async-runner.c++ @@ -0,0 +1,44 @@ +// Copyright (c) 2017-2022 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +#include "gpu-async-runner.h" +#include "workerd/io/io-context.h" +#include +#include + +#define BUSY_LOOP_DELAY_MS 50 + +namespace workerd::api::gpu { + +void AsyncRunner::Begin() { + KJ_ASSERT(count_ != std::numeric_limits::max()); + if (count_++ == 0) { + QueueTick(); + } +} + +void AsyncRunner::End() { + KJ_ASSERT(count_ > 0); + count_--; +} + +void AsyncRunner::QueueTick() { + if (tick_queued_) { + return; + } + tick_queued_ = true; + + IoContext::current().setTimeoutImpl( + timeoutIdGenerator, false, + [this](jsg::Lock &js) mutable { + this->tick_queued_ = false; + if (this->count_ > 0) { + this->device_.Tick(); + QueueTick(); + } + }, + BUSY_LOOP_DELAY_MS); +} + +} // namespace workerd::api::gpu diff --git a/src/workerd/api/gpu/gpu-async-runner.h b/src/workerd/api/gpu/gpu-async-runner.h new file mode 100644 index 00000000000..56b3c73ec3c --- /dev/null +++ b/src/workerd/api/gpu/gpu-async-runner.h @@ -0,0 +1,61 @@ +// Copyright (c) 2017-2022 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 +// Based on the dawn node bindings + +#pragma once + +#include "workerd/io/io-context.h" +#include +#include + +namespace workerd::api::gpu { + +// AsyncRunner is used to poll a wgpu::Device with calls to Tick() while there +// are asynchronous tasks in flight. +class AsyncRunner : public kj::Refcounted { +public: + AsyncRunner(wgpu::Device device) : device_(device){}; + + // Begin() should be called when a new asynchronous task is started. + // If the number of executing asynchronous tasks transitions from 0 to 1, then + // a function will be scheduled on the main JavaScript thread to call + // wgpu::Device::Tick() whenever the thread is idle. This will be repeatedly + // called until the number of executing asynchronous tasks reaches 0 again. + void Begin(); + + // End() should be called once the asynchronous task has finished. + // Every call to Begin() should eventually result in a call to End(). + void End(); + +private: + void QueueTick(); + wgpu::Device const device_; + uint64_t count_ = 0; + bool tick_queued_ = false; + TimeoutId::Generator timeoutIdGenerator; +}; + +// AsyncTask is a RAII helper for calling AsyncRunner::Begin() on construction, +// and AsyncRunner::End() on destruction. +class AsyncTask { +public: + inline AsyncTask(AsyncTask &&) = default; + + // Constructor. + // Calls AsyncRunner::Begin() + explicit inline AsyncTask(kj::Own runner) + : runner_(std::move(runner)) { + runner_->Begin(); + } + + // Destructor. + // Calls AsyncRunner::End() + inline ~AsyncTask() { runner_->End(); } + +private: + KJ_DISALLOW_COPY(AsyncTask); + kj::Own runner_; +}; + +} // namespace workerd::api::gpu diff --git a/src/workerd/api/gpu/gpu-bindgroup-layout.c++ b/src/workerd/api/gpu/gpu-bindgroup-layout.c++ index e8d2f9f1ff0..13e6ebee0bd 100644 --- a/src/workerd/api/gpu/gpu-bindgroup-layout.c++ +++ b/src/workerd/api/gpu/gpu-bindgroup-layout.c++ @@ -19,27 +19,17 @@ wgpu::BufferBindingType parseBufferBindingType(kj::StringPtr bType) { return wgpu::BufferBindingType::ReadOnlyStorage; } - KJ_FAIL_REQUIRE("unknown buffer binding type", bType); + JSG_FAIL_REQUIRE(TypeError, "unknown buffer binding type", bType); } wgpu::BufferBindingLayout -parseBufferBindingLayout(GPUBufferBindingLayout &buffer) { +parseBufferBindingLayout(GPUBufferBindingLayout& buffer) { wgpu::BufferBindingLayout l; - l.type = wgpu::BufferBindingType::Uniform; - l.hasDynamicOffset = false; - l.minBindingSize = 0; - KJ_IF_MAYBE (type, buffer.type) { - l.type = parseBufferBindingType(*type); - } - - KJ_IF_MAYBE (offset, buffer.hasDynamicOffset) { - l.hasDynamicOffset = *offset; - } - - KJ_IF_MAYBE (minSize, buffer.minBindingSize) { - l.minBindingSize = *minSize; - } + l.type = parseBufferBindingType( + buffer.type.orDefault([] { return "uniform"_kj; })); + l.hasDynamicOffset = buffer.hasDynamicOffset.orDefault(false); + l.minBindingSize = buffer.minBindingSize.orDefault(0); return kj::mv(l); } @@ -57,17 +47,14 @@ wgpu::SamplerBindingType parseSamplerBindingType(kj::StringPtr bType) { return wgpu::SamplerBindingType::Comparison; } - KJ_FAIL_REQUIRE("unknown sampler binding type", bType); + JSG_FAIL_REQUIRE(TypeError, "unknown sampler binding type", bType); } wgpu::SamplerBindingLayout -parseSamplerBindingLayout(GPUSamplerBindingLayout &sampler) { +parseSamplerBindingLayout(GPUSamplerBindingLayout& sampler) { wgpu::SamplerBindingLayout s; - s.type = wgpu::SamplerBindingType::Filtering; - - KJ_IF_MAYBE (sType, sampler.type) { - s.type = parseSamplerBindingType(*sType); - } + s.type = parseSamplerBindingType( + sampler.type.orDefault([] { return "filtering"_kj; })); return kj::mv(s); } @@ -93,7 +80,7 @@ wgpu::TextureSampleType parseTextureSampleType(kj::StringPtr sType) { return wgpu::TextureSampleType::Uint; } - KJ_FAIL_REQUIRE("unknown texture sample type", sType); + JSG_FAIL_REQUIRE(TypeError, "unknown texture sample type", sType); } wgpu::TextureViewDimension parseTextureViewDimension(kj::StringPtr dim) { @@ -122,27 +109,17 @@ wgpu::TextureViewDimension parseTextureViewDimension(kj::StringPtr dim) { return wgpu::TextureViewDimension::e3D; } - KJ_FAIL_REQUIRE("unknown texture view dimension", dim); + JSG_FAIL_REQUIRE(TypeError, "unknown texture view dimension", dim); } wgpu::TextureBindingLayout -parseTextureBindingLayout(GPUTextureBindingLayout &texture) { +parseTextureBindingLayout(GPUTextureBindingLayout& texture) { wgpu::TextureBindingLayout t; - t.sampleType = wgpu::TextureSampleType::Float; - t.viewDimension = wgpu::TextureViewDimension::e2D; - t.multisampled = false; - - KJ_IF_MAYBE (sType, texture.sampleType) { - t.sampleType = parseTextureSampleType(*sType); - } - - KJ_IF_MAYBE (dim, texture.viewDimension) { - t.viewDimension = parseTextureViewDimension(*dim); - } - - KJ_IF_MAYBE (ms, texture.multisampled) { - t.multisampled = *ms; - } + t.sampleType = parseTextureSampleType( + texture.sampleType.orDefault([] { return "float"_kj; })); + t.viewDimension = parseTextureViewDimension( + texture.viewDimension.orDefault([] { return "2d"_kj; })); + t.multisampled = texture.multisampled.orDefault(false); return kj::mv(t); } @@ -525,7 +502,7 @@ wgpu::TextureFormat parseTextureFormat(kj::StringPtr format) { return wgpu::TextureFormat::ASTC12x12UnormSrgb; } - KJ_FAIL_REQUIRE("unknown texture format", format); + JSG_FAIL_REQUIRE(TypeError, "unknown texture format", format); } wgpu::StorageTextureAccess parseStorageAccess(kj::StringPtr access) { @@ -533,30 +510,24 @@ wgpu::StorageTextureAccess parseStorageAccess(kj::StringPtr access) { return wgpu::StorageTextureAccess::WriteOnly; } - KJ_FAIL_REQUIRE("unknown storage access", access); + JSG_FAIL_REQUIRE(TypeError, "unknown storage access", access); } wgpu::StorageTextureBindingLayout -parseStorageTextureBindingLayout(GPUStorageTextureBindingLayout &storage) { +parseStorageTextureBindingLayout(GPUStorageTextureBindingLayout& storage) { wgpu::StorageTextureBindingLayout s; - s.access = wgpu::StorageTextureAccess::WriteOnly; + s.access = parseStorageAccess( + storage.access.orDefault([] { return "write-only"_kj; })); s.format = parseTextureFormat(storage.format); - s.viewDimension = wgpu::TextureViewDimension::e2D; - - KJ_IF_MAYBE (access, storage.access) { - s.access = parseStorageAccess(*access); - } - - KJ_IF_MAYBE (dimension, storage.viewDimension) { - s.viewDimension = parseTextureViewDimension(*dimension); - } + s.viewDimension = parseTextureViewDimension( + storage.viewDimension.orDefault([] { return "2d"_kj; })); return kj::mv(s); } wgpu::BindGroupLayoutEntry -parseBindGroupLayoutEntry(GPUBindGroupLayoutEntry &entry) { +parseBindGroupLayoutEntry(GPUBindGroupLayoutEntry& entry) { wgpu::BindGroupLayoutEntry e; e.binding = entry.binding; e.visibility = static_cast(entry.visibility); diff --git a/src/workerd/api/gpu/gpu-buffer.c++ b/src/workerd/api/gpu/gpu-buffer.c++ index d55c33d6f32..b30c21c78b9 100644 --- a/src/workerd/api/gpu/gpu-buffer.c++ +++ b/src/workerd/api/gpu/gpu-buffer.c++ @@ -3,61 +3,180 @@ // https://opensource.org/licenses/Apache-2.0 #include "gpu-buffer.h" -#include <_types/_uint64_t.h> +#include "workerd/io/io-context.h" +#include "workerd/jsg/exception.h" +#include "workerd/jsg/jsg.h" namespace workerd::api::gpu { -GPUBuffer::GPUBuffer(wgpu::Buffer b, wgpu::BufferDescriptor desc) - : buffer_(kj::mv(b)), desc_(kj::mv(desc)) { +GPUBuffer::GPUBuffer(jsg::Lock& js, wgpu::Buffer b, wgpu::BufferDescriptor desc, + wgpu::Device device, kj::Own async) + : buffer_(kj::mv(b)), device_(kj::mv(device)), desc_(kj::mv(desc)), + async_(kj::mv(async)), + detachKey_(jsg::V8Ref(js.v8Isolate, v8::Object::New(js.v8Isolate))) { if (desc.mappedAtCreation) { state_ = State::MappedAtCreation; } }; -kj::ArrayPtr -GPUBuffer::getMappedRange(jsg::Optional offset, +v8::Local +GPUBuffer::getMappedRange(jsg::Lock& js, jsg::Optional offset, jsg::Optional size) { JSG_REQUIRE(state_ == State::Mapped || state_ == State::MappedAtCreation, TypeError, "trying to get mapped range of unmapped buffer"); - uint64_t o = 0; - KJ_IF_MAYBE (real_offset, offset) { - o = *real_offset; - } - - uint64_t s = desc_.size - o; - KJ_IF_MAYBE (real_size, size) { - s = *real_size; - } + uint64_t o = offset.orDefault(0); + uint64_t s = size.orDefault(desc_.size - o); uint64_t start = o; uint64_t end = o + s; - for (auto &mapping : mapped_) { + for (auto& mapping : mapped_) { if (mapping.Intersects(start, end)) { - KJ_FAIL_REQUIRE("mapping intersects with existing one"); + JSG_FAIL_REQUIRE(TypeError, "mapping intersects with existing one"); } } - auto *ptr = (desc_.usage & wgpu::BufferUsage::MapWrite) + auto* ptr = (desc_.usage & wgpu::BufferUsage::MapWrite) ? buffer_.GetMappedRange(o, s) - : const_cast(buffer_.GetConstMappedRange(o, s)); + : const_cast(buffer_.GetConstMappedRange(o, s)); JSG_REQUIRE(ptr, TypeError, "could not obtain mapped range"); - auto arrayBuffer = kj::arrayPtr((byte *)ptr, s); - mapped_.add(Mapping{start, end, arrayBuffer}); + struct Context { + jsg::Ref buffer; + }; + auto ref = JSG_THIS; + // We're creating this context object in order for it to be available when the + // callback is invoked. It owns a persistent reference to this GPUBuffer + // object to ensure that it still lives while the arraybuffer is in scope. + // This object will be deallocated when the callback finishes. + auto ctx = new Context{ref.addRef()}; + std::shared_ptr backing = v8::ArrayBuffer::NewBackingStore( + ptr, s, + [](void* data, size_t length, void* deleter_data) { + // we have a persistent reference to GPUBuffer so that it lives at least + // as long as this arraybuffer + // Note: this is invoked outside the JS isolate lock + auto c = std::unique_ptr(static_cast(deleter_data)); + }, + ctx); + + v8::Local arrayBuffer = + v8::ArrayBuffer::New(js.v8Isolate, backing); + arrayBuffer->SetDetachKey(detachKey_.getHandle(js.v8Isolate)); + + mapped_.add(Mapping{start, end, + jsg::V8Ref(js.v8Isolate, arrayBuffer)}); return arrayBuffer; } -void GPUBuffer::unmap() { +void GPUBuffer::DetachMappings(jsg::Lock& js) { + for (auto& mapping : mapped_) { + auto ab = mapping.buffer.getHandle(js.v8Isolate); + + auto res = ab->Detach(detachKey_.getHandle(js.v8Isolate)); + KJ_ASSERT(res.IsJust()); + } + mapped_.clear(); +} + +void GPUBuffer::destroy(jsg::Lock& js) { + if (state_ == State::Destroyed) { + return; + } + + if (state_ != State::Unmapped) { + DetachMappings(js); + } + + buffer_.Destroy(); + state_ = State::Destroyed; +} + +void GPUBuffer::unmap(jsg::Lock& js) { buffer_.Unmap(); if (state_ != State::Destroyed && state_ != State::Unmapped) { - mapped_.clear(); + DetachMappings(js); state_ = State::Unmapped; } } +jsg::Promise GPUBuffer::mapAsync(GPUFlagsConstant mode, + jsg::Optional offset, + jsg::Optional size) { + wgpu::MapMode md = static_cast(mode); + + // we can only map unmapped buffers + if (state_ != State::Unmapped) { + device_.InjectError( + wgpu::ErrorType::Validation, + "mapAsync called on buffer that is not in the unmapped state"); + JSG_FAIL_REQUIRE( + Error, "mapAsync called on buffer that is not in the unmapped state"); + } + + uint64_t o = offset.orDefault(0); + uint64_t s = size.orDefault(desc_.size - o); + + struct Context { + kj::Own> fulfiller; + State& state; + AsyncTask task; + }; + auto paf = kj::newPromiseAndFulfiller(); + // This context object will hold information for the callback, including the + // fullfiller to signal the caller with the result, and an async task that + // will ensure the device's Tick() function is called periodically. It will be + // deallocated at the end of the callback function. + auto ctx = new Context{kj::mv(paf.fulfiller), state_, + AsyncTask(kj::addRef(*async_))}; + + state_ = State::MappingPending; + + buffer_.MapAsync( + md, o, s, + [](WGPUBufferMapAsyncStatus status, void* userdata) { + // Note: this is invoked outside the JS isolate lock + auto c = std::unique_ptr(static_cast(userdata)); + c->state = State::Unmapped; + + JSG_REQUIRE(c->fulfiller->isWaiting(), TypeError, + "async operation has been canceled"); + + switch (status) { + case WGPUBufferMapAsyncStatus_Force32: + KJ_UNREACHABLE + case WGPUBufferMapAsyncStatus_Success: + c->fulfiller->fulfill(); + c->state = State::Mapped; + break; + case WGPUBufferMapAsyncStatus_OffsetOutOfRange: + case WGPUBufferMapAsyncStatus_SizeOutOfRange: + case WGPUBufferMapAsyncStatus_ValidationError: + c->fulfiller->reject(JSG_KJ_EXCEPTION( + FAILED, TypeError, "validation failed or out of range")); + break; + case WGPUBufferMapAsyncStatus_UnmappedBeforeCallback: + case WGPUBufferMapAsyncStatus_DestroyedBeforeCallback: + c->fulfiller->reject(JSG_KJ_EXCEPTION( + FAILED, TypeError, "unmapped or destroyed before callback")); + break; + case WGPUBufferMapAsyncStatus_Unknown: + case WGPUBufferMapAsyncStatus_DeviceLost: + c->fulfiller->reject(JSG_KJ_EXCEPTION( + FAILED, TypeError, "unknown error or device lost")); + break; + case WGPUBufferMapAsyncStatus_MappingAlreadyPending: + break; + } + }, + ctx); + + auto& context = IoContext::current(); + return context.awaitIo(kj::mv(paf.promise)); +} + } // namespace workerd::api::gpu diff --git a/src/workerd/api/gpu/gpu-buffer.h b/src/workerd/api/gpu/gpu-buffer.h index 97f3d57992f..e534b6f2c34 100644 --- a/src/workerd/api/gpu/gpu-buffer.h +++ b/src/workerd/api/gpu/gpu-buffer.h @@ -4,6 +4,7 @@ #pragma once +#include "gpu-async-runner.h" #include "gpu-utils.h" #include @@ -12,12 +13,15 @@ namespace workerd::api::gpu { class GPUBuffer : public jsg::Object { public: // Implicit cast operator to Dawn GPU object - inline operator const wgpu::Buffer &() const { return buffer_; } - explicit GPUBuffer(wgpu::Buffer, wgpu::BufferDescriptor); + inline operator const wgpu::Buffer&() const { return buffer_; } + explicit GPUBuffer(jsg::Lock& js, wgpu::Buffer, wgpu::BufferDescriptor, + wgpu::Device, kj::Own); JSG_RESOURCE_TYPE(GPUBuffer) { JSG_METHOD(getMappedRange); JSG_METHOD(unmap); + JSG_METHOD(destroy); + JSG_METHOD(mapAsync); } private: @@ -36,17 +40,26 @@ class GPUBuffer : public jsg::Object { inline bool Intersects(uint64_t s, uint64_t e) const { return s < end && e > start; } - kj::ArrayPtr buffer; + jsg::V8Ref buffer; }; wgpu::Buffer buffer_; + wgpu::Device device_; wgpu::BufferDescriptor desc_; + kj::Own async_; State state_ = State::Unmapped; kj::Vector mapped_; + jsg::V8Ref detachKey_; - kj::ArrayPtr getMappedRange(jsg::Optional offset, - jsg::Optional size); - void unmap(); + v8::Local getMappedRange(jsg::Lock&, + jsg::Optional offset, + jsg::Optional size); + void unmap(jsg::Lock& js); + void destroy(jsg::Lock& js); + jsg::Promise mapAsync(GPUFlagsConstant mode, + jsg::Optional offset, + jsg::Optional size); + void DetachMappings(jsg::Lock& js); }; struct GPUBufferDescriptor { diff --git a/src/workerd/api/gpu/gpu-command-buffer.h b/src/workerd/api/gpu/gpu-command-buffer.h new file mode 100644 index 00000000000..efcf3fe8e55 --- /dev/null +++ b/src/workerd/api/gpu/gpu-command-buffer.h @@ -0,0 +1,29 @@ +// Copyright (c) 2017-2022 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +#pragma once + +#include +#include + +namespace workerd::api::gpu { + +class GPUCommandBuffer : public jsg::Object { +public: + // Implicit cast operator to Dawn GPU object + inline operator const wgpu::CommandBuffer &() const { return cmd_buf_; } + explicit GPUCommandBuffer(wgpu::CommandBuffer b) : cmd_buf_(kj::mv(b)){}; + JSG_RESOURCE_TYPE(GPUCommandBuffer) {} + +private: + wgpu::CommandBuffer cmd_buf_; +}; + +struct GPUCommandBufferDescriptor { + jsg::Optional label; + + JSG_STRUCT(label); +}; + +} // namespace workerd::api::gpu diff --git a/src/workerd/api/gpu/gpu-command-encoder.c++ b/src/workerd/api/gpu/gpu-command-encoder.c++ index 87035ca3af4..08f8bdbb428 100644 --- a/src/workerd/api/gpu/gpu-command-encoder.c++ +++ b/src/workerd/api/gpu/gpu-command-encoder.c++ @@ -3,9 +3,34 @@ // https://opensource.org/licenses/Apache-2.0 #include "gpu-command-encoder.h" +#include "gpu-command-buffer.h" namespace workerd::api::gpu { +jsg::Ref GPUCommandEncoder::finish( + jsg::Optional descriptor) { + wgpu::CommandBufferDescriptor desc{}; + + KJ_IF_MAYBE (d, descriptor) { + KJ_IF_MAYBE (label, d->label) { + desc.label = label->cStr(); + } + } + + auto buffer = encoder_.Finish(&desc); + return jsg::alloc(kj::mv(buffer)); +}; + +void GPUCommandEncoder::copyBufferToBuffer(jsg::Ref source, + GPUSize64 sourceOffset, + jsg::Ref destination, + GPUSize64 destinationOffset, + GPUSize64 size) { + + encoder_.CopyBufferToBuffer(*source, sourceOffset, *destination, + destinationOffset, size); +}; + jsg::Ref GPUCommandEncoder::beginComputePass( jsg::Optional descriptor) { diff --git a/src/workerd/api/gpu/gpu-command-encoder.h b/src/workerd/api/gpu/gpu-command-encoder.h index f835cd0abba..4a0ed512561 100644 --- a/src/workerd/api/gpu/gpu-command-encoder.h +++ b/src/workerd/api/gpu/gpu-command-encoder.h @@ -4,6 +4,7 @@ #pragma once +#include "gpu-command-buffer.h" #include "gpu-compute-pass-encoder.h" #include #include @@ -13,12 +14,20 @@ namespace workerd::api::gpu { class GPUCommandEncoder : public jsg::Object { public: explicit GPUCommandEncoder(wgpu::CommandEncoder e) : encoder_(kj::mv(e)){}; - JSG_RESOURCE_TYPE(GPUCommandEncoder) { JSG_METHOD(beginComputePass); } + JSG_RESOURCE_TYPE(GPUCommandEncoder) { + JSG_METHOD(beginComputePass); + JSG_METHOD(copyBufferToBuffer); + JSG_METHOD(finish); + } private: wgpu::CommandEncoder encoder_; jsg::Ref beginComputePass(jsg::Optional descriptor); + jsg::Ref finish(jsg::Optional); + void copyBufferToBuffer(jsg::Ref source, GPUSize64 sourceOffset, + jsg::Ref destination, + GPUSize64 destinationOffset, GPUSize64 size); }; struct GPUCommandEncoderDescriptor { diff --git a/src/workerd/api/gpu/gpu-compute-pass-encoder.c++ b/src/workerd/api/gpu/gpu-compute-pass-encoder.c++ index 6637dc4403a..33a3fa58c0c 100644 --- a/src/workerd/api/gpu/gpu-compute-pass-encoder.c++ +++ b/src/workerd/api/gpu/gpu-compute-pass-encoder.c++ @@ -10,6 +10,18 @@ void GPUComputePassEncoder::setPipeline(jsg::Ref pipeline) { encoder_.SetPipeline(*pipeline); } +void GPUComputePassEncoder::dispatchWorkgroups( + GPUSize32 workgroupCountX, jsg::Optional workgroupCountY, + jsg::Optional workgroupCountZ) { + + GPUSize32 countY = workgroupCountY.orDefault(1); + GPUSize32 countZ = workgroupCountZ.orDefault(1); + + encoder_.DispatchWorkgroups(workgroupCountX, countY, countZ); +} + +void GPUComputePassEncoder::end() { encoder_.End(); } + void GPUComputePassEncoder::setBindGroup( GPUIndex32 index, kj::Maybe> bindGroup, jsg::Optional> dynamicOffsets) { @@ -40,7 +52,8 @@ parseComputePassTimestampLocation(kj::StringPtr location) { return wgpu::ComputePassTimestampLocation::End; } - KJ_FAIL_REQUIRE("unknown compute pass timestamp location", location); + JSG_FAIL_REQUIRE(TypeError, "unknown compute pass timestamp location", + location); } } // namespace workerd::api::gpu diff --git a/src/workerd/api/gpu/gpu-compute-pass-encoder.h b/src/workerd/api/gpu/gpu-compute-pass-encoder.h index abe102e7cee..5e5a4b7df08 100644 --- a/src/workerd/api/gpu/gpu-compute-pass-encoder.h +++ b/src/workerd/api/gpu/gpu-compute-pass-encoder.h @@ -20,11 +20,17 @@ class GPUComputePassEncoder : public jsg::Object { JSG_RESOURCE_TYPE(GPUComputePassEncoder) { JSG_METHOD(setPipeline); JSG_METHOD(setBindGroup); + JSG_METHOD(dispatchWorkgroups); + JSG_METHOD(end); } private: wgpu::ComputePassEncoder encoder_; void setPipeline(jsg::Ref pipeline); + void dispatchWorkgroups(GPUSize32 workgroupCountX, + jsg::Optional workgroupCountY, + jsg::Optional workgroupCountZ); + void end(); void setBindGroup(GPUIndex32 index, kj::Maybe> bindGroup, jsg::Optional> dynamicOffsets); diff --git a/src/workerd/api/gpu/gpu-compute-pipeline.c++ b/src/workerd/api/gpu/gpu-compute-pipeline.c++ new file mode 100644 index 00000000000..87fda7b1b01 --- /dev/null +++ b/src/workerd/api/gpu/gpu-compute-pipeline.c++ @@ -0,0 +1,13 @@ +// Copyright (c) 2017-2022 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +#include "gpu-compute-pipeline.h" + +namespace workerd::api::gpu { +jsg::Ref +GPUComputePipeline::getBindGroupLayout(uint32_t index) { + auto layout = pipeline_.GetBindGroupLayout(index); + return jsg::alloc(kj::mv(layout)); +} +} // namespace workerd::api::gpu diff --git a/src/workerd/api/gpu/gpu-compute-pipeline.h b/src/workerd/api/gpu/gpu-compute-pipeline.h index 19d3a243508..c6b5b5785c0 100644 --- a/src/workerd/api/gpu/gpu-compute-pipeline.h +++ b/src/workerd/api/gpu/gpu-compute-pipeline.h @@ -16,10 +16,11 @@ class GPUComputePipeline : public jsg::Object { // Implicit cast operator to Dawn GPU object inline operator const wgpu::ComputePipeline &() const { return pipeline_; } explicit GPUComputePipeline(wgpu::ComputePipeline p) : pipeline_(kj::mv(p)){}; - JSG_RESOURCE_TYPE(GPUComputePipeline) {} + JSG_RESOURCE_TYPE(GPUComputePipeline) { JSG_METHOD(getBindGroupLayout); } private: wgpu::ComputePipeline pipeline_; + jsg::Ref getBindGroupLayout(uint32_t index); }; using GPUPipelineConstantValue = double; @@ -32,7 +33,8 @@ struct GPUProgrammableStage { JSG_STRUCT(module, entryPoint, constants); }; -using GPUComputePipelineLayout = kj::OneOf>; +using GPUComputePipelineLayout = + kj::OneOf>; struct GPUComputePipelineDescriptor { jsg::Optional label; diff --git a/src/workerd/api/gpu/gpu-device.c++ b/src/workerd/api/gpu/gpu-device.c++ index a1a8801b242..5523bfa4aa7 100644 --- a/src/workerd/api/gpu/gpu-device.c++ +++ b/src/workerd/api/gpu/gpu-device.c++ @@ -7,13 +7,18 @@ #include "gpu-bindgroup.h" #include "gpu-buffer.h" #include "gpu-command-encoder.h" +#include "gpu-compute-pipeline.h" +#include "gpu-errors.h" +#include "gpu-query-set.h" +#include "gpu-queue.h" #include "gpu-sampler.h" #include "gpu-utils.h" #include "workerd/jsg/exception.h" +#include "workerd/jsg/jsg.h" namespace workerd::api::gpu { -jsg::Ref GPUDevice::createBuffer(jsg::Lock &, +jsg::Ref GPUDevice::createBuffer(jsg::Lock& js, GPUBufferDescriptor descriptor) { wgpu::BufferDescriptor desc{}; desc.label = descriptor.label.cStr(); @@ -21,7 +26,8 @@ jsg::Ref GPUDevice::createBuffer(jsg::Lock &, desc.size = descriptor.size; desc.usage = static_cast(descriptor.usage); auto buffer = device_.CreateBuffer(&desc); - return jsg::alloc(kj::mv(buffer), kj::mv(desc)); + return jsg::alloc(js, kj::mv(buffer), kj::mv(desc), device_, + kj::addRef(*async_)); } wgpu::CompareFunction parseCompareFunction(kj::StringPtr compare) { @@ -57,7 +63,7 @@ wgpu::CompareFunction parseCompareFunction(kj::StringPtr compare) { return wgpu::CompareFunction::Always; } - KJ_FAIL_REQUIRE("unknown compare function", compare); + JSG_FAIL_REQUIRE(TypeError, "unknown compare function", compare); } wgpu::AddressMode parseAddressMode(kj::StringPtr mode) { @@ -74,7 +80,7 @@ wgpu::AddressMode parseAddressMode(kj::StringPtr mode) { return wgpu::AddressMode::MirrorRepeat; } - KJ_FAIL_REQUIRE("unknown address mode", mode); + JSG_FAIL_REQUIRE(TypeError, "unknown address mode", mode); } wgpu::FilterMode parseFilterMode(kj::StringPtr mode) { @@ -87,7 +93,7 @@ wgpu::FilterMode parseFilterMode(kj::StringPtr mode) { return wgpu::FilterMode::Linear; } - KJ_FAIL_REQUIRE("unknown filter mode", mode); + JSG_FAIL_REQUIRE(TypeError, "unknown filter mode", mode); } wgpu::MipmapFilterMode parseMipmapFilterMode(kj::StringPtr mode) { @@ -100,63 +106,33 @@ wgpu::MipmapFilterMode parseMipmapFilterMode(kj::StringPtr mode) { return wgpu::MipmapFilterMode::Linear; } - KJ_FAIL_REQUIRE("unknown mipmap filter mode", mode); + JSG_FAIL_REQUIRE(TypeError, "unknown mipmap filter mode", mode); } jsg::Ref GPUDevice::createSampler(GPUSamplerDescriptor descriptor) { wgpu::SamplerDescriptor desc{}; - desc.addressModeU = wgpu::AddressMode::ClampToEdge; - desc.addressModeV = wgpu::AddressMode::ClampToEdge; - desc.addressModeW = wgpu::AddressMode::ClampToEdge; - desc.magFilter = wgpu::FilterMode::Nearest; - desc.minFilter = wgpu::FilterMode::Nearest; - desc.mipmapFilter = wgpu::MipmapFilterMode::Nearest; - desc.lodMinClamp = 0; - desc.lodMaxClamp = 32; + desc.addressModeU = parseAddressMode( + descriptor.addressModeU.orDefault([] { return "clamp-to-edge"_kj; })); + desc.addressModeV = parseAddressMode( + descriptor.addressModeV.orDefault([] { return "clamp-to-edge"_kj; })); + desc.addressModeW = parseAddressMode( + descriptor.addressModeW.orDefault([] { return "clamp-to-edge"_kj; })); + desc.magFilter = parseFilterMode( + descriptor.magFilter.orDefault([] { return "nearest"_kj; })); + desc.minFilter = parseFilterMode( + descriptor.minFilter.orDefault([] { return "nearest"_kj; })); + desc.mipmapFilter = parseMipmapFilterMode( + descriptor.mipmapFilter.orDefault([] { return "nearest"_kj; })); + desc.lodMinClamp = descriptor.lodMinClamp.orDefault(0); + desc.lodMaxClamp = descriptor.lodMaxClamp.orDefault(32); desc.compare = parseCompareFunction(descriptor.compare); - desc.maxAnisotropy = 1; + desc.maxAnisotropy = descriptor.maxAnisotropy.orDefault(1); KJ_IF_MAYBE (label, descriptor.label) { desc.label = label->cStr(); } - KJ_IF_MAYBE (mode, descriptor.addressModeU) { - desc.addressModeU = parseAddressMode(*mode); - } - - KJ_IF_MAYBE (mode, descriptor.addressModeV) { - desc.addressModeV = parseAddressMode(*mode); - } - - KJ_IF_MAYBE (mode, descriptor.addressModeW) { - desc.addressModeW = parseAddressMode(*mode); - } - - KJ_IF_MAYBE (mode, descriptor.magFilter) { - desc.magFilter = parseFilterMode(*mode); - } - - KJ_IF_MAYBE (mode, descriptor.minFilter) { - desc.minFilter = parseFilterMode(*mode); - } - - KJ_IF_MAYBE (mode, descriptor.mipmapFilter) { - desc.mipmapFilter = parseMipmapFilterMode(*mode); - } - - KJ_IF_MAYBE (clamp, descriptor.lodMinClamp) { - desc.lodMinClamp = *clamp; - } - - KJ_IF_MAYBE (clamp, descriptor.lodMaxClamp) { - desc.lodMaxClamp = *clamp; - } - - KJ_IF_MAYBE (anisotropy, descriptor.maxAnisotropy) { - desc.maxAnisotropy = *anisotropy; - } - auto sampler = device_.CreateSampler(&desc); return jsg::alloc(kj::mv(sampler)); } @@ -170,7 +146,7 @@ GPUDevice::createBindGroupLayout(GPUBindGroupLayoutDescriptor descriptor) { } kj::Vector layoutEntries; - for (auto &e : descriptor.entries) { + for (auto& e : descriptor.entries) { layoutEntries.add(parseBindGroupLayoutEntry(e)); } desc.entries = layoutEntries.begin(); @@ -191,7 +167,7 @@ GPUDevice::createBindGroup(GPUBindGroupDescriptor descriptor) { desc.layout = *descriptor.layout; kj::Vector bindGroupEntries; - for (auto &e : descriptor.entries) { + for (auto& e : descriptor.entries) { bindGroupEntries.add(parseBindGroupEntry(e)); } @@ -227,7 +203,7 @@ GPUDevice::createPipelineLayout(GPUPipelineLayoutDescriptor descriptor) { } kj::Vector bindGroupLayouts; - for (auto &l : descriptor.bindGroupLayouts) { + for (auto& l : descriptor.bindGroupLayouts) { bindGroupLayouts.add(*l); } @@ -252,8 +228,8 @@ jsg::Ref GPUDevice::createCommandEncoder( return jsg::alloc(kj::mv(encoder)); } -jsg::Ref -GPUDevice::createComputePipeline(GPUComputePipelineDescriptor descriptor) { +wgpu::ComputePipelineDescriptor +parseComputePipelineDescriptor(GPUComputePipelineDescriptor& descriptor) { wgpu::ComputePipelineDescriptor desc{}; KJ_IF_MAYBE (label, descriptor.label) { @@ -265,7 +241,7 @@ GPUDevice::createComputePipeline(GPUComputePipelineDescriptor descriptor) { kj::Vector constants; KJ_IF_MAYBE (cDict, descriptor.compute.constants) { - for (auto &f : cDict->fields) { + for (auto& f : cDict->fields) { wgpu::ConstantEntry e; e.key = f.name.cStr(); e.value = f.value; @@ -287,8 +263,211 @@ GPUDevice::createComputePipeline(GPUComputePipelineDescriptor descriptor) { } } + return kj::mv(desc); +} + +jsg::Ref +GPUDevice::createComputePipeline(GPUComputePipelineDescriptor descriptor) { + wgpu::ComputePipelineDescriptor desc = + parseComputePipelineDescriptor(descriptor); auto pipeline = device_.CreateComputePipeline(&desc); return jsg::alloc(kj::mv(pipeline)); } +jsg::Promise>> GPUDevice::popErrorScope() { + struct Context { + kj::Own>>> fulfiller; + AsyncTask task; + }; + + auto paf = kj::newPromiseAndFulfiller>>(); + // This context object will hold information for the callback, including the + // fullfiller to signal the caller with the result, and an async task that + // will ensure the device's Tick() function is called periodically. It will be + // deallocated at the end of the callback function. + auto ctx = new Context{kj::mv(paf.fulfiller), AsyncTask(kj::addRef(*async_))}; + + device_.PopErrorScope( + [](WGPUErrorType type, char const* message, void* userdata) { + auto c = std::unique_ptr(static_cast(userdata)); + switch (type) { + case WGPUErrorType::WGPUErrorType_NoError: + c->fulfiller->fulfill(nullptr); + break; + case WGPUErrorType::WGPUErrorType_OutOfMemory: { + jsg::Ref err = jsg::alloc(kj::str(message)); + c->fulfiller->fulfill(kj::mv(err)); + break; + } + case WGPUErrorType::WGPUErrorType_Validation: { + jsg::Ref err = + jsg::alloc(kj::str(message)); + c->fulfiller->fulfill(kj::mv(err)); + break; + } + case WGPUErrorType::WGPUErrorType_Unknown: + case WGPUErrorType::WGPUErrorType_DeviceLost: + c->fulfiller->reject(JSG_KJ_EXCEPTION(FAILED, TypeError, message)); + break; + default: + c->fulfiller->reject( + JSG_KJ_EXCEPTION(FAILED, TypeError, "unhandled error type")); + break; + } + }, + ctx); + + auto& context = IoContext::current(); + return context.awaitIo(kj::mv(paf.promise)); +} + +jsg::Promise> +GPUDevice::createComputePipelineAsync(GPUComputePipelineDescriptor descriptor) { + wgpu::ComputePipelineDescriptor desc = + parseComputePipelineDescriptor(descriptor); + + struct Context { + kj::Own>> fulfiller; + AsyncTask task; + }; + auto paf = kj::newPromiseAndFulfiller>(); + // This context object will hold information for the callback, including the + // fullfiller to signal the caller with the result, and an async task that + // will ensure the device's Tick() function is called periodically. It will be + // deallocated at the end of the callback function. + auto ctx = new Context{kj::mv(paf.fulfiller), AsyncTask(kj::addRef(*async_))}; + + device_.CreateComputePipelineAsync( + &desc, + [](WGPUCreatePipelineAsyncStatus status, WGPUComputePipeline pipeline, + char const* message, void* userdata) { + // Note: this is invoked outside the JS isolate lock + auto c = std::unique_ptr(static_cast(userdata)); + + switch (status) { + case WGPUCreatePipelineAsyncStatus:: + WGPUCreatePipelineAsyncStatus_Success: + c->fulfiller->fulfill( + jsg::alloc(kj::mv(pipeline))); + break; + default: + c->fulfiller->reject( + JSG_KJ_EXCEPTION(FAILED, TypeError, "unknown error")); + break; + } + }, + ctx); + + auto& context = IoContext::current(); + return context.awaitIo(kj::mv(paf.promise)); +} + +jsg::Ref GPUDevice::getQueue() { + auto queue = device_.GetQueue(); + return jsg::alloc(kj::mv(queue)); +} + +GPUDevice::~GPUDevice() { + if (!destroyed_) { + device_.Destroy(); + destroyed_ = true; + } +} + +void GPUDevice::destroy() { + if (lost_promise_fulfiller_->isWaiting()) { + auto lostInfo = jsg::alloc( + kj::str("destroyed"), kj::str("device was destroyed")); + lost_promise_fulfiller_->fulfill(kj::mv(lostInfo)); + } + + device_.Destroy(); + destroyed_ = true; +} + +jsg::MemoizedIdentity>>& +GPUDevice::getLost() { + return lost_promise_; +} + +kj::String parseDeviceLostReason(WGPUDeviceLostReason reason) { + switch (reason) { + case WGPUDeviceLostReason_Force32: + KJ_UNREACHABLE + case WGPUDeviceLostReason_Destroyed: + return kj::str("destroyed"); + case WGPUDeviceLostReason_Undefined: + return kj::str("undefined"); + } +} + +GPUDevice::GPUDevice(jsg::Lock& js, wgpu::Device d) + : device_(d), lost_promise_(nullptr), + async_(kj::refcounted(d)) { + auto& context = IoContext::current(); + auto paf = kj::newPromiseAndFulfiller>(); + lost_promise_fulfiller_ = kj::mv(paf.fulfiller); + lost_promise_ = context.awaitIo(js, kj::mv(paf.promise)); + + device_.SetLoggingCallback( + [](WGPULoggingType type, char const* message, void* userdata) { + KJ_LOG(INFO, "WebGPU logging", kj::str(type), message); + }, + nullptr); + device_.SetUncapturedErrorCallback( + [](WGPUErrorType type, char const* message, void* userdata) { + KJ_LOG(INFO, "WebGPU uncaptured error", kj::str(type), message); + }, + nullptr); + + device_.SetDeviceLostCallback( + [](WGPUDeviceLostReason reason, char const* message, void* userdata) { + auto r = parseDeviceLostReason(reason); + auto* self = static_cast(userdata); + if (self->lost_promise_fulfiller_->isWaiting()) { + auto lostInfo = + jsg::alloc(kj::mv(r), kj::str(message)); + self->lost_promise_fulfiller_->fulfill(kj::mv(lostInfo)); + } + }, + this); +}; + +jsg::Ref +GPUDevice::createQuerySet(GPUQuerySetDescriptor descriptor) { + wgpu::QuerySetDescriptor desc{}; + + KJ_IF_MAYBE (label, descriptor.label) { + desc.label = label->cStr(); + } + + desc.count = descriptor.count; + desc.type = parseQueryType(descriptor.type); + + auto querySet = device_.CreateQuerySet(&desc); + return jsg::alloc(kj::mv(querySet)); +} + +wgpu::ErrorFilter parseErrorFilter(GPUErrorFilter& filter) { + + if (filter == "validation") { + return wgpu::ErrorFilter::Validation; + } + + if (filter == "out-of-memory") { + return wgpu::ErrorFilter::OutOfMemory; + } + + if (filter == "internal") { + return wgpu::ErrorFilter::Internal; + } + + JSG_FAIL_REQUIRE(TypeError, "unknown error filter", filter); +} + +void GPUDevice::pushErrorScope(GPUErrorFilter filter) { + wgpu::ErrorFilter f = parseErrorFilter(filter); + device_.PushErrorScope(f); +} + } // namespace workerd::api::gpu diff --git a/src/workerd/api/gpu/gpu-device.h b/src/workerd/api/gpu/gpu-device.h index 1973836ffb5..d2c5a94b171 100644 --- a/src/workerd/api/gpu/gpu-device.h +++ b/src/workerd/api/gpu/gpu-device.h @@ -4,14 +4,19 @@ #pragma once +#include "gpu-async-runner.h" #include "gpu-bindgroup-layout.h" #include "gpu-bindgroup.h" #include "gpu-buffer.h" #include "gpu-command-encoder.h" #include "gpu-compute-pipeline.h" +#include "gpu-errors.h" #include "gpu-pipeline-layout.h" +#include "gpu-query-set.h" +#include "gpu-queue.h" #include "gpu-sampler.h" #include "gpu-shader-module.h" +#include "workerd/jsg/promise.h" #include #include #include @@ -20,7 +25,8 @@ namespace workerd::api::gpu { class GPUDevice : public jsg::Object { public: - explicit GPUDevice(wgpu::Device d) : device_(d){}; + explicit GPUDevice(jsg::Lock& js, wgpu::Device d); + ~GPUDevice(); JSG_RESOURCE_TYPE(GPUDevice) { JSG_METHOD(createBuffer); JSG_METHOD(createBindGroupLayout); @@ -30,11 +36,23 @@ class GPUDevice : public jsg::Object { JSG_METHOD(createPipelineLayout); JSG_METHOD(createComputePipeline); JSG_METHOD(createCommandEncoder); + JSG_METHOD(destroy); + JSG_METHOD(createQuerySet); + JSG_METHOD(pushErrorScope); + JSG_METHOD(popErrorScope); + JSG_READONLY_PROTOTYPE_PROPERTY(queue, getQueue); + JSG_READONLY_PROTOTYPE_PROPERTY(lost, getLost); } private: wgpu::Device device_; - jsg::Ref createBuffer(jsg::Lock &, GPUBufferDescriptor); + jsg::MemoizedIdentity>> + lost_promise_; + kj::Own>> + lost_promise_fulfiller_; + kj::Own async_; + bool destroyed_ = false; + jsg::Ref createBuffer(jsg::Lock&, GPUBufferDescriptor); jsg::Ref createBindGroupLayout(GPUBindGroupLayoutDescriptor descriptor); jsg::Ref createBindGroup(GPUBindGroupDescriptor descriptor); @@ -45,8 +63,16 @@ class GPUDevice : public jsg::Object { createPipelineLayout(GPUPipelineLayoutDescriptor descriptor); jsg::Ref createComputePipeline(GPUComputePipelineDescriptor descriptor); + jsg::Promise> + createComputePipelineAsync(GPUComputePipelineDescriptor descriptor); jsg::Ref createCommandEncoder(jsg::Optional descriptor); + jsg::Ref getQueue(); + void destroy(); + jsg::Ref createQuerySet(GPUQuerySetDescriptor descriptor); + void pushErrorScope(GPUErrorFilter filter); + jsg::Promise>> popErrorScope(); + jsg::MemoizedIdentity>>& getLost(); }; struct GPUQueueDescriptor { @@ -58,11 +84,10 @@ struct GPUQueueDescriptor { struct GPUDeviceDescriptor { jsg::Optional label; jsg::Optional> requiredFeatures; - // jsg::Optional> requiredLimits; - // jsg::Optional defaultQueue; + jsg::Optional> requiredLimits; + jsg::Optional defaultQueue; - // JSG_STRUCT(label, requiredFeatures, requiredLimits, defaultQueue); - JSG_STRUCT(label, requiredFeatures); + JSG_STRUCT(label, requiredFeatures, requiredLimits, defaultQueue); }; } // namespace workerd::api::gpu diff --git a/src/workerd/api/gpu/gpu-errors.h b/src/workerd/api/gpu/gpu-errors.h new file mode 100644 index 00000000000..ac34657c80c --- /dev/null +++ b/src/workerd/api/gpu/gpu-errors.h @@ -0,0 +1,53 @@ +// Copyright (c) 2017-2022 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +#pragma once + +#include "gpu-utils.h" +#include +#include + +namespace workerd::api::gpu { + +class GPUError : public jsg::Object { +public: + explicit GPUError(kj::String m) : message_(kj::mv(m)){}; + JSG_RESOURCE_TYPE(GPUError) { + JSG_READONLY_PROTOTYPE_PROPERTY(message, getMessage); + } + +private: + kj::String message_; + kj::StringPtr getMessage() { return message_; } +}; + +class GPUOOMError : public GPUError { +public: + using GPUError::GPUError; + JSG_RESOURCE_TYPE(GPUOOMError) { JSG_INHERIT(GPUError); } +}; + +class GPUValidationError : public GPUError { +public: + using GPUError::GPUError; + JSG_RESOURCE_TYPE(GPUValidationError) { JSG_INHERIT(GPUError); } +}; + +class GPUDeviceLostInfo : public jsg::Object { +public: + explicit GPUDeviceLostInfo(GPUDeviceLostReason r, kj::String m) + : reason_(kj::mv(r)), message_(kj::mv(m)){}; + JSG_RESOURCE_TYPE(GPUDeviceLostInfo) { + JSG_READONLY_PROTOTYPE_PROPERTY(message, getMessage); + JSG_READONLY_PROTOTYPE_PROPERTY(reason, getReason); + } + +private: + GPUDeviceLostReason reason_; + kj::String message_; + kj::StringPtr getMessage() { return message_; } + kj::StringPtr getReason() { return reason_; } +}; + +} // namespace workerd::api::gpu diff --git a/src/workerd/api/gpu/gpu-query-set.c++ b/src/workerd/api/gpu/gpu-query-set.c++ new file mode 100644 index 00000000000..d43b7610938 --- /dev/null +++ b/src/workerd/api/gpu/gpu-query-set.c++ @@ -0,0 +1,22 @@ +// Copyright (c) 2017-2022 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +#include "gpu-query-set.h" +#include "workerd/jsg/exception.h" + +namespace workerd::api::gpu { + +wgpu::QueryType parseQueryType(kj::StringPtr type) { + if (type == "occlusion") { + return wgpu::QueryType::Occlusion; + } + + if (type == "timestamp") { + return wgpu::QueryType::Timestamp; + } + + JSG_FAIL_REQUIRE(TypeError, "unknown Query type", type); +} + +} // namespace workerd::api::gpu diff --git a/src/workerd/api/gpu/gpu-query-set.h b/src/workerd/api/gpu/gpu-query-set.h index 34cf9776483..34e48f35215 100644 --- a/src/workerd/api/gpu/gpu-query-set.h +++ b/src/workerd/api/gpu/gpu-query-set.h @@ -30,4 +30,6 @@ struct GPUQuerySetDescriptor { JSG_STRUCT(label); }; +wgpu::QueryType parseQueryType(kj::StringPtr type); + } // namespace workerd::api::gpu diff --git a/src/workerd/api/gpu/gpu-queue.c++ b/src/workerd/api/gpu/gpu-queue.c++ new file mode 100644 index 00000000000..07d4fa27ee1 --- /dev/null +++ b/src/workerd/api/gpu/gpu-queue.c++ @@ -0,0 +1,53 @@ +// Copyright (c) 2017-2022 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +#include "gpu-queue.h" +#include "workerd/jsg/exception.h" + +namespace workerd::api::gpu { + +void GPUQueue::submit(kj::Array> commandBuffers) { + kj::Vector bufs(commandBuffers.size()); + for (auto &cb : commandBuffers) { + bufs.add(*cb); + } + + queue_.Submit(bufs.size(), bufs.begin()); +} + +void GPUQueue::writeBuffer(jsg::Ref buffer, GPUSize64 bufferOffset, + jsg::BufferSource data, + jsg::Optional dataOffsetElements, + jsg::Optional sizeElements) { + wgpu::Buffer buf = *buffer; + + uint64_t dataOffset = 0; + KJ_IF_MAYBE (offset, dataOffsetElements) { + // In the JS semantics of WebGPU, writeBuffer works in number of + // elements of the typed arrays. + dataOffset = *offset * data.getElementSize(); + JSG_REQUIRE(dataOffset <= data.size(), TypeError, + "dataOffset is larger than data's size."); + } + + auto dataPtr = + reinterpret_cast(data.asArrayPtr().front()) + dataOffset; + size_t dataSize = data.size() - dataOffset; + KJ_IF_MAYBE (size, sizeElements) { + JSG_REQUIRE(*size <= std::numeric_limits::max() / + data.getElementSize(), + TypeError, "size overflows."); + dataSize = *size * data.getElementSize(); + JSG_REQUIRE(dataOffset + dataSize < data.size(), TypeError, + "size + dataOffset is larger than data's size."); + + JSG_REQUIRE(dataSize % 4 == 0, TypeError, + "size is not a multiple of 4 bytes."); + } + + KJ_ASSERT(dataSize <= std::numeric_limits::max()); + queue_.WriteBuffer(buf, bufferOffset, dataPtr, static_cast(dataSize)); +} + +} // namespace workerd::api::gpu diff --git a/src/workerd/api/gpu/gpu-queue.h b/src/workerd/api/gpu/gpu-queue.h new file mode 100644 index 00000000000..30489de1d7c --- /dev/null +++ b/src/workerd/api/gpu/gpu-queue.h @@ -0,0 +1,33 @@ +// Copyright (c) 2017-2022 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +#pragma once + +#include "gpu-buffer.h" +#include "gpu-command-buffer.h" +#include +#include +#include + +namespace workerd::api::gpu { + +class GPUQueue : public jsg::Object { +public: + // Implicit cast operator to Dawn GPU object + inline operator const wgpu::Queue &() const { return queue_; } + explicit GPUQueue(wgpu::Queue q) : queue_(kj::mv(q)){}; + JSG_RESOURCE_TYPE(GPUQueue) { + JSG_METHOD(submit); + JSG_METHOD(writeBuffer); + } + +private: + wgpu::Queue queue_; + void submit(kj::Array> commandBuffers); + void writeBuffer(jsg::Ref buffer, GPUSize64 bufferOffset, + jsg::BufferSource data, jsg::Optional dataOffset, + jsg::Optional size); +}; + +} // namespace workerd::api::gpu diff --git a/src/workerd/api/gpu/gpu-sampler.h b/src/workerd/api/gpu/gpu-sampler.h index 86d495bc082..832e56065e4 100644 --- a/src/workerd/api/gpu/gpu-sampler.h +++ b/src/workerd/api/gpu/gpu-sampler.h @@ -15,7 +15,7 @@ class GPUSampler : public jsg::Object { // Implicit cast operator to Dawn GPU object inline operator const wgpu::Sampler &() const { return sampler_; } - explicit GPUSampler(wgpu::Sampler s) : sampler_(s){}; + explicit GPUSampler(wgpu::Sampler s) : sampler_(kj::mv(s)){}; JSG_RESOURCE_TYPE(GPUSampler) {} private: diff --git a/src/workerd/api/gpu/gpu-supported-features.c++ b/src/workerd/api/gpu/gpu-supported-features.c++ new file mode 100644 index 00000000000..d2d6a8aebe9 --- /dev/null +++ b/src/workerd/api/gpu/gpu-supported-features.c++ @@ -0,0 +1,29 @@ +// Copyright (c) 2017-2022 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +#include "gpu-supported-features.h" + +namespace workerd::api::gpu { + +GPUSupportedFeatures::GPUSupportedFeatures( + kj::Array features) { + for (wgpu::FeatureName feature : features) { + // add only known features to the feature list + KJ_IF_MAYBE (knownF, getFeatureName(feature)) { + enabled_.insert(kj::mv(*knownF)); + } + } +} + +bool GPUSupportedFeatures::has(kj::String name) { + return enabled_.contains(name); +} + +kj::Array GPUSupportedFeatures::keys() { + kj::Vector res(enabled_.size()); + res.addAll(enabled_); + return res.releaseAsArray(); +} + +} // namespace workerd::api::gpu diff --git a/src/workerd/api/gpu/gpu-supported-features.h b/src/workerd/api/gpu/gpu-supported-features.h new file mode 100644 index 00000000000..76fc8c746f6 --- /dev/null +++ b/src/workerd/api/gpu/gpu-supported-features.h @@ -0,0 +1,27 @@ +// Copyright (c) 2017-2022 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +#pragma once + +#include "gpu-utils.h" +#include +#include + +namespace workerd::api::gpu { + +class GPUSupportedFeatures : public jsg::Object { +public: + explicit GPUSupportedFeatures(kj::Array features); + JSG_RESOURCE_TYPE(GPUSupportedFeatures) { + JSG_METHOD(has); + JSG_METHOD(keys); + } + +private: + kj::HashSet enabled_; + bool has(kj::String name); + kj::Array keys(); +}; + +} // namespace workerd::api::gpu diff --git a/src/workerd/api/gpu/gpu-supported-limits.h b/src/workerd/api/gpu/gpu-supported-limits.h new file mode 100644 index 00000000000..0bf95ea65a5 --- /dev/null +++ b/src/workerd/api/gpu/gpu-supported-limits.h @@ -0,0 +1,202 @@ +// Copyright (c) 2017-2022 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +#pragma once + +#include "gpu-utils.h" +#include +#include + +namespace workerd::api::gpu { + +class GPUSupportedLimits : public jsg::Object { +public: + explicit GPUSupportedLimits(wgpu::SupportedLimits limits) + : limits_(kj::mv(limits)){}; + JSG_RESOURCE_TYPE(GPUSupportedLimits) { + JSG_READONLY_PROTOTYPE_PROPERTY(maxTextureDimension1D, + getMaxTextureDimension1D); + JSG_READONLY_PROTOTYPE_PROPERTY(maxTextureDimension2D, + getMaxTextureDimension2D); + JSG_READONLY_PROTOTYPE_PROPERTY(maxTextureDimension3D, + getMaxTextureDimension3D); + JSG_READONLY_PROTOTYPE_PROPERTY(maxTextureArrayLayers, + getMaxTextureArrayLayers); + JSG_READONLY_PROTOTYPE_PROPERTY(maxBindGroups, getMaxBindGroups); + JSG_READONLY_PROTOTYPE_PROPERTY(maxBindingsPerBindGroup, + getMaxBindingsPerBindGroup); + JSG_READONLY_PROTOTYPE_PROPERTY( + maxDynamicUniformBuffersPerPipelineLayout, + getMaxDynamicUniformBuffersPerPipelineLayout); + JSG_READONLY_PROTOTYPE_PROPERTY( + maxDynamicStorageBuffersPerPipelineLayout, + getMaxDynamicStorageBuffersPerPipelineLayout); + JSG_READONLY_PROTOTYPE_PROPERTY(maxSampledTexturesPerShaderStage, + getMaxSampledTexturesPerShaderStage); + JSG_READONLY_PROTOTYPE_PROPERTY(maxSamplersPerShaderStage, + getMaxSamplersPerShaderStage); + JSG_READONLY_PROTOTYPE_PROPERTY(maxStorageBuffersPerShaderStage, + getMaxStorageBuffersPerShaderStage); + JSG_READONLY_PROTOTYPE_PROPERTY(maxStorageTexturesPerShaderStage, + getMaxStorageTexturesPerShaderStage); + JSG_READONLY_PROTOTYPE_PROPERTY(maxUniformBuffersPerShaderStage, + getMaxUniformBuffersPerShaderStage); + JSG_READONLY_PROTOTYPE_PROPERTY(maxUniformBufferBindingSize, + getMaxUniformBufferBindingSize); + JSG_READONLY_PROTOTYPE_PROPERTY(maxStorageBufferBindingSize, + getMaxStorageBufferBindingSize); + JSG_READONLY_PROTOTYPE_PROPERTY(minUniformBufferOffsetAlignment, + getMinUniformBufferOffsetAlignment); + JSG_READONLY_PROTOTYPE_PROPERTY(minStorageBufferOffsetAlignment, + getMinStorageBufferOffsetAlignment); + JSG_READONLY_PROTOTYPE_PROPERTY(maxVertexBuffers, getMaxVertexBuffers); + JSG_READONLY_PROTOTYPE_PROPERTY(maxBufferSize, getMaxBufferSize); + JSG_READONLY_PROTOTYPE_PROPERTY(maxVertexAttributes, + getMaxVertexAttributes); + JSG_READONLY_PROTOTYPE_PROPERTY(maxVertexBufferArrayStride, + getMaxVertexBufferArrayStride); + JSG_READONLY_PROTOTYPE_PROPERTY(maxInterStageShaderComponents, + getMaxInterStageShaderComponents); + JSG_READONLY_PROTOTYPE_PROPERTY(maxInterStageShaderVariables, + getMaxInterStageShaderVariables); + JSG_READONLY_PROTOTYPE_PROPERTY(maxColorAttachments, + getMaxColorAttachments); + JSG_READONLY_PROTOTYPE_PROPERTY(maxColorAttachmentBytesPerSample, + getMaxColorAttachmentBytesPerSample); + JSG_READONLY_PROTOTYPE_PROPERTY(maxComputeWorkgroupStorageSize, + getMaxComputeWorkgroupStorageSize); + JSG_READONLY_PROTOTYPE_PROPERTY(maxComputeInvocationsPerWorkgroup, + getMaxComputeInvocationsPerWorkgroup); + JSG_READONLY_PROTOTYPE_PROPERTY(maxComputeWorkgroupSizeX, + getMaxComputeWorkgroupSizeX); + JSG_READONLY_PROTOTYPE_PROPERTY(maxComputeWorkgroupSizeY, + getMaxComputeWorkgroupSizeY); + JSG_READONLY_PROTOTYPE_PROPERTY(maxComputeWorkgroupSizeZ, + getMaxComputeWorkgroupSizeZ); + JSG_READONLY_PROTOTYPE_PROPERTY(maxComputeWorkgroupsPerDimension, + getMaxComputeWorkgroupsPerDimension); + } + +private: + wgpu::SupportedLimits limits_; + uint32_t getMaxTextureDimension1D() { + return limits_.limits.maxTextureDimension1D; + } + + uint32_t getMaxTextureDimension2D() { + return limits_.limits.maxTextureDimension2D; + } + + uint32_t getMaxTextureDimension3D() { + return limits_.limits.maxTextureDimension3D; + } + + uint32_t getMaxTextureArrayLayers() { + return limits_.limits.maxTextureArrayLayers; + } + + uint32_t getMaxBindGroups() { return limits_.limits.maxBindGroups; } + + uint32_t getMaxBindingsPerBindGroup() { + return limits_.limits.maxBindingsPerBindGroup; + } + + uint32_t getMaxDynamicUniformBuffersPerPipelineLayout() { + return limits_.limits.maxDynamicUniformBuffersPerPipelineLayout; + } + + uint32_t getMaxDynamicStorageBuffersPerPipelineLayout() { + return limits_.limits.maxDynamicStorageBuffersPerPipelineLayout; + } + + uint32_t getMaxSampledTexturesPerShaderStage() { + return limits_.limits.maxSampledTexturesPerShaderStage; + } + + uint32_t getMaxSamplersPerShaderStage() { + return limits_.limits.maxSamplersPerShaderStage; + } + + uint32_t getMaxStorageBuffersPerShaderStage() { + return limits_.limits.maxStorageBuffersPerShaderStage; + } + + uint32_t getMaxStorageTexturesPerShaderStage() { + return limits_.limits.maxStorageTexturesPerShaderStage; + } + + uint32_t getMaxUniformBuffersPerShaderStage() { + return limits_.limits.maxUniformBuffersPerShaderStage; + } + + uint64_t getMaxUniformBufferBindingSize() { + return limits_.limits.maxUniformBufferBindingSize; + } + + uint64_t getMaxStorageBufferBindingSize() { + return limits_.limits.maxStorageBufferBindingSize; + } + + uint32_t getMinUniformBufferOffsetAlignment() { + return limits_.limits.minUniformBufferOffsetAlignment; + } + + uint32_t getMinStorageBufferOffsetAlignment() { + return limits_.limits.minStorageBufferOffsetAlignment; + } + + uint32_t getMaxVertexBuffers() { return limits_.limits.maxVertexBuffers; } + + uint64_t getMaxBufferSize() { return limits_.limits.maxBufferSize; } + + uint32_t getMaxVertexAttributes() { + return limits_.limits.maxVertexAttributes; + } + + uint32_t getMaxVertexBufferArrayStride() { + return limits_.limits.maxVertexBufferArrayStride; + } + + uint32_t getMaxInterStageShaderComponents() { + return limits_.limits.maxInterStageShaderComponents; + } + + uint32_t getMaxInterStageShaderVariables() { + return limits_.limits.maxInterStageShaderVariables; + } + + uint32_t getMaxColorAttachments() { + return limits_.limits.maxColorAttachments; + } + + uint32_t getMaxColorAttachmentBytesPerSample() { + return limits_.limits.maxColorAttachmentBytesPerSample; + } + + uint32_t getMaxComputeWorkgroupStorageSize() { + return limits_.limits.maxComputeWorkgroupStorageSize; + } + + uint32_t getMaxComputeInvocationsPerWorkgroup() { + return limits_.limits.maxComputeInvocationsPerWorkgroup; + } + + uint32_t getMaxComputeWorkgroupSizeX() { + return limits_.limits.maxComputeWorkgroupSizeX; + } + + uint32_t getMaxComputeWorkgroupSizeY() { + return limits_.limits.maxComputeWorkgroupSizeY; + } + + uint32_t getMaxComputeWorkgroupSizeZ() { + return limits_.limits.maxComputeWorkgroupSizeZ; + } + + uint32_t getMaxComputeWorkgroupsPerDimension() { + return limits_.limits.maxComputeWorkgroupsPerDimension; + } +}; + +} // namespace workerd::api::gpu diff --git a/src/workerd/api/gpu/gpu-utils.c++ b/src/workerd/api/gpu/gpu-utils.c++ index e8233be17e4..0ae00c738d9 100644 --- a/src/workerd/api/gpu/gpu-utils.c++ +++ b/src/workerd/api/gpu/gpu-utils.c++ @@ -6,7 +6,7 @@ namespace workerd::api::gpu { -wgpu::FeatureName parseFeatureName(GPUFeatureName &str) { +wgpu::FeatureName parseFeatureName(GPUFeatureName& str) { if (str == "depth-clip-control") { return wgpu::FeatureName::DepthClipControl; } @@ -41,7 +41,38 @@ wgpu::FeatureName parseFeatureName(GPUFeatureName &str) { return wgpu::FeatureName::Float32Filterable; } - KJ_FAIL_REQUIRE("unknown GPU feature", str); + JSG_FAIL_REQUIRE(TypeError, "unknown GPU feature", str); +} + +kj::Maybe getFeatureName(wgpu::FeatureName& feature) { + switch (feature) { + case wgpu::FeatureName::DepthClipControl: + return kj::str("depth-clip-control"); + case wgpu::FeatureName::Depth32FloatStencil8: + return kj::str("depth32float-stencil8"); + case wgpu::FeatureName::TextureCompressionBC: + return kj::str("texture-compression-bc"); + case wgpu::FeatureName::TextureCompressionETC2: + return kj::str("texture-compression-etc2"); + case wgpu::FeatureName::TextureCompressionASTC: + return kj::str("texture-compression-astc"); + case wgpu::FeatureName::TimestampQuery: + return kj::str("timestamp-query"); + case wgpu::FeatureName::IndirectFirstInstance: + return kj::str("indirect-first-instance"); + case wgpu::FeatureName::ShaderF16: + return kj::str("shader-f16"); + case wgpu::FeatureName::RG11B10UfloatRenderable: + return kj::str("rg11b10ufloat-renderable"); + case wgpu::FeatureName::BGRA8UnormStorage: + return kj::str("bgra8unorm-storage"); + case wgpu::FeatureName::Float32Filterable: + return kj::str("float32-filterable"); + default: + break; + } + + return nullptr; } } // namespace workerd::api::gpu diff --git a/src/workerd/api/gpu/gpu-utils.h b/src/workerd/api/gpu/gpu-utils.h index eaea9e25865..9f741ac99f4 100644 --- a/src/workerd/api/gpu/gpu-utils.h +++ b/src/workerd/api/gpu/gpu-utils.h @@ -27,6 +27,19 @@ using GPUMipmapFilterMode = kj::String; using GPUCompareFunction = kj::String; using GPUSize32 = uint32_t; using GPUBufferDynamicOffset = uint32_t; +using GPUPowerPreference = kj::String; +using GPUErrorFilter = kj::String; +using GPUDeviceLostReason = kj::String; + +struct GPUMapMode : public jsg::Object { + static constexpr GPUFlagsConstant READ = 0x0001; + static constexpr GPUFlagsConstant WRITE = 0x0002; + + JSG_RESOURCE_TYPE(GPUMapMode) { + JSG_STATIC_CONSTANT(READ); + JSG_STATIC_CONSTANT(WRITE); + } +}; struct GPUShaderStage : public jsg::Object { static constexpr GPUFlagsConstant VERTEX = 0x1; @@ -67,5 +80,6 @@ struct GPUBufferUsage : public jsg::Object { }; wgpu::FeatureName parseFeatureName(GPUFeatureName &); +kj::Maybe getFeatureName(wgpu::FeatureName &feature); } // namespace workerd::api::gpu diff --git a/src/workerd/api/gpu/gpu.c++ b/src/workerd/api/gpu/gpu.c++ index c17a7af76d4..7a009bad860 100644 --- a/src/workerd/api/gpu/gpu.c++ +++ b/src/workerd/api/gpu/gpu.c++ @@ -23,9 +23,22 @@ void initialize() { GPU::GPU() { instance_.DiscoverDefaultAdapters(); } -// TODO(soon): support options parameter +kj::String parseAdapterType(wgpu::AdapterType type) { + switch (type) { + case wgpu::AdapterType::DiscreteGPU: + return kj::str("Discrete GPU"); + case wgpu::AdapterType::IntegratedGPU: + return kj::str("Integrated GPU"); + case wgpu::AdapterType::CPU: + return kj::str("CPU"); + case wgpu::AdapterType::Unknown: + return kj::str("Unknown"); + } +} + jsg::Promise>> -GPU::requestAdapter(jsg::Lock &js) { +GPU::requestAdapter(jsg::Lock& js, + jsg::Optional options) { #if defined(_WIN32) constexpr auto defaultBackendType = wgpu::BackendType::D3D12; @@ -39,17 +52,20 @@ GPU::requestAdapter(jsg::Lock &js) { auto adapters = instance_.GetAdapters(); if (adapters.empty()) { - return nullptr; + KJ_LOG(WARNING, "no webgpu adapters found"); + return js.resolvedPromise(kj::Maybe>(nullptr)); } kj::Maybe adapter = nullptr; - for (auto &a : adapters) { + for (auto& a : adapters) { wgpu::AdapterProperties props; a.GetProperties(&props); if (props.backendType != defaultBackendType) { continue; } + KJ_LOG(INFO, kj::str("found webgpu device '", props.name, "' of type ", + parseAdapterType(props.adapterType))); adapter = a; break; } @@ -59,8 +75,8 @@ GPU::requestAdapter(jsg::Lock &js) { return js.resolvedPromise(kj::mv(gpuAdapter)); } - // We did not find an adapter that matched what we wanted - return nullptr; + KJ_LOG(WARNING, "did not find an adapter that matched what we wanted"); + return js.resolvedPromise(kj::Maybe>(nullptr)); } } // namespace workerd::api::gpu diff --git a/src/workerd/api/gpu/gpu.h b/src/workerd/api/gpu/gpu.h index 44a707dc2d6..cf72fe0b9e4 100644 --- a/src/workerd/api/gpu/gpu.h +++ b/src/workerd/api/gpu/gpu.h @@ -4,17 +4,23 @@ #pragma once +#include "gpu-adapter-info.h" #include "gpu-adapter.h" #include "gpu-bindgroup-layout.h" #include "gpu-bindgroup.h" +#include "gpu-command-buffer.h" #include "gpu-command-encoder.h" #include "gpu-compute-pass-encoder.h" #include "gpu-compute-pipeline.h" #include "gpu-device.h" +#include "gpu-errors.h" #include "gpu-pipeline-layout.h" #include "gpu-query-set.h" +#include "gpu-queue.h" #include "gpu-sampler.h" #include "gpu-shader-module.h" +#include "gpu-supported-features.h" +#include "gpu-supported-limits.h" #include "gpu-utils.h" #include #include @@ -24,13 +30,21 @@ namespace workerd::api::gpu { void initialize(); +struct GPURequestAdapterOptions { + GPUPowerPreference powerPreference; + jsg::Optional forceFallbackAdapter; + + JSG_STRUCT(powerPreference, forceFallbackAdapter); +}; + class GPU : public jsg::Object { public: explicit GPU(); JSG_RESOURCE_TYPE(GPU) { JSG_METHOD(requestAdapter); } private: - jsg::Promise>> requestAdapter(jsg::Lock &); + jsg::Promise>> + requestAdapter(jsg::Lock &, jsg::Optional); dawn::native::Instance instance_; }; @@ -53,6 +67,12 @@ class GPU : public jsg::Object { api::gpu::GPUProgrammableStage, api::gpu::GPUCommandEncoder, \ api::gpu::GPUCommandEncoderDescriptor, api::gpu::GPUComputePassEncoder, \ api::gpu::GPUComputePassDescriptor, api::gpu::GPUQuerySet, \ - api::gpu::GPUQuerySetDescriptor, api::gpu::GPUComputePassTimestampWrite + api::gpu::GPUQuerySetDescriptor, api::gpu::GPUComputePassTimestampWrite, \ + api::gpu::GPUCommandBufferDescriptor, api::gpu::GPUCommandBuffer, \ + api::gpu::GPUQueue, api::gpu::GPUMapMode, \ + api::gpu::GPURequestAdapterOptions, api::gpu::GPUAdapterInfo, \ + api::gpu::GPUSupportedFeatures, api::gpu::GPUSupportedLimits, \ + api::gpu::GPUError, api::gpu::GPUOOMError, api::gpu::GPUValidationError, \ + api::gpu::GPUDeviceLostInfo }; // namespace workerd::api::gpu diff --git a/src/workerd/api/gpu/webgpu-buffer-test.gpu-wd-test b/src/workerd/api/gpu/webgpu-buffer-test.gpu-wd-test new file mode 100644 index 00000000000..09e38a7ebe8 --- /dev/null +++ b/src/workerd/api/gpu/webgpu-buffer-test.gpu-wd-test @@ -0,0 +1,15 @@ +using Workerd = import "/workerd/workerd.capnp"; + +const unitTests :Workerd.Config = ( + services = [ + ( name = "webgpu-test", + worker = ( + modules = [ + (name = "worker", esModule = embed "webgpu-buffer-test.js") + ], + compatibilityDate = "2023-01-15", + compatibilityFlags = ["experimental", "nodejs_compat"], + ) + ), + ], +); diff --git a/src/workerd/api/gpu/webgpu-buffer-test.js b/src/workerd/api/gpu/webgpu-buffer-test.js new file mode 100644 index 00000000000..0f3480bff58 --- /dev/null +++ b/src/workerd/api/gpu/webgpu-buffer-test.js @@ -0,0 +1,61 @@ +import { deepEqual, ok } from "node:assert"; + +// run manually for now +// bazel run --//src/workerd/io:enable_experimental_webgpu //src/workerd/server:workerd -- test `realpath ./src/workerd/api/gpu/webgpu-buffer-test.gpu-wd-test` --verbose --experimental + +export const read_sync_stack = { + async test(ctrl, env, ctx) { + ok(navigator.gpu); + const adapter = await navigator.gpu.requestAdapter(); + ok(adapter); + const device = await adapter.requestDevice(); + ok(device); + + // Get a GPU buffer in a mapped state and an arrayBuffer for writing. + const gpuWriteBuffer = device.createBuffer({ + mappedAtCreation: true, + size: 4, + usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC, + }); + ok(gpuWriteBuffer); + const arrayBuffer = gpuWriteBuffer.getMappedRange(); + ok(arrayBuffer); + + // Write bytes to buffer. + new Uint8Array(arrayBuffer).set([0, 1, 2, 3]); + + // Unmap buffer so that it can be used later for copy. + gpuWriteBuffer.unmap(); + + // Get a GPU buffer for reading in an unmapped state. + const gpuReadBuffer = device.createBuffer({ + mappedAtCreation: false, + size: 4, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); + ok(gpuReadBuffer); + + // Encode commands for copying buffer to buffer. + const copyEncoder = device.createCommandEncoder(); + ok(copyEncoder); + copyEncoder.copyBufferToBuffer( + gpuWriteBuffer /* source buffer */, + 0 /* source offset */, + gpuReadBuffer /* destination buffer */, + 0 /* destination offset */, + 4 /* size */ + ); + + // Submit copy commands. + const copyCommands = copyEncoder.finish(); + ok(copyCommands); + device.queue.submit([copyCommands]); + + // Read buffer. + await gpuReadBuffer.mapAsync(GPUMapMode.READ); + const copyArrayBuffer = gpuReadBuffer.getMappedRange(); + ok(copyArrayBuffer); + + deepEqual(new Uint8Array(copyArrayBuffer), new Uint8Array([ 0, 1, 2, 3 ])); + }, +}; diff --git a/src/workerd/api/gpu/webgpu-compute-test.gpu-wd-test b/src/workerd/api/gpu/webgpu-compute-test.gpu-wd-test new file mode 100644 index 00000000000..37f446f6734 --- /dev/null +++ b/src/workerd/api/gpu/webgpu-compute-test.gpu-wd-test @@ -0,0 +1,15 @@ +using Workerd = import "/workerd/workerd.capnp"; + +const unitTests :Workerd.Config = ( + services = [ + ( name = "webgpu-test", + worker = ( + modules = [ + (name = "worker", esModule = embed "webgpu-compute-test.js") + ], + compatibilityDate = "2023-01-15", + compatibilityFlags = ["experimental", "nodejs_compat"], + ) + ), + ], +); diff --git a/src/workerd/api/gpu/webgpu-test.js b/src/workerd/api/gpu/webgpu-compute-test.js similarity index 67% rename from src/workerd/api/gpu/webgpu-test.js rename to src/workerd/api/gpu/webgpu-compute-test.js index f7880ac7238..1a32eb86479 100644 --- a/src/workerd/api/gpu/webgpu-test.js +++ b/src/workerd/api/gpu/webgpu-compute-test.js @@ -1,7 +1,7 @@ -import { strictEqual, deepStrictEqual, ok } from "node:assert"; +import { deepEqual, ok } from "node:assert"; // run manually for now -// bazel run --//src/workerd/io:enable_experimental_webgpu //src/workerd/server:workerd -- test `realpath ./src/workerd/api/gpu/webgpu-test.gpu-wd-test` --verbose --experimental +// bazel run --//src/workerd/io:enable_experimental_webgpu //src/workerd/server:workerd -- test `realpath ./src/workerd/api/gpu/webgpu-compute-test.gpu-wd-test` --verbose --experimental export const read_sync_stack = { async test(ctrl, env, ctx) { @@ -16,6 +16,16 @@ export const read_sync_stack = { const adapter = await navigator.gpu.requestAdapter(); ok(adapter); + const adapterInfo = await adapter.requestAdapterInfo(["device"]); + ok(adapterInfo); + ok(adapterInfo.device); + + ok(adapter.features.keys()); + ok(adapter.features.has("depth-clip-control")); + + ok(adapter.limits); + ok(adapter.limits.maxBufferSize) + const requiredFeatures = []; requiredFeatures.push("texture-compression-astc"); requiredFeatures.push("depth-clip-control"); @@ -24,10 +34,15 @@ export const read_sync_stack = { }); ok(device); + ok(device.lost); + const firstMatrix = new Float32Array([ 2 /* rows */, 4 /* columns */, 1, 2, 3, 4, 5, 6, 7, 8, ]); + device.pushErrorScope('out-of-memory'); + device.pushErrorScope('validation'); + const gpuBufferFirstMatrix = device.createBuffer({ mappedAtCreation: true, size: firstMatrix.byteLength, @@ -35,6 +50,8 @@ export const read_sync_stack = { }); ok(gpuBufferFirstMatrix); + ok(await device.popErrorScope() === null); + const arrayBufferFirstMatrix = gpuBufferFirstMatrix.getMappedRange(); ok(arrayBufferFirstMatrix); @@ -169,6 +186,42 @@ export const read_sync_stack = { }); ok(computePipeline); + // Pipeline with auto layout + const computePipelineAuto = device.createComputePipeline({ + layout: "auto", + compute: { + module: shaderModule, + entryPoint: "main", + }, + }); + ok(computePipelineAuto); + + // bind group with inferred layout + const bindGroupAutoLayout = device.createBindGroup({ + layout: computePipelineAuto.getBindGroupLayout(0 /* index */), + entries: [ + { + binding: 0, + resource: { + buffer: gpuBufferFirstMatrix, + }, + }, + { + binding: 1, + resource: { + buffer: gpuBufferSecondMatrix, + }, + }, + { + binding: 2, + resource: { + buffer: resultMatrixBuffer, + }, + }, + ], + }); + ok(bindGroupAutoLayout); + // Commands submission const commandEncoder = device.createCommandEncoder(); ok(commandEncoder); @@ -177,5 +230,43 @@ export const read_sync_stack = { ok(passEncoder); passEncoder.setPipeline(computePipeline); passEncoder.setBindGroup(0, bindGroup); + + const workgroupCountX = Math.ceil(firstMatrix[0] / 8); + const workgroupCountY = Math.ceil(secondMatrix[1] / 8); + passEncoder.dispatchWorkgroups(workgroupCountX, workgroupCountY); + passEncoder.end(); + + // Get a GPU buffer for reading in an unmapped state. + const gpuReadBuffer = device.createBuffer({ + size: resultMatrixBufferSize, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); + ok(gpuReadBuffer); + + // Encode commands for copying buffer to buffer. + commandEncoder.copyBufferToBuffer( + resultMatrixBuffer /* source buffer */, + 0 /* source offset */, + gpuReadBuffer /* destination buffer */, + 0 /* destination offset */, + resultMatrixBufferSize /* size */ + ); + + // Submit GPU commands. + const gpuCommands = commandEncoder.finish(); + ok(gpuCommands); + + device.queue.submit([gpuCommands]); + + // Read buffer. + await gpuReadBuffer.mapAsync(GPUMapMode.READ); + + const arrayBuffer = gpuReadBuffer.getMappedRange(); + ok(arrayBuffer); + + deepEqual( + new Float32Array(arrayBuffer), + new Float32Array([2, 2, 50, 60, 114, 140]) + ); }, }; diff --git a/src/workerd/api/gpu/webgpu-test.gpu-wd-test b/src/workerd/api/gpu/webgpu-write-test.gpu-wd-test similarity index 81% rename from src/workerd/api/gpu/webgpu-test.gpu-wd-test rename to src/workerd/api/gpu/webgpu-write-test.gpu-wd-test index 4c8ed9024a9..388018bc5fd 100644 --- a/src/workerd/api/gpu/webgpu-test.gpu-wd-test +++ b/src/workerd/api/gpu/webgpu-write-test.gpu-wd-test @@ -5,7 +5,7 @@ const unitTests :Workerd.Config = ( ( name = "webgpu-test", worker = ( modules = [ - (name = "worker", esModule = embed "webgpu-test.js") + (name = "worker", esModule = embed "webgpu-write-test.js") ], compatibilityDate = "2023-01-15", compatibilityFlags = ["experimental", "nodejs_compat"], diff --git a/src/workerd/api/gpu/webgpu-write-test.js b/src/workerd/api/gpu/webgpu-write-test.js new file mode 100644 index 00000000000..b7994fad484 --- /dev/null +++ b/src/workerd/api/gpu/webgpu-write-test.js @@ -0,0 +1,28 @@ +import { ok, deepEqual } from "node:assert"; + +// run manually for now +// bazel run --//src/workerd/io:enable_experimental_webgpu //src/workerd/server:workerd -- test `realpath ./src/workerd/api/gpu/webgpu-write-test.gpu-wd-test` --verbose --experimental + +export const read_sync_stack = { + async test(ctrl, env, ctx) { + ok(navigator.gpu); + const adapter = await navigator.gpu.requestAdapter(); + ok(adapter); + const device = await adapter.requestDevice(); + ok(device); + + // Get a GPU buffer in a mapped state and an arrayBuffer for writing. + const gpuWriteBuffer = device.createBuffer({ + mappedAtCreation: true, + size: 4, + usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC, + }); + ok(gpuWriteBuffer); + const arrayBuffer = gpuWriteBuffer.getMappedRange(); + ok(arrayBuffer); + + // Write bytes to buffer. + new Uint8Array(arrayBuffer).set([0, 1, 2, 3]); + deepEqual(new Uint8Array(arrayBuffer), new Uint8Array([ 0, 1, 2, 3 ])); + }, +}; diff --git a/src/workerd/jsg/rtti.capnp b/src/workerd/jsg/rtti.capnp index cdc285d29ab..1fe42e82798 100644 --- a/src/workerd/jsg/rtti.capnp +++ b/src/workerd/jsg/rtti.capnp @@ -142,6 +142,9 @@ struct BuiltinType { v8Function @4; # v8::Function + + v8ArrayBuffer @5; + # v8::ArrayBuffer } type @0 :Type; diff --git a/src/workerd/jsg/rtti.h b/src/workerd/jsg/rtti.h index 44a570788dd..ccace1eea26 100644 --- a/src/workerd/jsg/rtti.h +++ b/src/workerd/jsg/rtti.h @@ -380,6 +380,7 @@ struct BuildRtti { \ F(jsg::BufferSource, BuiltinType::Type::JSG_BUFFER_SOURCE) \ F(kj::Date, BuiltinType::Type::KJ_DATE) \ F(v8::ArrayBufferView, BuiltinType::Type::V8_ARRAY_BUFFER_VIEW) \ + F(v8::ArrayBuffer, BuiltinType::Type::V8_ARRAY_BUFFER) \ F(v8::Function, BuiltinType::Type::V8_FUNCTION) \ F(v8::Uint8Array, BuiltinType::Type::V8_UINT8_ARRAY)