blob: 26cd6566beb24249e69589091f1731a7b3fbbb46 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "modules/canvas2d/CanvasRenderingContext2D.h"
#include <memory>
#include "bindings/core/v8/V8BindingForCore.h"
#include "bindings/core/v8/V8BindingForTesting.h"
#include "core/dom/Document.h"
#include "core/frame/LocalFrameView.h"
#include "core/frame/Settings.h"
#include "core/html/HTMLCanvasElement.h"
#include "core/html/ImageData.h"
#include "core/imagebitmap/ImageBitmap.h"
#include "core/imagebitmap/ImageBitmapOptions.h"
#include "core/layout/LayoutBoxModelObject.h"
#include "core/loader/EmptyClients.h"
#include "core/paint/PaintLayer.h"
#include "core/testing/DummyPageHolder.h"
#include "modules/canvas2d/CanvasGradient.h"
#include "modules/canvas2d/CanvasPattern.h"
#include "modules/webgl/WebGLRenderingContext.h"
#include "platform/graphics/Canvas2DImageBufferSurface.h"
#include "platform/graphics/CanvasHeuristicParameters.h"
#include "platform/graphics/RecordingImageBufferSurface.h"
#include "platform/graphics/StaticBitmapImage.h"
#include "platform/graphics/UnacceleratedImageBufferSurface.h"
#include "platform/graphics/test/FakeGLES2Interface.h"
#include "platform/graphics/test/FakeWebGraphicsContext3DProvider.h"
#include "platform/loader/fetch/MemoryCache.h"
#include "platform/testing/TestingPlatformSupport.h"
#include "platform/wtf/PtrUtil.h"
#include "public/platform/scheduler/test/renderer_scheduler_test_support.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkColorSpaceXform.h"
#include "third_party/skia/include/core/SkImage.h"
#include "third_party/skia/include/core/SkSurface.h"
#include "third_party/skia/include/core/SkSwizzle.h"
using ::testing::Mock;
namespace blink {
enum BitmapOpacity { kOpaqueBitmap, kTransparentBitmap };
class FakeImageSource : public CanvasImageSource {
public:
FakeImageSource(IntSize, BitmapOpacity);
PassRefPtr<Image> GetSourceImageForCanvas(SourceImageStatus*,
AccelerationHint,
SnapshotReason,
const FloatSize&) override;
bool WouldTaintOrigin(
SecurityOrigin* destination_security_origin) const override {
return false;
}
FloatSize ElementSize(const FloatSize&) const override {
return FloatSize(size_);
}
bool IsOpaque() const override { return is_opaque_; }
bool IsAccelerated() const { return false; }
int SourceWidth() override { return size_.Width(); }
int SourceHeight() override { return size_.Height(); }
~FakeImageSource() override {}
private:
IntSize size_;
RefPtr<Image> image_;
bool is_opaque_;
};
FakeImageSource::FakeImageSource(IntSize size, BitmapOpacity opacity)
: size_(size), is_opaque_(opacity == kOpaqueBitmap) {
sk_sp<SkSurface> surface(
SkSurface::MakeRasterN32Premul(size_.Width(), size_.Height()));
surface->getCanvas()->clear(opacity == kOpaqueBitmap ? SK_ColorWHITE
: SK_ColorTRANSPARENT);
image_ = StaticBitmapImage::Create(surface->makeImageSnapshot());
}
PassRefPtr<Image> FakeImageSource::GetSourceImageForCanvas(
SourceImageStatus* status,
AccelerationHint,
SnapshotReason,
const FloatSize&) {
if (status)
*status = kNormalSourceImageStatus;
return image_;
}
//============================================================================
enum LinearPixelMathState { kLinearPixelMathDisabled, kLinearPixelMathEnabled };
class CanvasRenderingContext2DTest : public ::testing::Test {
protected:
CanvasRenderingContext2DTest();
void SetUp() override;
DummyPageHolder& Page() const { return *dummy_page_holder_; }
Document& GetDocument() const { return *document_; }
HTMLCanvasElement& CanvasElement() const { return *canvas_element_; }
CanvasRenderingContext2D* Context2d() const {
return static_cast<CanvasRenderingContext2D*>(
CanvasElement().RenderingContext());
}
intptr_t GetGlobalGPUMemoryUsage() const {
return ImageBuffer::GetGlobalGPUMemoryUsage();
}
unsigned GetGlobalAcceleratedImageBufferCount() const {
return ImageBuffer::GetGlobalAcceleratedImageBufferCount();
}
intptr_t GetCurrentGPUMemoryUsage() const {
return CanvasElement().GetImageBuffer()->GetGPUMemoryUsage();
}
void CreateContext(OpacityMode,
String color_space = String(),
LinearPixelMathState = kLinearPixelMathDisabled);
ScriptState* GetScriptState() {
return ToScriptStateForMainWorld(canvas_element_->GetFrame());
}
void TearDown();
void UnrefCanvas();
PassRefPtr<Canvas2DLayerBridge> MakeBridge(
std::unique_ptr<FakeWebGraphicsContext3DProvider>,
const IntSize&,
Canvas2DLayerBridge::AccelerationMode);
private:
std::unique_ptr<DummyPageHolder> dummy_page_holder_;
Persistent<Document> document_;
Persistent<HTMLCanvasElement> canvas_element_;
Persistent<MemoryCache> global_memory_cache_;
class WrapGradients final : public GarbageCollectedFinalized<WrapGradients> {
public:
static WrapGradients* Create() { return new WrapGradients; }
DEFINE_INLINE_TRACE() {
visitor->Trace(opaque_gradient_);
visitor->Trace(alpha_gradient_);
}
StringOrCanvasGradientOrCanvasPattern opaque_gradient_;
StringOrCanvasGradientOrCanvasPattern alpha_gradient_;
};
// TODO(Oilpan): avoid tedious part-object wrapper by supporting on-heap
// ::testing::Tests.
Persistent<WrapGradients> wrap_gradients_;
protected:
// Pre-canned objects for testing
Persistent<ImageData> full_image_data_;
Persistent<ImageData> partial_image_data_;
FakeImageSource opaque_bitmap_;
FakeImageSource alpha_bitmap_;
// Set this to override frame settings.
FrameSettingOverrideFunction override_settings_function_ = nullptr;
StringOrCanvasGradientOrCanvasPattern& OpaqueGradient() {
return wrap_gradients_->opaque_gradient_;
}
StringOrCanvasGradientOrCanvasPattern& AlphaGradient() {
return wrap_gradients_->alpha_gradient_;
}
};
CanvasRenderingContext2DTest::CanvasRenderingContext2DTest()
: wrap_gradients_(WrapGradients::Create()),
opaque_bitmap_(IntSize(10, 10), kOpaqueBitmap),
alpha_bitmap_(IntSize(10, 10), kTransparentBitmap) {}
void CanvasRenderingContext2DTest::CreateContext(
OpacityMode opacity_mode,
String color_space,
LinearPixelMathState linear_pixel_math_state) {
String canvas_type("2d");
CanvasContextCreationAttributes attributes;
attributes.setAlpha(opacity_mode == kNonOpaque);
if (!color_space.IsEmpty()) {
attributes.setColorSpace(color_space);
if (linear_pixel_math_state == kLinearPixelMathEnabled) {
attributes.setPixelFormat("float16");
attributes.setLinearPixelMath(true);
}
}
canvas_element_->GetCanvasRenderingContext(canvas_type, attributes);
}
void CanvasRenderingContext2DTest::SetUp() {
Page::PageClients page_clients;
FillWithEmptyClients(page_clients);
dummy_page_holder_ = DummyPageHolder::Create(
IntSize(800, 600), &page_clients, nullptr, override_settings_function_);
document_ = &dummy_page_holder_->GetDocument();
document_->documentElement()->setInnerHTML(
"<body><canvas id='c'></canvas></body>");
document_->View()->UpdateAllLifecyclePhases();
canvas_element_ = toHTMLCanvasElement(document_->getElementById("c"));
full_image_data_ = ImageData::Create(IntSize(10, 10));
partial_image_data_ = ImageData::Create(IntSize(2, 2));
NonThrowableExceptionState exception_state;
CanvasGradient* opaque_gradient =
CanvasGradient::Create(FloatPoint(0, 0), FloatPoint(10, 0));
opaque_gradient->addColorStop(0, String("green"), exception_state);
EXPECT_FALSE(exception_state.HadException());
opaque_gradient->addColorStop(1, String("blue"), exception_state);
EXPECT_FALSE(exception_state.HadException());
this->OpaqueGradient().setCanvasGradient(opaque_gradient);
CanvasGradient* alpha_gradient =
CanvasGradient::Create(FloatPoint(0, 0), FloatPoint(10, 0));
alpha_gradient->addColorStop(0, String("green"), exception_state);
EXPECT_FALSE(exception_state.HadException());
alpha_gradient->addColorStop(1, String("rgba(0, 0, 255, 0.5)"),
exception_state);
EXPECT_FALSE(exception_state.HadException());
StringOrCanvasGradientOrCanvasPattern wrapped_alpha_gradient;
this->AlphaGradient().setCanvasGradient(alpha_gradient);
global_memory_cache_ = ReplaceMemoryCacheForTesting(MemoryCache::Create());
}
void CanvasRenderingContext2DTest::TearDown() {
ThreadState::Current()->CollectGarbage(BlinkGC::kNoHeapPointersOnStack,
BlinkGC::kGCWithSweep,
BlinkGC::kForcedGC);
ReplaceMemoryCacheForTesting(global_memory_cache_.Release());
}
PassRefPtr<Canvas2DLayerBridge> CanvasRenderingContext2DTest::MakeBridge(
std::unique_ptr<FakeWebGraphicsContext3DProvider> provider,
const IntSize& size,
Canvas2DLayerBridge::AccelerationMode acceleration_mode) {
return AdoptRef(new Canvas2DLayerBridge(std::move(provider), size, 0,
kNonOpaque, acceleration_mode,
CanvasColorParams()));
}
//============================================================================
class FakeAcceleratedImageBufferSurface
: public UnacceleratedImageBufferSurface {
public:
FakeAcceleratedImageBufferSurface(const IntSize& size, OpacityMode mode)
: UnacceleratedImageBufferSurface(size, mode), is_accelerated_(true) {}
~FakeAcceleratedImageBufferSurface() override {}
bool IsAccelerated() const override { return is_accelerated_; }
void SetIsAccelerated(bool is_accelerated) {
if (is_accelerated != is_accelerated_)
is_accelerated_ = is_accelerated;
}
private:
bool is_accelerated_;
};
//============================================================================
class MockImageBufferSurfaceForOverwriteTesting
: public UnacceleratedImageBufferSurface {
public:
MockImageBufferSurfaceForOverwriteTesting(const IntSize& size,
OpacityMode mode)
: UnacceleratedImageBufferSurface(size, mode) {}
~MockImageBufferSurfaceForOverwriteTesting() override {}
bool IsRecording() const override {
return true;
} // otherwise overwrites are not tracked
MOCK_METHOD0(WillOverwriteCanvas, void());
};
//============================================================================
#define TEST_OVERDRAW_SETUP(EXPECTED_OVERDRAWS) \
std::unique_ptr<MockImageBufferSurfaceForOverwriteTesting> mock_surface = \
WTF::MakeUnique<MockImageBufferSurfaceForOverwriteTesting>( \
IntSize(10, 10), kNonOpaque); \
MockImageBufferSurfaceForOverwriteTesting* surface_ptr = mock_surface.get(); \
CanvasElement().CreateImageBufferUsingSurfaceForTesting( \
std::move(mock_surface)); \
EXPECT_CALL(*surface_ptr, WillOverwriteCanvas()).Times(EXPECTED_OVERDRAWS); \
Context2d()->save();
#define TEST_OVERDRAW_FINALIZE \
Context2d()->restore(); \
Mock::VerifyAndClearExpectations(surface_ptr);
#define TEST_OVERDRAW_1(EXPECTED_OVERDRAWS, CALL1) \
do { \
TEST_OVERDRAW_SETUP(EXPECTED_OVERDRAWS) \
Context2d()->CALL1; \
TEST_OVERDRAW_FINALIZE \
} while (0)
#define TEST_OVERDRAW_2(EXPECTED_OVERDRAWS, CALL1, CALL2) \
do { \
TEST_OVERDRAW_SETUP(EXPECTED_OVERDRAWS) \
Context2d()->CALL1; \
Context2d()->CALL2; \
TEST_OVERDRAW_FINALIZE \
} while (0)
#define TEST_OVERDRAW_3(EXPECTED_OVERDRAWS, CALL1, CALL2, CALL3) \
do { \
TEST_OVERDRAW_SETUP(EXPECTED_OVERDRAWS) \
Context2d()->CALL1; \
Context2d()->CALL2; \
Context2d()->CALL3; \
TEST_OVERDRAW_FINALIZE \
} while (0)
#define TEST_OVERDRAW_4(EXPECTED_OVERDRAWS, CALL1, CALL2, CALL3, CALL4) \
do { \
TEST_OVERDRAW_SETUP(EXPECTED_OVERDRAWS) \
Context2d()->CALL1; \
Context2d()->CALL2; \
Context2d()->CALL3; \
Context2d()->CALL4; \
TEST_OVERDRAW_FINALIZE \
} while (0)
//============================================================================
TEST_F(CanvasRenderingContext2DTest, detectOverdrawWithFillRect) {
CreateContext(kNonOpaque);
TEST_OVERDRAW_1(1, fillRect(-1, -1, 12, 12));
TEST_OVERDRAW_1(1, fillRect(0, 0, 10, 10));
TEST_OVERDRAW_1(
0, strokeRect(0, 0, 10,
10)); // stroking instead of filling does not overwrite
TEST_OVERDRAW_2(0, setGlobalAlpha(0.5f), fillRect(0, 0, 10, 10));
TEST_OVERDRAW_1(0, fillRect(0, 0, 9, 9));
TEST_OVERDRAW_2(0, translate(1, 1), fillRect(0, 0, 10, 10));
TEST_OVERDRAW_2(1, translate(1, 1), fillRect(-1, -1, 10, 10));
TEST_OVERDRAW_2(1, setFillStyle(OpaqueGradient()), fillRect(0, 0, 10, 10));
TEST_OVERDRAW_2(0, setFillStyle(AlphaGradient()), fillRect(0, 0, 10, 10));
TEST_OVERDRAW_3(0, setGlobalAlpha(0.5), setFillStyle(OpaqueGradient()),
fillRect(0, 0, 10, 10));
TEST_OVERDRAW_3(1, setGlobalAlpha(0.5f),
setGlobalCompositeOperation(String("copy")),
fillRect(0, 0, 10, 10));
TEST_OVERDRAW_2(1, setGlobalCompositeOperation(String("copy")),
fillRect(0, 0, 9, 9));
TEST_OVERDRAW_3(0, rect(0, 0, 5, 5), clip(), fillRect(0, 0, 10, 10));
TEST_OVERDRAW_4(0, rect(0, 0, 5, 5), clip(),
setGlobalCompositeOperation(String("copy")),
fillRect(0, 0, 10, 10));
}
TEST_F(CanvasRenderingContext2DTest, detectOverdrawWithClearRect) {
CreateContext(kNonOpaque);
TEST_OVERDRAW_1(1, clearRect(0, 0, 10, 10));
TEST_OVERDRAW_1(0, clearRect(0, 0, 9, 9));
TEST_OVERDRAW_2(1, setGlobalAlpha(0.5f), clearRect(0, 0, 10, 10));
TEST_OVERDRAW_2(1, setFillStyle(AlphaGradient()), clearRect(0, 0, 10, 10));
TEST_OVERDRAW_2(0, translate(1, 1), clearRect(0, 0, 10, 10));
TEST_OVERDRAW_2(1, translate(1, 1), clearRect(-1, -1, 10, 10));
TEST_OVERDRAW_2(1, setGlobalCompositeOperation(String("destination-in")),
clearRect(0, 0, 10, 10)); // composite op ignored
TEST_OVERDRAW_3(0, rect(0, 0, 5, 5), clip(), clearRect(0, 0, 10, 10));
}
TEST_F(CanvasRenderingContext2DTest, detectOverdrawWithDrawImage) {
CreateContext(kNonOpaque);
NonThrowableExceptionState exception_state;
TEST_OVERDRAW_1(1, drawImage(GetScriptState(), &opaque_bitmap_, 0, 0, 10, 10,
0, 0, 10, 10, exception_state));
EXPECT_FALSE(exception_state.HadException());
TEST_OVERDRAW_1(1, drawImage(GetScriptState(), &opaque_bitmap_, 0, 0, 1, 1, 0,
0, 10, 10, exception_state));
EXPECT_FALSE(exception_state.HadException());
TEST_OVERDRAW_2(0, setGlobalAlpha(0.5f),
drawImage(GetScriptState(), &opaque_bitmap_, 0, 0, 10, 10, 0,
0, 10, 10, exception_state));
EXPECT_FALSE(exception_state.HadException());
TEST_OVERDRAW_1(0, drawImage(GetScriptState(), &alpha_bitmap_, 0, 0, 10, 10,
0, 0, 10, 10, exception_state));
EXPECT_FALSE(exception_state.HadException());
TEST_OVERDRAW_2(0, setGlobalAlpha(0.5f),
drawImage(GetScriptState(), &alpha_bitmap_, 0, 0, 10, 10, 0,
0, 10, 10, exception_state));
EXPECT_FALSE(exception_state.HadException());
TEST_OVERDRAW_1(0, drawImage(GetScriptState(), &opaque_bitmap_, 0, 0, 10, 10,
1, 0, 10, 10, exception_state));
EXPECT_FALSE(exception_state.HadException());
TEST_OVERDRAW_1(0, drawImage(GetScriptState(), &opaque_bitmap_, 0, 0, 10, 10,
0, 0, 9, 9, exception_state));
EXPECT_FALSE(exception_state.HadException());
TEST_OVERDRAW_1(1, drawImage(GetScriptState(), &opaque_bitmap_, 0, 0, 10, 10,
0, 0, 11, 11, exception_state));
EXPECT_FALSE(exception_state.HadException());
TEST_OVERDRAW_2(1, translate(-1, 0),
drawImage(GetScriptState(), &opaque_bitmap_, 0, 0, 10, 10, 1,
0, 10, 10, exception_state));
EXPECT_FALSE(exception_state.HadException());
TEST_OVERDRAW_2(0, translate(-1, 0),
drawImage(GetScriptState(), &opaque_bitmap_, 0, 0, 10, 10, 0,
0, 10, 10, exception_state));
EXPECT_FALSE(exception_state.HadException());
TEST_OVERDRAW_2(
0, setFillStyle(OpaqueGradient()),
drawImage(GetScriptState(), &alpha_bitmap_, 0, 0, 10, 10, 0, 0, 10, 10,
exception_state)); // fillStyle ignored by drawImage
EXPECT_FALSE(exception_state.HadException());
TEST_OVERDRAW_2(
1, setFillStyle(AlphaGradient()),
drawImage(GetScriptState(), &opaque_bitmap_, 0, 0, 10, 10, 0, 0, 10, 10,
exception_state)); // fillStyle ignored by drawImage
EXPECT_FALSE(exception_state.HadException());
TEST_OVERDRAW_2(1, setGlobalCompositeOperation(String("copy")),
drawImage(GetScriptState(), &opaque_bitmap_, 0, 0, 10, 10, 1,
0, 10, 10, exception_state));
EXPECT_FALSE(exception_state.HadException());
TEST_OVERDRAW_3(0, rect(0, 0, 5, 5), clip(),
drawImage(GetScriptState(), &opaque_bitmap_, 0, 0, 10, 10, 0,
0, 10, 10, exception_state));
EXPECT_FALSE(exception_state.HadException());
}
TEST_F(CanvasRenderingContext2DTest, detectOverdrawWithPutImageData) {
CreateContext(kNonOpaque);
NonThrowableExceptionState exception_state;
// Test putImageData
TEST_OVERDRAW_1(1,
putImageData(full_image_data_.Get(), 0, 0, exception_state));
EXPECT_FALSE(exception_state.HadException());
TEST_OVERDRAW_1(1, putImageData(full_image_data_.Get(), 0, 0, 0, 0, 10, 10,
exception_state));
EXPECT_FALSE(exception_state.HadException());
TEST_OVERDRAW_1(0, putImageData(full_image_data_.Get(), 0, 0, 1, 1, 8, 8,
exception_state));
EXPECT_FALSE(exception_state.HadException());
TEST_OVERDRAW_2(1, setGlobalAlpha(0.5f),
putImageData(full_image_data_.Get(), 0, 0,
exception_state)); // alpha has no effect
EXPECT_FALSE(exception_state.HadException());
TEST_OVERDRAW_1(
0, putImageData(partial_image_data_.Get(), 0, 0, exception_state));
EXPECT_FALSE(exception_state.HadException());
TEST_OVERDRAW_2(1, translate(1, 1),
putImageData(full_image_data_.Get(), 0, 0,
exception_state)); // ignores tranforms
EXPECT_FALSE(exception_state.HadException());
TEST_OVERDRAW_1(0,
putImageData(full_image_data_.Get(), 1, 0, exception_state));
EXPECT_FALSE(exception_state.HadException());
TEST_OVERDRAW_3(1, rect(0, 0, 5, 5), clip(),
putImageData(full_image_data_.Get(), 0, 0,
exception_state)); // ignores clip
EXPECT_FALSE(exception_state.HadException());
}
TEST_F(CanvasRenderingContext2DTest, detectOverdrawWithCompositeOperations) {
CreateContext(kNonOpaque);
// Test composite operators with an opaque rect that covers the entire canvas
// Note: all the untested composite operations take the same code path as
// source-in, which assumes that the destination may not be overwritten
TEST_OVERDRAW_2(1, setGlobalCompositeOperation(String("clear")),
fillRect(0, 0, 10, 10));
TEST_OVERDRAW_2(1, setGlobalCompositeOperation(String("copy")),
fillRect(0, 0, 10, 10));
TEST_OVERDRAW_2(1, setGlobalCompositeOperation(String("source-over")),
fillRect(0, 0, 10, 10));
TEST_OVERDRAW_2(0, setGlobalCompositeOperation(String("source-in")),
fillRect(0, 0, 10, 10));
// Test composite operators with a transparent rect that covers the entire
// canvas
TEST_OVERDRAW_3(1, setGlobalAlpha(0.5f),
setGlobalCompositeOperation(String("clear")),
fillRect(0, 0, 10, 10));
TEST_OVERDRAW_3(1, setGlobalAlpha(0.5f),
setGlobalCompositeOperation(String("copy")),
fillRect(0, 0, 10, 10));
TEST_OVERDRAW_3(0, setGlobalAlpha(0.5f),
setGlobalCompositeOperation(String("source-over")),
fillRect(0, 0, 10, 10));
TEST_OVERDRAW_3(0, setGlobalAlpha(0.5f),
setGlobalCompositeOperation(String("source-in")),
fillRect(0, 0, 10, 10));
// Test composite operators with an opaque rect that does not cover the entire
// canvas
TEST_OVERDRAW_2(0, setGlobalCompositeOperation(String("clear")),
fillRect(0, 0, 5, 5));
TEST_OVERDRAW_2(1, setGlobalCompositeOperation(String("copy")),
fillRect(0, 0, 5, 5));
TEST_OVERDRAW_2(0, setGlobalCompositeOperation(String("source-over")),
fillRect(0, 0, 5, 5));
TEST_OVERDRAW_2(0, setGlobalCompositeOperation(String("source-in")),
fillRect(0, 0, 5, 5));
}
TEST_F(CanvasRenderingContext2DTest, NoLayerPromotionByDefault) {
CreateContext(kNonOpaque);
auto surface = WTF::MakeUnique<RecordingImageBufferSurface>(
IntSize(10, 10), RecordingImageBufferSurface::kAllowFallback, kNonOpaque);
auto* surface_ptr = surface.get();
CanvasElement().CreateImageBufferUsingSurfaceForTesting(std::move(surface));
EXPECT_FALSE(CanvasElement().ShouldBeDirectComposited());
EXPECT_TRUE(surface_ptr->IsRecording());
}
TEST_F(CanvasRenderingContext2DTest, NoLayerPromotionUnderOverdrawLimit) {
CreateContext(kNonOpaque);
auto surface = WTF::MakeUnique<RecordingImageBufferSurface>(
IntSize(10, 10), RecordingImageBufferSurface::kAllowFallback, kNonOpaque);
auto* surface_ptr = surface.get();
CanvasElement().CreateImageBufferUsingSurfaceForTesting(std::move(surface));
Context2d()->setGlobalAlpha(0.5f); // To prevent overdraw optimization
for (int i = 0;
i < CanvasHeuristicParameters::kExpensiveOverdrawThreshold - 1; i++) {
Context2d()->fillRect(0, 0, 10, 10);
}
EXPECT_FALSE(CanvasElement().ShouldBeDirectComposited());
EXPECT_TRUE(surface_ptr->IsRecording());
}
TEST_F(CanvasRenderingContext2DTest, LayerPromotionOverOverdrawLimit) {
CreateContext(kNonOpaque);
auto surface = WTF::MakeUnique<RecordingImageBufferSurface>(
IntSize(10, 10), RecordingImageBufferSurface::kAllowFallback, kNonOpaque);
auto* surface_ptr = surface.get();
CanvasElement().CreateImageBufferUsingSurfaceForTesting(std::move(surface));
Context2d()->setGlobalAlpha(0.5f); // To prevent overdraw optimization
for (int i = 0; i < CanvasHeuristicParameters::kExpensiveOverdrawThreshold;
i++) {
Context2d()->fillRect(0, 0, 10, 10);
}
EXPECT_TRUE(CanvasElement().ShouldBeDirectComposited());
EXPECT_TRUE(surface_ptr->IsRecording());
}
TEST_F(CanvasRenderingContext2DTest, NoLayerPromotionUnderImageSizeRatioLimit) {
CreateContext(kNonOpaque);
auto surface = WTF::MakeUnique<RecordingImageBufferSurface>(
IntSize(10, 10), RecordingImageBufferSurface::kAllowFallback, kNonOpaque);
auto* surface_ptr = surface.get();
CanvasElement().CreateImageBufferUsingSurfaceForTesting(std::move(surface));
NonThrowableExceptionState exception_state;
Element* source_canvas_element =
GetDocument().createElement("canvas", exception_state);
EXPECT_FALSE(exception_state.HadException());
HTMLCanvasElement* source_canvas =
static_cast<HTMLCanvasElement*>(source_canvas_element);
IntSize source_size(10,
10 * CanvasHeuristicParameters::kExpensiveImageSizeRatio);
std::unique_ptr<UnacceleratedImageBufferSurface> source_surface =
WTF::MakeUnique<UnacceleratedImageBufferSurface>(source_size, kNonOpaque);
source_canvas->CreateImageBufferUsingSurfaceForTesting(
std::move(source_surface));
const ImageBitmapOptions default_options;
Optional<IntRect> crop_rect = IntRect(IntPoint(0, 0), source_size);
// Go through an ImageBitmap to avoid triggering a display list fallback
ImageBitmap* source_image_bitmap =
ImageBitmap::Create(source_canvas, crop_rect, default_options);
Context2d()->drawImage(GetScriptState(), source_image_bitmap, 0, 0, 1, 1, 0,
0, 1, 1, exception_state);
EXPECT_FALSE(exception_state.HadException());
EXPECT_FALSE(CanvasElement().ShouldBeDirectComposited());
EXPECT_TRUE(surface_ptr->IsRecording());
}
TEST_F(CanvasRenderingContext2DTest, LayerPromotionOverImageSizeRatioLimit) {
CreateContext(kNonOpaque);
auto surface = WTF::MakeUnique<RecordingImageBufferSurface>(
IntSize(10, 10), RecordingImageBufferSurface::kAllowFallback, kNonOpaque);
auto* surface_ptr = surface.get();
CanvasElement().CreateImageBufferUsingSurfaceForTesting(std::move(surface));
NonThrowableExceptionState exception_state;
Element* source_canvas_element =
GetDocument().createElement("canvas", exception_state);
EXPECT_FALSE(exception_state.HadException());
HTMLCanvasElement* source_canvas =
static_cast<HTMLCanvasElement*>(source_canvas_element);
IntSize source_size(
10, 10 * CanvasHeuristicParameters::kExpensiveImageSizeRatio + 1);
std::unique_ptr<UnacceleratedImageBufferSurface> source_surface =
WTF::MakeUnique<UnacceleratedImageBufferSurface>(source_size, kNonOpaque);
source_canvas->CreateImageBufferUsingSurfaceForTesting(
std::move(source_surface));
const ImageBitmapOptions default_options;
Optional<IntRect> crop_rect = IntRect(IntPoint(0, 0), source_size);
// Go through an ImageBitmap to avoid triggering a display list fallback
ImageBitmap* source_image_bitmap =
ImageBitmap::Create(source_canvas, crop_rect, default_options);
Context2d()->drawImage(GetScriptState(), source_image_bitmap, 0, 0, 1, 1, 0,
0, 1, 1, exception_state);
EXPECT_FALSE(exception_state.HadException());
EXPECT_TRUE(CanvasElement().ShouldBeDirectComposited());
EXPECT_TRUE(surface_ptr->IsRecording());
}
TEST_F(CanvasRenderingContext2DTest,
NoLayerPromotionUnderExpensivePathPointCount) {
CreateContext(kNonOpaque);
auto surface = WTF::MakeUnique<RecordingImageBufferSurface>(
IntSize(10, 10), RecordingImageBufferSurface::kAllowFallback, kNonOpaque);
auto* surface_ptr = surface.get();
CanvasElement().CreateImageBufferUsingSurfaceForTesting(std::move(surface));
Context2d()->beginPath();
Context2d()->moveTo(7, 5);
for (int i = 1; i < CanvasHeuristicParameters::kExpensivePathPointCount - 1;
i++) {
float angle_rad = twoPiFloat * i /
(CanvasHeuristicParameters::kExpensivePathPointCount - 1);
Context2d()->lineTo(5 + 2 * cos(angle_rad), 5 + 2 * sin(angle_rad));
}
Context2d()->fill();
EXPECT_FALSE(CanvasElement().ShouldBeDirectComposited());
EXPECT_TRUE(surface_ptr->IsRecording());
}
TEST_F(CanvasRenderingContext2DTest,
LayerPromotionOverExpensivePathPointCount) {
CreateContext(kNonOpaque);
auto surface = WTF::MakeUnique<RecordingImageBufferSurface>(
IntSize(10, 10), RecordingImageBufferSurface::kAllowFallback, kNonOpaque);
auto* surface_ptr = surface.get();
CanvasElement().CreateImageBufferUsingSurfaceForTesting(std::move(surface));
Context2d()->beginPath();
Context2d()->moveTo(7, 5);
for (int i = 1; i < CanvasHeuristicParameters::kExpensivePathPointCount + 1;
i++) {
float angle_rad = twoPiFloat * i /
(CanvasHeuristicParameters::kExpensivePathPointCount + 1);
Context2d()->lineTo(5 + 2 * cos(angle_rad), 5 + 2 * sin(angle_rad));
}
Context2d()->fill();
EXPECT_TRUE(CanvasElement().ShouldBeDirectComposited());
EXPECT_TRUE(surface_ptr->IsRecording());
}
TEST_F(CanvasRenderingContext2DTest, LayerPromotionWhenPathIsConcave) {
CreateContext(kNonOpaque);
auto surface = WTF::MakeUnique<RecordingImageBufferSurface>(
IntSize(10, 10), RecordingImageBufferSurface::kAllowFallback, kNonOpaque);
auto* surface_ptr = surface.get();
CanvasElement().CreateImageBufferUsingSurfaceForTesting(std::move(surface));
Context2d()->beginPath();
Context2d()->moveTo(1, 1);
Context2d()->lineTo(5, 5);
Context2d()->lineTo(9, 1);
Context2d()->lineTo(5, 9);
Context2d()->fill();
if (CanvasHeuristicParameters::kConcavePathsAreExpensive) {
EXPECT_TRUE(CanvasElement().ShouldBeDirectComposited());
} else {
EXPECT_FALSE(CanvasElement().ShouldBeDirectComposited());
}
EXPECT_TRUE(surface_ptr->IsRecording());
}
TEST_F(CanvasRenderingContext2DTest, NoLayerPromotionWithRectangleClip) {
CreateContext(kNonOpaque);
auto surface = WTF::MakeUnique<RecordingImageBufferSurface>(
IntSize(10, 10), RecordingImageBufferSurface::kAllowFallback, kNonOpaque);
auto* surface_ptr = surface.get();
CanvasElement().CreateImageBufferUsingSurfaceForTesting(std::move(surface));
Context2d()->beginPath();
Context2d()->rect(1, 1, 2, 2);
Context2d()->clip();
Context2d()->fillRect(0, 0, 4, 4);
EXPECT_FALSE(CanvasElement().ShouldBeDirectComposited());
EXPECT_TRUE(surface_ptr->IsRecording());
}
TEST_F(CanvasRenderingContext2DTest, LayerPromotionWithComplexClip) {
CreateContext(kNonOpaque);
auto surface = WTF::MakeUnique<RecordingImageBufferSurface>(
IntSize(10, 10), RecordingImageBufferSurface::kAllowFallback, kNonOpaque);
auto* surface_ptr = surface.get();
CanvasElement().CreateImageBufferUsingSurfaceForTesting(std::move(surface));
Context2d()->beginPath();
Context2d()->moveTo(1, 1);
Context2d()->lineTo(5, 5);
Context2d()->lineTo(9, 1);
Context2d()->lineTo(5, 9);
Context2d()->clip();
Context2d()->fillRect(0, 0, 4, 4);
if (CanvasHeuristicParameters::kComplexClipsAreExpensive) {
EXPECT_TRUE(CanvasElement().ShouldBeDirectComposited());
} else {
EXPECT_FALSE(CanvasElement().ShouldBeDirectComposited());
}
EXPECT_TRUE(surface_ptr->IsRecording());
}
TEST_F(CanvasRenderingContext2DTest, LayerPromotionWithBlurredShadow) {
CreateContext(kNonOpaque);
auto surface = WTF::MakeUnique<RecordingImageBufferSurface>(
IntSize(10, 10), RecordingImageBufferSurface::kAllowFallback, kNonOpaque);
auto* surface_ptr = surface.get();
CanvasElement().CreateImageBufferUsingSurfaceForTesting(std::move(surface));
Context2d()->setShadowColor(String("red"));
Context2d()->setShadowBlur(1.0f);
Context2d()->fillRect(1, 1, 1, 1);
if (CanvasHeuristicParameters::kBlurredShadowsAreExpensive) {
EXPECT_TRUE(CanvasElement().ShouldBeDirectComposited());
} else {
EXPECT_FALSE(CanvasElement().ShouldBeDirectComposited());
}
EXPECT_TRUE(surface_ptr->IsRecording());
}
TEST_F(CanvasRenderingContext2DTest, NoLayerPromotionWithSharpShadow) {
CreateContext(kNonOpaque);
auto surface = WTF::MakeUnique<RecordingImageBufferSurface>(
IntSize(10, 10), RecordingImageBufferSurface::kAllowFallback, kNonOpaque);
auto* surface_ptr = surface.get();
CanvasElement().CreateImageBufferUsingSurfaceForTesting(std::move(surface));
Context2d()->setShadowColor(String("red"));
Context2d()->setShadowOffsetX(1.0f);
Context2d()->fillRect(1, 1, 1, 1);
EXPECT_FALSE(CanvasElement().ShouldBeDirectComposited());
EXPECT_TRUE(surface_ptr->IsRecording());
}
TEST_F(CanvasRenderingContext2DTest, NoFallbackWithSmallState) {
CreateContext(kNonOpaque);
auto surface = WTF::MakeUnique<RecordingImageBufferSurface>(
IntSize(10, 10), RecordingImageBufferSurface::kAllowFallback, kNonOpaque);
auto* surface_ptr = surface.get();
CanvasElement().CreateImageBufferUsingSurfaceForTesting(std::move(surface));
Context2d()->fillRect(0, 0, 1, 1); // To have a non-empty dirty rect.
for (int i = 0;
i < CanvasHeuristicParameters::kExpensiveRecordingStackDepth - 1; ++i) {
Context2d()->save();
Context2d()->translate(1.0f, 0.0f);
}
CanvasElement().FinalizeFrame(); // To close the current frame.
EXPECT_TRUE(surface_ptr->IsRecording());
}
TEST_F(CanvasRenderingContext2DTest, FallbackWithLargeState) {
CreateContext(kNonOpaque);
auto surface = WTF::MakeUnique<RecordingImageBufferSurface>(
IntSize(10, 10), RecordingImageBufferSurface::kAllowFallback, kNonOpaque);
auto* surface_ptr = surface.get();
CanvasElement().CreateImageBufferUsingSurfaceForTesting(std::move(surface));
Context2d()->fillRect(0, 0, 1, 1); // To have a non-empty dirty rect.
for (int i = 0; i < CanvasHeuristicParameters::kExpensiveRecordingStackDepth;
++i) {
Context2d()->save();
Context2d()->translate(1.0f, 0.0f);
}
CanvasElement().FinalizeFrame(); // To close the current frame.
EXPECT_FALSE(surface_ptr->IsRecording());
}
TEST_F(CanvasRenderingContext2DTest, OpaqueDisplayListFallsBackForText) {
// Verify that drawing text to an opaque canvas, which is expected to
// render with subpixel text anti-aliasing, results in falling out
// of display list mode because the current diplay list implementation
// does not support pixel geometry settings.
// See: crbug.com/583809
CreateContext(kOpaque);
auto surface = WTF::MakeUnique<RecordingImageBufferSurface>(
IntSize(10, 10), RecordingImageBufferSurface::kAllowFallback, kOpaque);
auto* surface_ptr = surface.get();
CanvasElement().CreateImageBufferUsingSurfaceForTesting(std::move(surface));
Context2d()->fillText("Text", 0, 5);
EXPECT_FALSE(surface_ptr->IsRecording());
}
TEST_F(CanvasRenderingContext2DTest, ImageResourceLifetime) {
NonThrowableExceptionState non_throwable_exception_state;
Element* canvas_element =
GetDocument().createElement("canvas", non_throwable_exception_state);
EXPECT_FALSE(non_throwable_exception_state.HadException());
HTMLCanvasElement* canvas = static_cast<HTMLCanvasElement*>(canvas_element);
canvas->SetSize(IntSize(40, 40));
ImageBitmap* image_bitmap_derived = nullptr;
{
const ImageBitmapOptions default_options;
Optional<IntRect> crop_rect =
IntRect(0, 0, canvas->width(), canvas->height());
ImageBitmap* image_bitmap_from_canvas =
ImageBitmap::Create(canvas, crop_rect, default_options);
crop_rect = IntRect(0, 0, 20, 20);
image_bitmap_derived = ImageBitmap::Create(image_bitmap_from_canvas,
crop_rect, default_options);
}
CanvasContextCreationAttributes attributes;
CanvasRenderingContext2D* context = static_cast<CanvasRenderingContext2D*>(
canvas->GetCanvasRenderingContext("2d", attributes));
DummyExceptionStateForTesting exception_state;
CanvasImageSourceUnion image_source;
image_source.setImageBitmap(image_bitmap_derived);
context->drawImage(GetScriptState(), image_source, 0, 0, exception_state);
}
TEST_F(CanvasRenderingContext2DTest, GPUMemoryUpdateForAcceleratedCanvas) {
CreateContext(kNonOpaque);
std::unique_ptr<FakeAcceleratedImageBufferSurface> fake_accelerate_surface =
WTF::MakeUnique<FakeAcceleratedImageBufferSurface>(IntSize(10, 10),
kNonOpaque);
FakeAcceleratedImageBufferSurface* fake_accelerate_surface_ptr =
fake_accelerate_surface.get();
CanvasElement().CreateImageBufferUsingSurfaceForTesting(
std::move(fake_accelerate_surface));
// 800 = 10 * 10 * 4 * 2 where 10*10 is canvas size, 4 is num of bytes per
// pixel per buffer, and 2 is an estimate of num of gpu buffers required
EXPECT_EQ(800, GetCurrentGPUMemoryUsage());
EXPECT_EQ(800, GetGlobalGPUMemoryUsage());
EXPECT_EQ(1u, GetGlobalAcceleratedImageBufferCount());
// Switching accelerated mode to non-accelerated mode
fake_accelerate_surface_ptr->SetIsAccelerated(false);
CanvasElement().GetImageBuffer()->UpdateGPUMemoryUsage();
EXPECT_EQ(0, GetCurrentGPUMemoryUsage());
EXPECT_EQ(0, GetGlobalGPUMemoryUsage());
EXPECT_EQ(0u, GetGlobalAcceleratedImageBufferCount());
// Switching non-accelerated mode to accelerated mode
fake_accelerate_surface_ptr->SetIsAccelerated(true);
CanvasElement().GetImageBuffer()->UpdateGPUMemoryUsage();
EXPECT_EQ(800, GetCurrentGPUMemoryUsage());
EXPECT_EQ(800, GetGlobalGPUMemoryUsage());
EXPECT_EQ(1u, GetGlobalAcceleratedImageBufferCount());
// Creating a different accelerated image buffer
auto fake_accelerate_surface2 =
WTF::MakeUnique<FakeAcceleratedImageBufferSurface>(IntSize(10, 5),
kNonOpaque);
std::unique_ptr<ImageBuffer> image_buffer2 =
ImageBuffer::Create(std::move(fake_accelerate_surface2));
EXPECT_EQ(800, GetCurrentGPUMemoryUsage());
EXPECT_EQ(1200, GetGlobalGPUMemoryUsage());
EXPECT_EQ(2u, GetGlobalAcceleratedImageBufferCount());
// Tear down the first image buffer that resides in current canvas element
CanvasElement().SetSize(IntSize(20, 20));
Mock::VerifyAndClearExpectations(fake_accelerate_surface_ptr);
EXPECT_EQ(400, GetGlobalGPUMemoryUsage());
EXPECT_EQ(1u, GetGlobalAcceleratedImageBufferCount());
// Tear down the second image buffer
image_buffer2.reset();
EXPECT_EQ(0, GetGlobalGPUMemoryUsage());
EXPECT_EQ(0u, GetGlobalAcceleratedImageBufferCount());
}
TEST_F(CanvasRenderingContext2DTest, CanvasDisposedBeforeContext) {
CreateContext(kNonOpaque);
Context2d()->fillRect(0, 0, 1, 1); // results in task observer registration
Context2d()->DetachHost();
// This is the only method that is callable after DetachHost
// Test passes by not crashing.
Context2d()->DidProcessTask();
// Test passes by not crashing during teardown
}
TEST_F(CanvasRenderingContext2DTest, ContextDisposedBeforeCanvas) {
CreateContext(kNonOpaque);
CanvasElement().DetachContext();
// Passes by not crashing later during teardown
}
#if defined(MEMORY_SANITIZER)
#define MAYBE_GetImageDataDisablesAcceleration \
DISABLED_GetImageDataDisablesAcceleration
#else
#define MAYBE_GetImageDataDisablesAcceleration GetImageDataDisablesAcceleration
#endif
TEST_F(CanvasRenderingContext2DTest, MAYBE_GetImageDataDisablesAcceleration) {
bool saved_fixed_rendering_mode =
RuntimeEnabledFeatures::Canvas2dFixedRenderingModeEnabled();
RuntimeEnabledFeatures::SetCanvas2dFixedRenderingModeEnabled(false);
CreateContext(kNonOpaque);
FakeGLES2Interface gl;
std::unique_ptr<FakeWebGraphicsContext3DProvider> context_provider(
new FakeWebGraphicsContext3DProvider(&gl));
IntSize size(300, 300);
RefPtr<Canvas2DLayerBridge> bridge =
MakeBridge(std::move(context_provider), size,
Canvas2DLayerBridge::kEnableAcceleration);
std::unique_ptr<Canvas2DImageBufferSurface> surface(
new Canvas2DImageBufferSurface(bridge, size));
CanvasElement().CreateImageBufferUsingSurfaceForTesting(std::move(surface));
EXPECT_TRUE(CanvasElement().GetImageBuffer()->IsAccelerated());
EXPECT_EQ(1u, GetGlobalAcceleratedImageBufferCount());
EXPECT_EQ(720000, GetGlobalGPUMemoryUsage());
DummyExceptionStateForTesting exception_state;
for (int i = 0;
i < CanvasHeuristicParameters::kGPUReadbackMinSuccessiveFrames - 1;
i++) {
Context2d()->getImageData(0, 0, 1, 1, exception_state);
CanvasElement().FinalizeFrame();
EXPECT_FALSE(exception_state.HadException());
EXPECT_TRUE(CanvasElement().GetImageBuffer()->IsAccelerated());
EXPECT_EQ(1u, GetGlobalAcceleratedImageBufferCount());
EXPECT_EQ(720000, GetGlobalGPUMemoryUsage());
}
Context2d()->getImageData(0, 0, 1, 1, exception_state);
CanvasElement().FinalizeFrame();
EXPECT_FALSE(exception_state.HadException());
if (CanvasHeuristicParameters::kGPUReadbackForcesNoAcceleration) {
EXPECT_FALSE(CanvasElement().GetImageBuffer()->IsAccelerated());
EXPECT_EQ(0u, GetGlobalAcceleratedImageBufferCount());
EXPECT_EQ(0, GetGlobalGPUMemoryUsage());
} else {
EXPECT_TRUE(CanvasElement().GetImageBuffer()->IsAccelerated());
EXPECT_EQ(1u, GetGlobalAcceleratedImageBufferCount());
EXPECT_EQ(720000, GetGlobalGPUMemoryUsage());
}
// Restore global state to prevent side-effects on other tests
RuntimeEnabledFeatures::SetCanvas2dFixedRenderingModeEnabled(
saved_fixed_rendering_mode);
}
TEST_F(CanvasRenderingContext2DTest, TextureUploadHeuristics) {
bool saved_fixed_rendering_mode =
RuntimeEnabledFeatures::Canvas2dFixedRenderingModeEnabled();
RuntimeEnabledFeatures::SetCanvas2dFixedRenderingModeEnabled(false);
enum TestVariants {
kLargeTextureDisablesAcceleration = 0,
kSmallTextureDoesNotDisableAcceleration = 1,
kTestVariantCount = 2,
};
for (int test_variant = 0; test_variant < kTestVariantCount; test_variant++) {
int delta = test_variant == kLargeTextureDisablesAcceleration ? 1 : -1;
int src_size =
std::sqrt(static_cast<float>(
CanvasHeuristicParameters::kDrawImageTextureUploadSoftSizeLimit)) +
delta;
int dst_size =
src_size /
std::sqrt(static_cast<float>(
CanvasHeuristicParameters::
kDrawImageTextureUploadSoftSizeLimitScaleThreshold)) -
delta;
CreateContext(kNonOpaque);
FakeGLES2Interface gl;
std::unique_ptr<FakeWebGraphicsContext3DProvider> context_provider(
new FakeWebGraphicsContext3DProvider(&gl));
IntSize size(dst_size, dst_size);
RefPtr<Canvas2DLayerBridge> bridge =
MakeBridge(std::move(context_provider), size,
Canvas2DLayerBridge::kEnableAcceleration);
std::unique_ptr<Canvas2DImageBufferSurface> surface(
new Canvas2DImageBufferSurface(bridge, size));
CanvasElement().CreateImageBufferUsingSurfaceForTesting(std::move(surface));
EXPECT_TRUE(CanvasElement().GetImageBuffer()->IsAccelerated());
EXPECT_EQ(1u, GetGlobalAcceleratedImageBufferCount());
// 4 bytes per pixel * 2 buffers = 8
EXPECT_EQ(8 * dst_size * dst_size, GetGlobalGPUMemoryUsage());
sk_sp<SkSurface> sk_surface =
SkSurface::MakeRasterN32Premul(src_size, src_size);
RefPtr<StaticBitmapImage> big_bitmap =
StaticBitmapImage::Create(sk_surface->makeImageSnapshot());
ImageBitmap* big_image = ImageBitmap::Create(std::move(big_bitmap));
NonThrowableExceptionState exception_state;
V8TestingScope scope;
Context2d()->drawImage(scope.GetScriptState(), big_image, 0, 0, src_size,
src_size, 0, 0, dst_size, dst_size, exception_state);
EXPECT_FALSE(exception_state.HadException());
if (test_variant == kLargeTextureDisablesAcceleration) {
EXPECT_FALSE(CanvasElement().GetImageBuffer()->IsAccelerated());
EXPECT_EQ(0u, GetGlobalAcceleratedImageBufferCount());
EXPECT_EQ(0, GetGlobalGPUMemoryUsage());
} else {
EXPECT_TRUE(CanvasElement().GetImageBuffer()->IsAccelerated());
EXPECT_EQ(1u, GetGlobalAcceleratedImageBufferCount());
EXPECT_EQ(8 * dst_size * dst_size, GetGlobalGPUMemoryUsage());
}
}
// Restore global state to prevent side-effects on other tests
RuntimeEnabledFeatures::SetCanvas2dFixedRenderingModeEnabled(
saved_fixed_rendering_mode);
}
TEST_F(CanvasRenderingContext2DTest, DisableAcceleration) {
CreateContext(kNonOpaque);
auto fake_accelerate_surface =
WTF::MakeUnique<FakeAcceleratedImageBufferSurface>(IntSize(10, 10),
kNonOpaque);
CanvasElement().CreateImageBufferUsingSurfaceForTesting(
std::move(fake_accelerate_surface));
CanvasRenderingContext2D* context = Context2d();
// 800 = 10 * 10 * 4 * 2 where 10*10 is canvas size, 4 is num of bytes per
// pixel per buffer, and 2 is an estimate of num of gpu buffers required
EXPECT_EQ(800, GetCurrentGPUMemoryUsage());
EXPECT_EQ(800, GetGlobalGPUMemoryUsage());
EXPECT_EQ(1u, GetGlobalAcceleratedImageBufferCount());
context->fillRect(10, 10, 100, 100);
EXPECT_TRUE(CanvasElement().GetImageBuffer()->IsAccelerated());
CanvasElement().GetImageBuffer()->DisableAcceleration();
EXPECT_FALSE(CanvasElement().GetImageBuffer()->IsAccelerated());
context->fillRect(10, 10, 100, 100);
EXPECT_EQ(0, GetCurrentGPUMemoryUsage());
EXPECT_EQ(0, GetGlobalGPUMemoryUsage());
EXPECT_EQ(0u, GetGlobalAcceleratedImageBufferCount());
}
enum class ColorSpaceConversion : uint8_t {
NONE = 0,
DEFAULT_NOT_COLOR_CORRECTED = 1,
DEFAULT_COLOR_CORRECTED = 2,
SRGB = 3,
LINEAR_RGB = 4,
LAST = LINEAR_RGB
};
static ImageBitmapOptions PrepareBitmapOptionsAndSetRuntimeFlags(
const ColorSpaceConversion& color_space_conversion) {
// Set the color space conversion in ImageBitmapOptions
ImageBitmapOptions options;
static const Vector<String> kConversions = {"none", "default", "default",
"srgb", "linear-rgb"};
options.setColorSpaceConversion(
kConversions[static_cast<uint8_t>(color_space_conversion)]);
// Set the runtime flags
bool flag = (color_space_conversion !=
ColorSpaceConversion::DEFAULT_NOT_COLOR_CORRECTED);
RuntimeEnabledFeatures::SetExperimentalCanvasFeaturesEnabled(true);
RuntimeEnabledFeatures::SetColorCorrectRenderingEnabled(flag);
RuntimeEnabledFeatures::SetColorCanvasExtensionsEnabled(true);
return options;
}
TEST_F(CanvasRenderingContext2DTest, ImageBitmapColorSpaceConversion) {
bool experimental_canvas_features_runtime_flag =
RuntimeEnabledFeatures::ExperimentalCanvasFeaturesEnabled();
bool color_correct_rendering_runtime_flag =
RuntimeEnabledFeatures::ColorCorrectRenderingEnabled();
bool color_canvas_extensions_flag =
RuntimeEnabledFeatures::ColorCanvasExtensionsEnabled();
RuntimeEnabledFeatures::SetExperimentalCanvasFeaturesEnabled(true);
RuntimeEnabledFeatures::SetColorCorrectRenderingEnabled(true);
RuntimeEnabledFeatures::SetColorCanvasExtensionsEnabled(true);
Persistent<HTMLCanvasElement> canvas =
Persistent<HTMLCanvasElement>(CanvasElement());
CanvasContextCreationAttributes attributes;
attributes.setAlpha(true);
attributes.setColorSpace("srgb");
CanvasRenderingContext2D* context = static_cast<CanvasRenderingContext2D*>(
canvas->GetCanvasRenderingContext("2d", attributes));
StringOrCanvasGradientOrCanvasPattern fill_style;
fill_style.setString("#FF0000");
context->setFillStyle(fill_style);
context->fillRect(0, 0, 10, 10);
NonThrowableExceptionState exception_state;
uint8_t* src_pixel =
context->getImageData(5, 5, 1, 1, exception_state)->data()->Data();
// Swizzle if needed
if (kN32_SkColorType == kBGRA_8888_SkColorType)
std::swap(src_pixel[0], src_pixel[2]);
// Create and test the ImageBitmap objects.
Optional<IntRect> crop_rect = IntRect(0, 0, 300, 150);
sk_sp<SkColorSpace> color_space = nullptr;
SkColorType color_type = SkColorType::kN32_SkColorType;
SkColorSpaceXform::ColorFormat color_format32 =
(color_type == kBGRA_8888_SkColorType)
? SkColorSpaceXform::ColorFormat::kBGRA_8888_ColorFormat
: SkColorSpaceXform::ColorFormat::kRGBA_8888_ColorFormat;
SkColorSpaceXform::ColorFormat color_format = color_format32;
sk_sp<SkColorSpace> src_rgb_color_space = SkColorSpace::MakeSRGB();
for (uint8_t i = static_cast<uint8_t>(
ColorSpaceConversion::DEFAULT_NOT_COLOR_CORRECTED);
i <= static_cast<uint8_t>(ColorSpaceConversion::LAST); i++) {
ColorSpaceConversion color_space_conversion =
static_cast<ColorSpaceConversion>(i);
ImageBitmapOptions options =
PrepareBitmapOptionsAndSetRuntimeFlags(color_space_conversion);
ImageBitmap* image_bitmap = ImageBitmap::Create(canvas, crop_rect, options);
SkImage* converted_image =
image_bitmap->BitmapImage()->ImageForCurrentFrame().get();
switch (color_space_conversion) {
case ColorSpaceConversion::NONE:
NOTREACHED();
break;
case ColorSpaceConversion::DEFAULT_NOT_COLOR_CORRECTED:
color_space = ColorBehavior::GlobalTargetColorSpace().ToSkColorSpace();
if (color_space->gammaIsLinear()) {
color_type = SkColorType::kRGBA_F16_SkColorType;
color_format = SkColorSpaceXform::ColorFormat::kRGBA_F16_ColorFormat;
} else {
color_format = color_format32;
}
break;
case ColorSpaceConversion::DEFAULT_COLOR_CORRECTED:
case ColorSpaceConversion::SRGB:
color_space = SkColorSpace::MakeSRGB();
color_format = color_format32;
break;
case ColorSpaceConversion::LINEAR_RGB:
color_space = SkColorSpace::MakeSRGBLinear();
color_type = SkColorType::kRGBA_F16_SkColorType;
color_format = SkColorSpaceXform::ColorFormat::kRGBA_F16_ColorFormat;
break;
default:
NOTREACHED();
}
SkImageInfo image_info = SkImageInfo::Make(
1, 1, color_type, SkAlphaType::kPremul_SkAlphaType, color_space);
std::unique_ptr<uint8_t[]> converted_pixel(
new uint8_t[image_info.bytesPerPixel()]());
converted_image->readPixels(
image_info, converted_pixel.get(),
converted_image->width() * image_info.bytesPerPixel(), 5, 5);
// Transform the source pixel and check if the image bitmap color conversion
// is done correctly.
std::unique_ptr<SkColorSpaceXform> color_space_xform =
SkColorSpaceXform::New(src_rgb_color_space.get(), color_space.get());
std::unique_ptr<uint8_t[]> transformed_pixel(
new uint8_t[image_info.bytesPerPixel()]());
color_space_xform->apply(color_format, transformed_pixel.get(),
color_format32, src_pixel, 1,
SkAlphaType::kPremul_SkAlphaType);
int compare = std::memcmp(converted_pixel.get(), transformed_pixel.get(),
image_info.bytesPerPixel());
ASSERT_EQ(compare, 0);
}
RuntimeEnabledFeatures::SetExperimentalCanvasFeaturesEnabled(
experimental_canvas_features_runtime_flag);
RuntimeEnabledFeatures::SetColorCorrectRenderingEnabled(
color_correct_rendering_runtime_flag);
RuntimeEnabledFeatures::SetColorCanvasExtensionsEnabled(
color_canvas_extensions_flag);
}
bool ConvertPixelsToColorSpaceAndPixelFormatForTest(
DOMArrayBufferView* data_array,
CanvasColorSpace src_color_space,
CanvasColorSpace dst_color_space,
CanvasPixelFormat dst_pixel_format,
std::unique_ptr<uint8_t[]>& converted_pixels) {
// Setting SkColorSpaceXform::apply parameters
SkColorSpaceXform::ColorFormat src_color_format =
SkColorSpaceXform::kRGBA_8888_ColorFormat;
unsigned data_length = data_array->byteLength() / data_array->TypeSize();
unsigned num_pixels = data_length / 4;
DOMUint8ClampedArray* u8_array = nullptr;
DOMUint16Array* u16_array = nullptr;
DOMFloat32Array* f32_array = nullptr;
void* src_data = nullptr;
switch (data_array->GetType()) {
case ArrayBufferView::ViewType::kTypeUint8Clamped:
u8_array = const_cast<DOMUint8ClampedArray*>(
static_cast<const DOMUint8ClampedArray*>(data_array));
src_data = static_cast<void*>(u8_array->Data());
break;
case ArrayBufferView::ViewType::kTypeUint16:
u16_array = const_cast<DOMUint16Array*>(
static_cast<const DOMUint16Array*>(data_array));
src_color_format =
SkColorSpaceXform::ColorFormat::kRGBA_U16_BE_ColorFormat;
src_data = static_cast<void*>(u16_array->Data());
break;
case ArrayBufferView::ViewType::kTypeFloat32:
f32_array = const_cast<DOMFloat32Array*>(
static_cast<const DOMFloat32Array*>(data_array));
src_color_format = SkColorSpaceXform::kRGBA_F32_ColorFormat;
src_data = static_cast<void*>(f32_array->Data());
break;
default:
NOTREACHED();
return false;
}
SkColorSpaceXform::ColorFormat dst_color_format =
SkColorSpaceXform::ColorFormat::kRGBA_8888_ColorFormat;
if (dst_pixel_format == kF16CanvasPixelFormat)
dst_color_format = SkColorSpaceXform::ColorFormat::kRGBA_F32_ColorFormat;
sk_sp<SkColorSpace> src_sk_color_space = nullptr;
if (u8_array) {
src_sk_color_space =
CanvasColorParams(src_color_space, kRGBA8CanvasPixelFormat)
.GetSkColorSpaceForSkSurfaces();
} else {
src_sk_color_space =
CanvasColorParams(src_color_space, kF16CanvasPixelFormat)
.GetSkColorSpaceForSkSurfaces();
}
sk_sp<SkColorSpace> dst_sk_color_space =
CanvasColorParams(dst_color_space, dst_pixel_format)
.GetSkColorSpaceForSkSurfaces();
// When the input dataArray is in Uint16, we normally should convert the
// values from Little Endian to Big Endian before passing the buffer to
// SkColorSpaceXform::apply. However, in this test scenario we are creating
// the Uin16 dataArray by multiplying a Uint8Clamped array members by 257,
// hence the Big Endian and Little Endian representations are the same.
std::unique_ptr<SkColorSpaceXform> xform = SkColorSpaceXform::New(
src_sk_color_space.get(), dst_sk_color_space.get());
if (!xform->apply(dst_color_format, converted_pixels.get(), src_color_format,
src_data, num_pixels, kUnpremul_SkAlphaType))
return false;
return true;
}
// The color settings of the surface of the canvas always remaines loyal to the
// first created context 2D. Therefore, we have to test different canvas color
// space settings for CanvasRenderingContext2D::putImageData() in different
// tests.
enum class CanvasColorSpaceSettings : uint8_t {
CANVAS_SRGB = 0,
CANVAS_LINEARSRGB = 1,
CANVAS_REC2020 = 2,
CANVAS_P3 = 3,
LAST = CANVAS_P3
};
// This test verifies the correct behavior of putImageData member function in
// color managed mode.
void TestPutImageDataOnCanvasWithColorSpaceSettings(
HTMLCanvasElement& canvas_element,
CanvasColorSpaceSettings canvas_colorspace_setting,
float color_tolerance) {
bool experimental_canvas_features_runtime_flag =
RuntimeEnabledFeatures::ExperimentalCanvasFeaturesEnabled();
bool color_correct_rendering_runtime_flag =
RuntimeEnabledFeatures::ColorCorrectRenderingEnabled();
bool color_canvas_extensions_flag =
RuntimeEnabledFeatures::ColorCanvasExtensionsEnabled();
RuntimeEnabledFeatures::SetExperimentalCanvasFeaturesEnabled(true);
RuntimeEnabledFeatures::SetColorCorrectRenderingEnabled(true);
RuntimeEnabledFeatures::SetColorCanvasExtensionsEnabled(true);
bool test_passed = true;
unsigned num_image_data_color_spaces = 3;
CanvasColorSpace image_data_color_spaces[] = {
kSRGBCanvasColorSpace, kRec2020CanvasColorSpace, kP3CanvasColorSpace,
};
unsigned num_image_data_storage_formats = 3;
ImageDataStorageFormat image_data_storage_formats[] = {
kUint8ClampedArrayStorageFormat, kUint16ArrayStorageFormat,
kFloat32ArrayStorageFormat,
};
CanvasColorSpace canvas_color_spaces[] = {
kSRGBCanvasColorSpace, kSRGBCanvasColorSpace, kRec2020CanvasColorSpace,
kP3CanvasColorSpace,
};
String canvas_color_space_names[] = {
kSRGBCanvasColorSpaceName, kSRGBCanvasColorSpaceName,
kRec2020CanvasColorSpaceName, kP3CanvasColorSpaceName};
CanvasPixelFormat canvas_pixel_formats[] = {
kRGBA8CanvasPixelFormat, kF16CanvasPixelFormat, kF16CanvasPixelFormat,
kF16CanvasPixelFormat,
};
String canvas_pixel_format_names[] = {
kRGBA8CanvasPixelFormatName, kF16CanvasPixelFormatName,
kF16CanvasPixelFormatName, kF16CanvasPixelFormatName};
// Source pixels in RGBA32
uint8_t u8_pixels[] = {255, 0, 0, 255, // Red
0, 0, 0, 0, // Transparent
255, 192, 128, 64, // Decreasing values
93, 117, 205, 11}; // Random values
unsigned data_length = 16;
uint16_t* u16_pixels = new uint16_t[data_length];
for (unsigned i = 0; i < data_length; i++)
u16_pixels[i] = u8_pixels[i] * 257;
float* f32_pixels = new float[data_length];
for (unsigned i = 0; i < data_length; i++)
f32_pixels[i] = u8_pixels[i] / 255.0;
DOMArrayBufferView* data_array = nullptr;
DOMUint8ClampedArray* data_u8 =
DOMUint8ClampedArray::Create(u8_pixels, data_length);
DCHECK(data_u8);
EXPECT_EQ(data_length, data_u8->length());
DOMUint16Array* data_u16 = DOMUint16Array::Create(u16_pixels, data_length);
DCHECK(data_u16);
EXPECT_EQ(data_length, data_u16->length());
DOMFloat32Array* data_f32 = DOMFloat32Array::Create(f32_pixels, data_length);
DCHECK(data_f32);
EXPECT_EQ(data_length, data_f32->length());
ImageData* image_data = nullptr;
ImageDataColorSettings color_settings;
// At most four bytes are needed for Float32 output per color component.
std::unique_ptr<uint8_t[]> pixels_converted_manually(
new uint8_t[data_length * 4]());
// Loop through different possible combinations of image data color space and
// storage formats and create the respective test image data objects.
for (unsigned i = 0; i < num_image_data_color_spaces; i++) {
color_settings.setColorSpace(
ImageData::CanvasColorSpaceName(image_data_color_spaces[i]));
for (unsigned j = 0; j < num_image_data_storage_formats; j++) {
switch (image_data_storage_formats[j]) {
case kUint8ClampedArrayStorageFormat:
data_array = static_cast<DOMArrayBufferView*>(data_u8);
color_settings.setStorageFormat(kUint8ClampedArrayStorageFormatName);
break;
case kUint16ArrayStorageFormat:
data_array = static_cast<DOMArrayBufferView*>(data_u16);
color_settings.setStorageFormat(kUint16ArrayStorageFormatName);
break;
case kFloat32ArrayStorageFormat:
data_array = static_cast<DOMArrayBufferView*>(data_f32);
color_settings.setStorageFormat(kFloat32ArrayStorageFormatName);
break;
default:
NOTREACHED();
}
image_data =
ImageData::CreateForTest(IntSize(2, 2), data_array, &color_settings);
unsigned k = (unsigned)(canvas_colorspace_setting);
// Convert the original data used to create ImageData to the
// canvas color space and canvas pixel format.
EXPECT_TRUE(ConvertPixelsToColorSpaceAndPixelFormatForTest(
data_array, image_data_color_spaces[i], canvas_color_spaces[k],
canvas_pixel_formats[k], pixels_converted_manually));
// Create a canvas and call putImageData and getImageData to make sure
// the conversion is done correctly.
CanvasContextCreationAttributes attributes;
attributes.setAlpha(true);
attributes.setColorSpace(canvas_color_space_names[k]);
attributes.setPixelFormat(canvas_pixel_format_names[k]);
CanvasRenderingContext2D* context =
static_cast<CanvasRenderingContext2D*>(
canvas_element.GetCanvasRenderingContext("2d", attributes));
NonThrowableExceptionState exception_state;
context->putImageData(image_data, 0, 0, exception_state);
void* pixels_from_get_image_data = nullptr;
if (canvas_pixel_formats[k] == kRGBA8CanvasPixelFormat) {
pixels_from_get_image_data =
context->getImageData(0, 0, 2, 2, exception_state)->data()->Data();
// Swizzle if needed
if (kN32_SkColorType == kBGRA_8888_SkColorType) {
SkSwapRB(static_cast<uint32_t*>(pixels_from_get_image_data),
static_cast<uint32_t*>(pixels_from_get_image_data),
data_length / 4);
}
unsigned char* cpixels1 =
static_cast<unsigned char*>(pixels_converted_manually.get());
unsigned char* cpixels2 =
static_cast<unsigned char*>(pixels_from_get_image_data);
for (unsigned m = 0; m < data_length; m++) {
if (abs(cpixels1[m] - cpixels2[m]) > color_tolerance)
test_passed = false;
}
} else {
pixels_from_get_image_data =
context->getImageData(0, 0, 2, 2, exception_state)
->dataUnion()
.getAsFloat32Array()
.View()
->Data();
float* fpixels1 = nullptr;
float* fpixels2 = nullptr;
void* vpointer = pixels_converted_manually.get();
fpixels1 = static_cast<float*>(vpointer);
fpixels2 = static_cast<float*>(pixels_from_get_image_data);
for (unsigned m = 0; m < data_length; m++) {
if (fabs(fpixels1[m] - fpixels2[m]) > color_tolerance) {
test_passed = false;
}
}
ASSERT_TRUE(test_passed);
}
}
}
delete[] u16_pixels;
delete[] f32_pixels;
RuntimeEnabledFeatures::SetExperimentalCanvasFeaturesEnabled(
experimental_canvas_features_runtime_flag);
RuntimeEnabledFeatures::SetColorCorrectRenderingEnabled(
color_correct_rendering_runtime_flag);
RuntimeEnabledFeatures::SetColorCanvasExtensionsEnabled(
color_canvas_extensions_flag);
}
TEST_F(CanvasRenderingContext2DTest, ColorManagedPutImageDataOnSRGBCanvas) {
TestPutImageDataOnCanvasWithColorSpaceSettings(
CanvasElement(), CanvasColorSpaceSettings::CANVAS_SRGB, 0);
}
TEST_F(CanvasRenderingContext2DTest,
ColorManagedPutImageDataOnLinearSRGBCanvas) {
TestPutImageDataOnCanvasWithColorSpaceSettings(
CanvasElement(), CanvasColorSpaceSettings::CANVAS_LINEARSRGB, 0.01);
}
TEST_F(CanvasRenderingContext2DTest, ColorManagedPutImageDataOnRec2020Canvas) {
TestPutImageDataOnCanvasWithColorSpaceSettings(
CanvasElement(), CanvasColorSpaceSettings::CANVAS_REC2020, 0.01);
}
TEST_F(CanvasRenderingContext2DTest, ColorManagedPutImageDataOnP3Canvas) {
TestPutImageDataOnCanvasWithColorSpaceSettings(
CanvasElement(), CanvasColorSpaceSettings::CANVAS_P3, 0.01);
}
void OverrideScriptEnabled(Settings& settings) {
// Simulate that we allow scripts, so that HTMLCanvasElement uses
// LayoutHTMLCanvas.
settings.SetScriptEnabled(true);
}
class CanvasRenderingContext2DTestWithTestingPlatform
: public CanvasRenderingContext2DTest {
protected:
void SetUp() override {
platform_ = WTF::MakeUnique<ScopedTestingPlatformSupport<
TestingPlatformSupportWithMockScheduler>>();
override_settings_function_ = &OverrideScriptEnabled;
(*platform_)
->AdvanceClockSeconds(1.); // For non-zero DocumentParserTimings.
CanvasRenderingContext2DTest::SetUp();
GetDocument().View()->UpdateLayout();
}
void TearDown() override {
platform_.reset();
CanvasRenderingContext2DTest::TearDown();
}
void RunUntilIdle() { (*platform_)->RunUntilIdle(); }
std::unique_ptr<
ScopedTestingPlatformSupport<TestingPlatformSupportWithMockScheduler>>
platform_;
};
// https://crbug.com/708445: When the Canvas2DLayerBridge hibernates or wakes up
// from hibernation, the compositing reasons for the canvas element may change.
// In these cases, the element should request a compositing update.
TEST_F(CanvasRenderingContext2DTestWithTestingPlatform,
ElementRequestsCompositingUpdateOnHibernateAndWakeUp) {
CreateContext(kNonOpaque);
FakeGLES2Interface gl;
std::unique_ptr<FakeWebGraphicsContext3DProvider> context_provider(
new FakeWebGraphicsContext3DProvider(&gl));
IntSize size(300, 300);
RefPtr<Canvas2DLayerBridge> bridge =
MakeBridge(std::move(context_provider), size,
Canvas2DLayerBridge::kEnableAcceleration);
// Force hibernatation to occur in an immediate task.
bridge->DontUseIdleSchedulingForTesting();
std::unique_ptr<Canvas2DImageBufferSurface> surface(
new Canvas2DImageBufferSurface(bridge, size));
CanvasElement().CreateImageBufferUsingSurfaceForTesting(std::move(surface));
EXPECT_TRUE(CanvasElement().GetImageBuffer()->IsAccelerated());
EXPECT_TRUE(CanvasElement().GetLayoutBoxModelObject());
PaintLayer* layer = CanvasElement().GetLayoutBoxModelObject()->Layer();
EXPECT_TRUE(layer);
GetDocument().View()->UpdateAllLifecyclePhases();
// Hide element to trigger hibernation (if enabled).
GetDocument().GetPage()->SetVisibilityState(kPageVisibilityStateHidden,
false);
RunUntilIdle(); // Run hibernation task.
// If enabled, hibernation should cause compositing update.
EXPECT_EQ(!!CANVAS2D_HIBERNATION_ENABLED,
layer->NeedsCompositingInputsUpdate());
GetDocument().View()->UpdateAllLifecyclePhases();
EXPECT_FALSE(layer->NeedsCompositingInputsUpdate());
// Wake up again, which should request a compositing update synchronously.
GetDocument().GetPage()->SetVisibilityState(kPageVisibilityStateVisible,
false);
EXPECT_EQ(!!CANVAS2D_HIBERNATION_ENABLED,
layer->NeedsCompositingInputsUpdate());
RunUntilIdle(); // Clear task queue.
}
} // namespace blink