Files
tubestation/dom/webgpu/CanvasContext.cpp
Jamie Nicol 1366435a07 Bug 1955214 - Ensure alpha channel is cleared to 1.0 for opaque webgpu canvas readbacks. r=webgpu-reviewers,ErichDonGubler
The spec[1] mandates that we must clear the alpha channel to 1.0 when
snapshotting the contents of an opaque canvas context.

This was causing failures in the `alphaMode="opaque"` subtests of the
`webgpu:web_platform,canvas,readbackFromWebGPUCanvas:*` CTS tree, due to
unexpected pixel values. Additionally we would crash in debug builds
when subsequently rendering the surface using Skia, as Skia requires
RGBX/BGRX format surfaces to have an alpha value of 255.

This patch ensures we clear the alpha channel to 1.0 for opaque
contexts. This ensures the pixel values are as expected and avoids the
crash in debug builds. Note, however, that the affected tests still fail
due to bug 1948583.

[1] https://www.w3.org/TR/webgpu/#abstract-opdef-get-a-copy-of-the-image-contents-of-a-context

Differential Revision: https://phabricator.services.mozilla.com/D242284
2025-03-20 14:19:41 +00:00

457 lines
14 KiB
C++

/* -*- 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 "CanvasContext.h"
#include "gfxUtils.h"
#include "LayerUserData.h"
#include "nsDisplayList.h"
#include "mozilla/dom/HTMLCanvasElement.h"
#include "mozilla/gfx/2D.h"
#include "mozilla/gfx/CanvasManagerChild.h"
#include "mozilla/gfx/gfxVars.h"
#include "mozilla/layers/CanvasRenderer.h"
#include "mozilla/layers/CompositableForwarder.h"
#include "mozilla/layers/ImageDataSerializer.h"
#include "mozilla/layers/LayersSurfaces.h"
#include "mozilla/layers/RenderRootStateManager.h"
#include "mozilla/layers/WebRenderCanvasRenderer.h"
#include "mozilla/StaticPrefs_privacy.h"
#include "mozilla/SVGObserverUtils.h"
#include "ipc/WebGPUChild.h"
#include "Utility.h"
namespace mozilla {
inline void ImplCycleCollectionTraverse(
nsCycleCollectionTraversalCallback& aCallback,
dom::GPUCanvasConfiguration& aField, const char* aName, uint32_t aFlags) {
aField.TraverseForCC(aCallback, aFlags);
}
inline void ImplCycleCollectionUnlink(dom::GPUCanvasConfiguration& aField) {
aField.UnlinkForCC();
}
// -
template <class T>
inline void ImplCycleCollectionTraverse(
nsCycleCollectionTraversalCallback& aCallback,
const std::unique_ptr<T>& aField, const char* aName, uint32_t aFlags) {
if (aField) {
ImplCycleCollectionTraverse(aCallback, *aField, aName, aFlags);
}
}
template <class T>
inline void ImplCycleCollectionUnlink(std::unique_ptr<T>& aField) {
aField = nullptr;
}
} // namespace mozilla
// -
namespace mozilla::webgpu {
NS_IMPL_CYCLE_COLLECTING_ADDREF(CanvasContext)
NS_IMPL_CYCLE_COLLECTING_RELEASE(CanvasContext)
GPU_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_WEAK_PTR(CanvasContext, mConfig,
mTexture, mBridge,
mCanvasElement,
mOffscreenCanvas)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CanvasContext)
NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
NS_INTERFACE_MAP_ENTRY(nsICanvasRenderingContextInternal)
NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END
// -
CanvasContext::CanvasContext() = default;
CanvasContext::~CanvasContext() {
Cleanup();
RemovePostRefreshObserver();
}
void CanvasContext::Cleanup() { Unconfigure(); }
JSObject* CanvasContext::WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) {
return dom::GPUCanvasContext_Binding::Wrap(aCx, this, aGivenProto);
}
// -
void CanvasContext::GetCanvas(
dom::OwningHTMLCanvasElementOrOffscreenCanvas& aRetVal) const {
if (mCanvasElement) {
aRetVal.SetAsHTMLCanvasElement() = mCanvasElement;
} else if (mOffscreenCanvas) {
aRetVal.SetAsOffscreenCanvas() = mOffscreenCanvas;
} else {
MOZ_CRASH(
"This should only happen briefly during CC Unlink, and no JS should "
"happen then.");
}
}
void CanvasContext::Configure(const dom::GPUCanvasConfiguration& aConfig) {
Unconfigure();
// Bug 1864904: Failures in validation should throw a TypeError, per spec.
// these formats are guaranteed by the spec
switch (aConfig.mFormat) {
case dom::GPUTextureFormat::Rgba8unorm:
case dom::GPUTextureFormat::Rgba8unorm_srgb:
mGfxFormat = gfx::SurfaceFormat::R8G8B8A8;
break;
case dom::GPUTextureFormat::Bgra8unorm:
case dom::GPUTextureFormat::Bgra8unorm_srgb:
mGfxFormat = gfx::SurfaceFormat::B8G8R8A8;
break;
default:
NS_WARNING("Specified swap chain format is not supported");
return;
}
mConfig.reset(new dom::GPUCanvasConfiguration(aConfig));
mRemoteTextureOwnerId = Some(layers::RemoteTextureOwnerId::GetNext());
mUseExternalTextureInSwapChain =
aConfig.mDevice->mSupportExternalTextureInSwapChain &&
wgpu_client_use_external_texture_in_swapChain(
ConvertTextureFormat(aConfig.mFormat));
if (!gfx::gfxVars::AllowWebGPUPresentWithoutReadback()) {
mUseExternalTextureInSwapChain = false;
}
#ifdef XP_WIN
// When WebRender does not use hardware acceleration, disable external texture
// in swap chain. Since compositor device might not exist.
if (gfx::gfxVars::UseSoftwareWebRender() &&
!gfx::gfxVars::AllowSoftwareWebRenderD3D11()) {
mUseExternalTextureInSwapChain = false;
}
#elif defined(XP_LINUX) && !defined(MOZ_WIDGET_ANDROID)
// When DMABufDevice is not enabled, disable external texture in swap chain.
const auto& modifiers = gfx::gfxVars::DMABufModifiersARGB();
if (modifiers.IsEmpty()) {
mUseExternalTextureInSwapChain = false;
}
#endif
mTexture = aConfig.mDevice->InitSwapChain(
mConfig.get(), mRemoteTextureOwnerId.ref(),
mUseExternalTextureInSwapChain, mGfxFormat, mCanvasSize);
if (!mTexture) {
Unconfigure();
return;
}
mTexture->mTargetContext = this;
mBridge = aConfig.mDevice->GetBridge();
if (mCanvasElement) {
mWaitingCanvasRendererInitialized = true;
}
ForceNewFrame();
}
void CanvasContext::Unconfigure() {
if (mBridge && mBridge->CanSend() && mRemoteTextureOwnerId) {
mBridge->SendSwapChainDrop(
*mRemoteTextureOwnerId,
layers::ToRemoteTextureTxnType(mFwdTransactionTracker),
layers::ToRemoteTextureTxnId(mFwdTransactionTracker));
}
mRemoteTextureOwnerId = Nothing();
mFwdTransactionTracker = nullptr;
mBridge = nullptr;
mConfig = nullptr;
mTexture = nullptr;
mGfxFormat = gfx::SurfaceFormat::UNKNOWN;
}
NS_IMETHODIMP CanvasContext::SetDimensions(int32_t aWidth, int32_t aHeight) {
const auto newSize = gfx::IntSize{aWidth, aHeight};
if (newSize == mCanvasSize) return NS_OK; // No-op no-change resizes.
mCanvasSize = newSize;
if (mConfig) {
const auto copy = dom::GPUCanvasConfiguration{
*mConfig}; // So we can't null it out on ourselves.
Configure(copy);
}
return NS_OK;
}
RefPtr<Texture> CanvasContext::GetCurrentTexture(ErrorResult& aRv) {
if (!mTexture) {
aRv.ThrowOperationError("Canvas not configured");
return nullptr;
}
MOZ_ASSERT(mConfig);
MOZ_ASSERT(mRemoteTextureOwnerId.isSome());
if (mNewTextureRequested) {
mNewTextureRequested = false;
mTexture = mConfig->mDevice->CreateTextureForSwapChain(
mConfig.get(), mCanvasSize, mRemoteTextureOwnerId.ref());
mTexture->mTargetContext = this;
}
return mTexture;
}
void CanvasContext::MaybeQueueSwapChainPresent() {
if (!mConfig) {
return;
}
MOZ_ASSERT(mTexture);
if (mTexture) {
mBridge->NotifyWaitForSubmit(mTexture->mId);
}
if (mPendingSwapChainPresent) {
return;
}
mPendingSwapChainPresent = true;
if (mWaitingCanvasRendererInitialized) {
return;
}
InvalidateCanvasContent();
}
Maybe<layers::SurfaceDescriptor> CanvasContext::SwapChainPresent() {
mPendingSwapChainPresent = false;
if (!mBridge || !mBridge->CanSend() || mRemoteTextureOwnerId.isNothing() ||
!mTexture) {
return Nothing();
}
mLastRemoteTextureId = Some(layers::RemoteTextureId::GetNext());
mBridge->SwapChainPresent(mTexture->mId, *mLastRemoteTextureId,
*mRemoteTextureOwnerId);
if (mUseExternalTextureInSwapChain) {
mTexture->Destroy();
mNewTextureRequested = true;
}
return Some(layers::SurfaceDescriptorRemoteTexture(*mLastRemoteTextureId,
*mRemoteTextureOwnerId));
}
bool CanvasContext::UpdateWebRenderCanvasData(
mozilla::nsDisplayListBuilder* aBuilder, WebRenderCanvasData* aCanvasData) {
auto* renderer = aCanvasData->GetCanvasRenderer();
if (renderer && mRemoteTextureOwnerId.isSome() &&
renderer->GetRemoteTextureOwnerId() == mRemoteTextureOwnerId) {
return true;
}
renderer = aCanvasData->CreateCanvasRenderer();
if (!InitializeCanvasRenderer(aBuilder, renderer)) {
// Clear CanvasRenderer of WebRenderCanvasData
aCanvasData->ClearCanvasRenderer();
return false;
}
return true;
}
bool CanvasContext::InitializeCanvasRenderer(
nsDisplayListBuilder* aBuilder, layers::CanvasRenderer* aRenderer) {
if (mRemoteTextureOwnerId.isNothing()) {
return false;
}
layers::CanvasRendererData data;
data.mContext = this;
data.mSize = mCanvasSize;
data.mIsOpaque = false;
data.mRemoteTextureOwnerId = mRemoteTextureOwnerId;
aRenderer->Initialize(data);
aRenderer->SetDirty();
if (mWaitingCanvasRendererInitialized) {
InvalidateCanvasContent();
}
mWaitingCanvasRendererInitialized = false;
return true;
}
mozilla::UniquePtr<uint8_t[]> CanvasContext::GetImageBuffer(
int32_t* out_format, gfx::IntSize* out_imageSize) {
*out_format = 0;
*out_imageSize = {};
gfxAlphaType any;
RefPtr<gfx::SourceSurface> snapshot = GetSurfaceSnapshot(&any);
if (!snapshot) {
return nullptr;
}
RefPtr<gfx::DataSourceSurface> dataSurface = snapshot->GetDataSurface();
*out_imageSize = dataSurface->GetSize();
if (ShouldResistFingerprinting(RFPTarget::CanvasRandomization)) {
gfxUtils::GetImageBufferWithRandomNoise(
dataSurface,
/* aIsAlphaPremultiplied */ true, GetCookieJarSettings(), &*out_format);
}
return gfxUtils::GetImageBuffer(dataSurface, /* aIsAlphaPremultiplied */ true,
&*out_format);
}
NS_IMETHODIMP CanvasContext::GetInputStream(const char* aMimeType,
const nsAString& aEncoderOptions,
nsIInputStream** aStream) {
gfxAlphaType any;
RefPtr<gfx::SourceSurface> snapshot = GetSurfaceSnapshot(&any);
if (!snapshot) {
return NS_ERROR_FAILURE;
}
RefPtr<gfx::DataSourceSurface> dataSurface = snapshot->GetDataSurface();
if (ShouldResistFingerprinting(RFPTarget::CanvasRandomization)) {
gfxUtils::GetInputStreamWithRandomNoise(
dataSurface, /* aIsAlphaPremultiplied */ true, aMimeType,
aEncoderOptions, GetCookieJarSettings(), aStream);
}
return gfxUtils::GetInputStream(dataSurface, /* aIsAlphaPremultiplied */ true,
aMimeType, aEncoderOptions, aStream);
}
bool CanvasContext::GetIsOpaque() {
if (!mConfig) {
return false;
}
return mConfig->mAlphaMode == dom::GPUCanvasAlphaMode::Opaque;
}
already_AddRefed<gfx::SourceSurface> CanvasContext::GetSurfaceSnapshot(
gfxAlphaType* aOutAlphaType) {
const bool isOpaque = GetIsOpaque();
gfx::SurfaceFormat snapshotFormat = mGfxFormat;
if (isOpaque) {
if (aOutAlphaType) {
*aOutAlphaType = gfxAlphaType::Opaque;
}
if (mGfxFormat == gfx::SurfaceFormat::B8G8R8A8) {
snapshotFormat = gfx::SurfaceFormat::B8G8R8X8;
} else if (mGfxFormat == gfx::SurfaceFormat::R8G8B8A8) {
snapshotFormat = gfx::SurfaceFormat::R8G8B8X8;
}
} else {
if (aOutAlphaType) {
*aOutAlphaType = gfxAlphaType::Premult;
}
}
auto* const cm = gfx::CanvasManagerChild::Get();
if (!cm) {
return nullptr;
}
if (!mBridge || !mBridge->CanSend() || mRemoteTextureOwnerId.isNothing()) {
return nullptr;
}
MOZ_ASSERT(mRemoteTextureOwnerId.isSome());
// The parent side needs to create a command encoder which will be submitted
// and dropped right away so we create and release an encoder ID here.
RawId encoderId = ffi::wgpu_client_make_encoder_id(mBridge->GetClient());
RefPtr<gfx::DataSourceSurface> snapshot =
cm->GetSnapshot(cm->Id(), mBridge->Id(), mRemoteTextureOwnerId,
Some(encoderId), snapshotFormat, /* aPremultiply */ false,
/* aYFlip */ false);
ffi::wgpu_client_free_command_encoder_id(mBridge->GetClient(), encoderId);
if (!snapshot) {
return nullptr;
}
// Clear alpha channel to 0xFF / 1.0 for opaque contexts.
// https://www.w3.org/TR/webgpu/#abstract-opdef-get-a-copy-of-the-image-contents-of-a-context
if (isOpaque) {
gfx::DataSourceSurface::ScopedMap map(snapshot,
gfx::DataSourceSurface::WRITE);
if (!map.IsMapped()) {
return nullptr;
}
for (int32_t y = 0; y < snapshot->GetSize().height; y++) {
for (int32_t x = 0; x < snapshot->GetSize().width; x++) {
uint8_t* const pixel = map.GetData() + y * map.GetStride() + x * 4;
pixel[3] = 0xFF;
}
}
}
return snapshot.forget();
}
Maybe<layers::SurfaceDescriptor> CanvasContext::GetFrontBuffer(
WebGLFramebufferJS*, const bool) {
if (mPendingSwapChainPresent) {
auto desc = SwapChainPresent();
MOZ_ASSERT(!mPendingSwapChainPresent);
return desc;
}
return Nothing();
}
already_AddRefed<layers::FwdTransactionTracker>
CanvasContext::UseCompositableForwarder(
layers::CompositableForwarder* aForwarder) {
return layers::FwdTransactionTracker::GetOrCreate(mFwdTransactionTracker);
}
void CanvasContext::ForceNewFrame() {
if (!mCanvasElement && !mOffscreenCanvas) {
return;
}
// Force a new frame to be built, which will execute the
// `CanvasContextType::WebGPU` switch case in `CreateWebRenderCommands` and
// populate the WR user data.
if (mCanvasElement) {
mCanvasElement->InvalidateCanvas();
} else if (mOffscreenCanvas) {
dom::OffscreenCanvasDisplayData data;
data.mSize = mCanvasSize;
data.mIsOpaque = false;
mOffscreenCanvas->UpdateDisplayData(data);
}
}
void CanvasContext::InvalidateCanvasContent() {
if (!mCanvasElement && !mOffscreenCanvas) {
MOZ_ASSERT_UNREACHABLE("unexpected to be called");
return;
}
if (mCanvasElement) {
SVGObserverUtils::InvalidateDirectRenderingObservers(mCanvasElement);
mCanvasElement->InvalidateCanvasContent(nullptr);
} else if (mOffscreenCanvas) {
mOffscreenCanvas->QueueCommitToCompositor();
}
}
} // namespace mozilla::webgpu