diff options
Diffstat (limited to 'dom/webgpu/Queue.cpp')
-rw-r--r-- | dom/webgpu/Queue.cpp | 475 |
1 files changed, 475 insertions, 0 deletions
diff --git a/dom/webgpu/Queue.cpp b/dom/webgpu/Queue.cpp new file mode 100644 index 0000000000..c675c42cae --- /dev/null +++ b/dom/webgpu/Queue.cpp @@ -0,0 +1,475 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/WebGPUBinding.h" +#include "mozilla/dom/UnionTypes.h" +#include "Queue.h" + +#include <algorithm> + +#include "CommandBuffer.h" +#include "CommandEncoder.h" +#include "ipc/WebGPUChild.h" +#include "mozilla/Casting.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/HTMLCanvasElement.h" +#include "mozilla/dom/ImageBitmap.h" +#include "mozilla/dom/OffscreenCanvas.h" +#include "mozilla/dom/WebGLTexelConversions.h" +#include "mozilla/dom/WebGLTypes.h" +#include "nsLayoutUtils.h" +#include "Utility.h" + +namespace mozilla::webgpu { + +GPU_IMPL_CYCLE_COLLECTION(Queue, mParent, mBridge) +GPU_IMPL_JS_WRAP(Queue) + +Queue::Queue(Device* const aParent, WebGPUChild* aBridge, RawId aId) + : ChildOf(aParent), mBridge(aBridge), mId(aId) {} + +Queue::~Queue() { Cleanup(); } + +void Queue::Submit( + const dom::Sequence<OwningNonNull<CommandBuffer>>& aCommandBuffers) { + nsTArray<RawId> list(aCommandBuffers.Length()); + for (uint32_t i = 0; i < aCommandBuffers.Length(); ++i) { + auto idMaybe = aCommandBuffers[i]->Commit(); + if (idMaybe) { + list.AppendElement(*idMaybe); + } + } + + mBridge->SendQueueSubmit(mId, mParent->mId, list); +} + +// Get the base address and length of part of a `BufferSource`. +// +// Given `aBufferSource` and an offset `aDataOffset` and optional length +// `aSizeOrRemainder` describing the range of its contents we want to see, check +// all arguments and set `aDataContents` and `aContentsSize` to a pointer to the +// bytes and a length. Report errors in `aRv`. +// +// If `ASizeOrRemainder` was not passed, return a view from the starting offset +// to the end of `aBufferSource`. +// +// On success, the returned `aDataContents` is never `nullptr`. If the +// `ArrayBuffer` is detached, return a pointer to a dummy buffer and set +// `aContentsSize` to zero. +// +// The `aBufferSource` argument is a WebIDL `BufferSource`, which WebGPU methods +// use anywhere they accept a block of raw bytes. WebIDL defines `BufferSource` +// as: +// +// typedef (ArrayBufferView or ArrayBuffer) BufferSource; +// +// This appears in Gecko code as `dom::ArrayBufferViewOrArrayBuffer`. +static void GetBufferSourceDataAndSize( + const dom::ArrayBufferViewOrArrayBuffer& aBufferSource, + uint64_t aDataOffset, const dom::Optional<uint64_t>& aSizeOrRemainder, + uint8_t*& aDataContents, uint64_t& aContentsSize, const char* aOffsetName, + ErrorResult& aRv) { + uint64_t dataSize = 0; + uint8_t* dataContents = nullptr; + if (aBufferSource.IsArrayBufferView()) { + const auto& view = aBufferSource.GetAsArrayBufferView(); + view.ComputeState(); + dataSize = view.Length(); + dataContents = view.Data(); + } + if (aBufferSource.IsArrayBuffer()) { + const auto& ab = aBufferSource.GetAsArrayBuffer(); + ab.ComputeState(); + dataSize = ab.Length(); + dataContents = ab.Data(); + } + + if (aDataOffset > dataSize) { + aRv.ThrowOperationError( + nsPrintfCString("%s is greater than data length", aOffsetName)); + return; + } + + uint64_t contentsSize = 0; + if (aSizeOrRemainder.WasPassed()) { + contentsSize = aSizeOrRemainder.Value(); + } else { + // We already know that aDataOffset <= length, so this cannot underflow. + contentsSize = dataSize - aDataOffset; + } + + // This could be folded into the if above, but it's nice to make it + // obvious that the check always occurs. + // We already know that aDataOffset <= length, so this cannot underflow. + if (contentsSize > dataSize - aDataOffset) { + aRv.ThrowOperationError( + nsPrintfCString("%s + size is greater than data length", aOffsetName)); + return; + } + + if (!dataContents) { + // Passing `nullptr` as either the source or destination to + // `memcpy` is undefined behavior, even when the count is zero: + // + // https://en.cppreference.com/w/cpp/string/byte/memcpy + // + // We can either make callers responsible for checking the pointer + // before calling `memcpy`, or we can have it point to a + // permanently-live `static` dummy byte, so that the copies are + // harmless. The latter seems less error-prone. + static uint8_t dummy; + dataContents = &dummy; + MOZ_RELEASE_ASSERT(contentsSize == 0); + } + aDataContents = dataContents; + aContentsSize = contentsSize; +} + +void Queue::WriteBuffer(const Buffer& aBuffer, uint64_t aBufferOffset, + const dom::ArrayBufferViewOrArrayBuffer& aData, + uint64_t aDataOffset, + const dom::Optional<uint64_t>& aSize, + ErrorResult& aRv) { + uint8_t* dataContents = nullptr; + uint64_t contentsSize = 0; + GetBufferSourceDataAndSize(aData, aDataOffset, aSize, dataContents, + contentsSize, "dataOffset", aRv); + if (aRv.Failed()) { + return; + } + + if (contentsSize % 4 != 0) { + aRv.ThrowAbortError("Byte size must be a multiple of 4"); + return; + } + + auto alloc = + mozilla::ipc::UnsafeSharedMemoryHandle::CreateAndMap(contentsSize); + if (alloc.isNothing()) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + auto handle = std::move(alloc.ref().first); + auto mapping = std::move(alloc.ref().second); + + memcpy(mapping.Bytes().data(), dataContents + aDataOffset, contentsSize); + ipc::ByteBuf bb; + ffi::wgpu_queue_write_buffer(aBuffer.mId, aBufferOffset, ToFFI(&bb)); + if (!mBridge->SendQueueWriteAction(mId, mParent->mId, std::move(bb), + std::move(handle))) { + MOZ_CRASH("IPC failure"); + } +} + +void Queue::WriteTexture(const dom::GPUImageCopyTexture& aDestination, + const dom::ArrayBufferViewOrArrayBuffer& aData, + const dom::GPUImageDataLayout& aDataLayout, + const dom::GPUExtent3D& aSize, ErrorResult& aRv) { + ffi::WGPUImageCopyTexture copyView = {}; + CommandEncoder::ConvertTextureCopyViewToFFI(aDestination, ©View); + ffi::WGPUImageDataLayout dataLayout = {}; + CommandEncoder::ConvertTextureDataLayoutToFFI(aDataLayout, &dataLayout); + dataLayout.offset = 0; // our Shmem has the contents starting from 0. + ffi::WGPUExtent3d extent = {}; + ConvertExtent3DToFFI(aSize, &extent); + + uint8_t* dataContents = nullptr; + uint64_t contentsSize = 0; + GetBufferSourceDataAndSize(aData, aDataLayout.mOffset, + dom::Optional<uint64_t>(), dataContents, + contentsSize, "dataLayout.offset", aRv); + if (aRv.Failed()) { + return; + } + + if (!contentsSize) { + aRv.ThrowAbortError("Input size cannot be zero."); + return; + } + MOZ_ASSERT(dataContents != nullptr); + + auto alloc = + mozilla::ipc::UnsafeSharedMemoryHandle::CreateAndMap(contentsSize); + if (alloc.isNothing()) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + auto handle = std::move(alloc.ref().first); + auto mapping = std::move(alloc.ref().second); + + memcpy(mapping.Bytes().data(), dataContents + aDataLayout.mOffset, + contentsSize); + + ipc::ByteBuf bb; + ffi::wgpu_queue_write_texture(copyView, dataLayout, extent, ToFFI(&bb)); + if (!mBridge->SendQueueWriteAction(mId, mParent->mId, std::move(bb), + std::move(handle))) { + MOZ_CRASH("IPC failure"); + } +} + +static WebGLTexelFormat ToWebGLTexelFormat(gfx::SurfaceFormat aFormat) { + switch (aFormat) { + case gfx::SurfaceFormat::B8G8R8A8: + case gfx::SurfaceFormat::B8G8R8X8: + return WebGLTexelFormat::BGRA8; + case gfx::SurfaceFormat::R8G8B8A8: + case gfx::SurfaceFormat::R8G8B8X8: + return WebGLTexelFormat::RGBA8; + default: + return WebGLTexelFormat::FormatNotSupportingAnyConversion; + } +} + +static WebGLTexelFormat ToWebGLTexelFormat(dom::GPUTextureFormat aFormat) { + // TODO: We need support for Rbg10a2unorm as well. + switch (aFormat) { + case dom::GPUTextureFormat::R8unorm: + return WebGLTexelFormat::R8; + case dom::GPUTextureFormat::R16float: + return WebGLTexelFormat::R16F; + case dom::GPUTextureFormat::R32float: + return WebGLTexelFormat::R32F; + case dom::GPUTextureFormat::Rg8unorm: + return WebGLTexelFormat::RG8; + case dom::GPUTextureFormat::Rg16float: + return WebGLTexelFormat::RG16F; + case dom::GPUTextureFormat::Rg32float: + return WebGLTexelFormat::RG32F; + case dom::GPUTextureFormat::Rgba8unorm: + case dom::GPUTextureFormat::Rgba8unorm_srgb: + return WebGLTexelFormat::RGBA8; + case dom::GPUTextureFormat::Bgra8unorm: + case dom::GPUTextureFormat::Bgra8unorm_srgb: + return WebGLTexelFormat::BGRA8; + case dom::GPUTextureFormat::Rgba16float: + return WebGLTexelFormat::RGBA16F; + case dom::GPUTextureFormat::Rgba32float: + return WebGLTexelFormat::RGBA32F; + default: + return WebGLTexelFormat::FormatNotSupportingAnyConversion; + } +} + +void Queue::CopyExternalImageToTexture( + const dom::GPUImageCopyExternalImage& aSource, + const dom::GPUImageCopyTextureTagged& aDestination, + const dom::GPUExtent3D& aCopySize, ErrorResult& aRv) { + const auto dstFormat = ToWebGLTexelFormat(aDestination.mTexture->Format()); + if (dstFormat == WebGLTexelFormat::FormatNotSupportingAnyConversion) { + aRv.ThrowInvalidStateError("Unsupported destination format"); + return; + } + + const uint32_t surfaceFlags = nsLayoutUtils::SFE_ALLOW_NON_PREMULT; + SurfaceFromElementResult sfeResult; + switch (aSource.mSource.GetType()) { + case decltype(aSource.mSource)::Type::eImageBitmap: { + const auto& bitmap = aSource.mSource.GetAsImageBitmap(); + if (bitmap->IsClosed()) { + aRv.ThrowInvalidStateError("Detached ImageBitmap"); + return; + } + + sfeResult = nsLayoutUtils::SurfaceFromImageBitmap(bitmap, surfaceFlags); + break; + } + case decltype(aSource.mSource)::Type::eHTMLCanvasElement: { + MOZ_ASSERT(NS_IsMainThread()); + + const auto& canvas = aSource.mSource.GetAsHTMLCanvasElement(); + if (canvas->Width() == 0 || canvas->Height() == 0) { + aRv.ThrowInvalidStateError("Zero-sized HTMLCanvasElement"); + return; + } + + sfeResult = nsLayoutUtils::SurfaceFromElement(canvas, surfaceFlags); + break; + } + case decltype(aSource.mSource)::Type::eOffscreenCanvas: { + const auto& canvas = aSource.mSource.GetAsOffscreenCanvas(); + if (canvas->Width() == 0 || canvas->Height() == 0) { + aRv.ThrowInvalidStateError("Zero-sized OffscreenCanvas"); + return; + } + + sfeResult = + nsLayoutUtils::SurfaceFromOffscreenCanvas(canvas, surfaceFlags); + break; + } + } + + if (!sfeResult.mCORSUsed) { + nsIGlobalObject* global = mParent->GetOwnerGlobal(); + nsIPrincipal* dstPrincipal = global ? global->PrincipalOrNull() : nullptr; + if (!sfeResult.mPrincipal || !dstPrincipal || + !dstPrincipal->Subsumes(sfeResult.mPrincipal)) { + aRv.ThrowSecurityError("Cross-origin elements require CORS!"); + return; + } + } + + if (sfeResult.mIsWriteOnly) { + aRv.ThrowSecurityError("Write only source data not supported!"); + return; + } + + RefPtr<gfx::SourceSurface> surface = sfeResult.GetSourceSurface(); + if (!surface) { + aRv.ThrowInvalidStateError("No surface available from source"); + return; + } + + RefPtr<gfx::DataSourceSurface> dataSurface = surface->GetDataSurface(); + if (!dataSurface) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + bool srcPremultiplied; + switch (sfeResult.mAlphaType) { + case gfxAlphaType::Premult: + srcPremultiplied = true; + break; + case gfxAlphaType::NonPremult: + srcPremultiplied = false; + break; + case gfxAlphaType::Opaque: + // No (un)premultiplication necessary so match the output. + srcPremultiplied = aDestination.mPremultipliedAlpha; + break; + } + + const auto surfaceFormat = dataSurface->GetFormat(); + const auto srcFormat = ToWebGLTexelFormat(surfaceFormat); + if (srcFormat == WebGLTexelFormat::FormatNotSupportingAnyConversion) { + gfxCriticalError() << "Unsupported surface format from source " + << surfaceFormat; + MOZ_CRASH(); + } + + gfx::DataSourceSurface::ScopedMap map(dataSurface, + gfx::DataSourceSurface::READ); + if (!map.IsMapped()) { + aRv.ThrowInvalidStateError("Cannot map surface from source"); + return; + } + + if (!aSource.mOrigin.IsGPUOrigin2DDict()) { + aRv.ThrowInvalidStateError("Cannot get origin from source"); + return; + } + + ffi::WGPUExtent3d extent = {}; + ConvertExtent3DToFFI(aCopySize, &extent); + if (extent.depth_or_array_layers > 1) { + aRv.ThrowOperationError("Depth is greater than 1"); + return; + } + + uint32_t srcOriginX; + uint32_t srcOriginY; + if (aSource.mOrigin.IsRangeEnforcedUnsignedLongSequence()) { + const auto& seq = aSource.mOrigin.GetAsRangeEnforcedUnsignedLongSequence(); + srcOriginX = seq.Length() > 0 ? seq[0] : 0; + srcOriginY = seq.Length() > 1 ? seq[1] : 0; + } else if (aSource.mOrigin.IsGPUOrigin2DDict()) { + const auto& dict = aSource.mOrigin.GetAsGPUOrigin2DDict(); + srcOriginX = dict.mX; + srcOriginY = dict.mY; + } else { + MOZ_CRASH("Unexpected origin type!"); + } + + const auto checkedMaxWidth = CheckedInt<uint32_t>(srcOriginX) + extent.width; + const auto checkedMaxHeight = + CheckedInt<uint32_t>(srcOriginY) + extent.height; + if (!checkedMaxWidth.isValid() || !checkedMaxHeight.isValid()) { + aRv.ThrowOperationError("Offset and copy size exceed integer bounds"); + return; + } + + const gfx::IntSize surfaceSize = dataSurface->GetSize(); + const auto surfaceWidth = AssertedCast<uint32_t>(surfaceSize.width); + const auto surfaceHeight = AssertedCast<uint32_t>(surfaceSize.height); + if (surfaceWidth < checkedMaxWidth.value() || + surfaceHeight < checkedMaxHeight.value()) { + aRv.ThrowOperationError("Offset and copy size exceed surface bounds"); + return; + } + + const auto dstWidth = extent.width; + const auto dstHeight = extent.height; + if (dstWidth == 0 || dstHeight == 0) { + aRv.ThrowOperationError("Destination size is empty"); + return; + } + + if (!aDestination.mTexture->mBytesPerBlock) { + // TODO(bug 1781071) This should emmit a GPUValidationError on the device + // timeline. + aRv.ThrowInvalidStateError("Invalid destination format"); + return; + } + + // Note: This assumes bytes per block == bytes per pixel which is the case + // here because the spec only allows non-compressed texture formats for the + // destination. + const auto dstStride = CheckedInt<uint32_t>(extent.width) * + aDestination.mTexture->mBytesPerBlock.value(); + const auto dstByteLength = dstStride * extent.height; + if (!dstStride.isValid() || !dstByteLength.isValid()) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + auto alloc = mozilla::ipc::UnsafeSharedMemoryHandle::CreateAndMap( + dstByteLength.value()); + if (alloc.isNothing()) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + auto handle = std::move(alloc.ref().first); + auto mapping = std::move(alloc.ref().second); + + const int32_t pixelSize = gfx::BytesPerPixel(surfaceFormat); + auto* dstBegin = mapping.Bytes().data(); + const auto* srcBegin = + map.GetData() + srcOriginX * pixelSize + srcOriginY * map.GetStride(); + const auto srcOriginPos = gl::OriginPos::TopLeft; + const auto srcStride = AssertedCast<uint32_t>(map.GetStride()); + const auto dstOriginPos = + aSource.mFlipY ? gl::OriginPos::BottomLeft : gl::OriginPos::TopLeft; + bool wasTrivial; + + auto dstStrideVal = dstStride.value(); + + if (!ConvertImage(dstWidth, dstHeight, srcBegin, srcStride, srcOriginPos, + srcFormat, srcPremultiplied, dstBegin, dstStrideVal, + dstOriginPos, dstFormat, aDestination.mPremultipliedAlpha, + &wasTrivial)) { + MOZ_ASSERT_UNREACHABLE("ConvertImage failed!"); + aRv.ThrowInvalidStateError( + nsPrintfCString("Failed to convert source to destination format " + "(%i/%i), please file a bug!", + (int)srcFormat, (int)dstFormat)); + return; + } + + ffi::WGPUImageDataLayout dataLayout = {0, &dstStrideVal, &dstHeight}; + ffi::WGPUImageCopyTexture copyView = {}; + CommandEncoder::ConvertTextureCopyViewToFFI(aDestination, ©View); + ipc::ByteBuf bb; + ffi::wgpu_queue_write_texture(copyView, dataLayout, extent, ToFFI(&bb)); + if (!mBridge->SendQueueWriteAction(mId, mParent->mId, std::move(bb), + std::move(handle))) { + MOZ_CRASH("IPC failure"); + } +} + +} // namespace mozilla::webgpu |