/* -*- 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 #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>& aCommandBuffers) { nsTArray 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& 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& 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(), 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 surface = sfeResult.GetSourceSurface(); if (!surface) { aRv.ThrowInvalidStateError("No surface available from source"); return; } RefPtr 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(srcOriginX) + extent.width; const auto checkedMaxHeight = CheckedInt(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(surfaceSize.width); const auto surfaceHeight = AssertedCast(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(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(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