Skip to content
Permalink
Browse files
OffscreenCanvas should use the same buffer size checks as HTMLCanvasE…
…lement.

https://bugs.webkit.org/show_bug.cgi?id=249630
<rdar://103177223>

Reviewed by Dean Jackson.

HTMLCanvasElement has a lot of checks to determine if the specified size is within an acceptable range, whereas OffscreenCanvas will just allocate giant buffers if requested.
This moves the HTMLCanvasElement implementation of createImageBuffer into CanvasBase, so that it can be used by OffscreenCanvas and have consistent behaviour.

* Source/WebCore/html/CanvasBase.cpp:
(WebCore::CanvasBase::maxActivePixelMemory):
(WebCore::CanvasBase::setMaxPixelMemoryForTesting):
(WebCore::maxCanvasArea):
(WebCore::CanvasBase::setMaxCanvasAreaForTesting):
(WebCore::CanvasBase::graphicsClient const):
(WebCore::CanvasBase::shouldAccelerate const):
(WebCore::CanvasBase::createImageBuffer const):
* Source/WebCore/html/CanvasBase.h:
* Source/WebCore/html/HTMLCanvasElement.cpp:
(WebCore::HTMLCanvasElement::createImageBuffer const):
(WebCore::HTMLCanvasElement::maxActivePixelMemory): Deleted.
(WebCore::HTMLCanvasElement::setMaxPixelMemoryForTesting): Deleted.
(WebCore::maxCanvasArea): Deleted.
(WebCore::HTMLCanvasElement::setMaxCanvasAreaForTesting): Deleted.
(WebCore::HTMLCanvasElement::shouldAccelerate const): Deleted.
* Source/WebCore/html/HTMLCanvasElement.h:
* Source/WebCore/html/OffscreenCanvas.cpp:
(WebCore::OffscreenCanvas::createImageBuffer const):
* Source/WebCore/html/canvas/WebGLRenderingContextBase.cpp:
(WebCore::WebGLRenderingContextBase::create):
(WebCore::WebGLRenderingContextBase::maybeRestoreContext):
(WebCore::getGraphicsClient): Deleted.
* Source/WebCore/testing/Internals.cpp:
(WebCore::Internals::resetToConsistentState):
(WebCore::Internals::setMaxCanvasPixelMemory):
(WebCore::Internals::setMaxCanvasArea):
(WebCore::Internals::avoidIOSurfaceSizeCheckInWebProcess):

Canonical link: https://commits.webkit.org/258237@main
  • Loading branch information
mattwoodrow committed Dec 22, 2022
1 parent 9ced63b commit d21c61e18c6ffba8cae41d4e35630351c4d1945a
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 142 deletions.
@@ -1253,6 +1253,7 @@ http/tests/websocket/tests/hybi/imported/blink/permessage-deflate-window-bits.ht
http/wpt/offscreen-canvas/transferToImageBitmap-webgl.html [ ImageOnlyFailure Failure ]
imported/w3c/web-platform-tests/html/canvas/offscreen/manual/the-offscreen-canvas/offscreencanvas.commit.html [ Failure Pass ]
imported/w3c/web-platform-tests/html/canvas/offscreen/manual/the-offscreen-canvas/offscreencanvas.resize.html [ Failure Pass ]
imported/w3c/web-platform-tests/html/canvas/offscreen/the-offscreen-canvas/size.large.html [ Skip ]

# Tests a setting behind the ENABLE_FULLSCREEN_API flag, which is WK2-only
media/video-supports-fullscreen.html [ Skip ]
@@ -27,13 +27,20 @@
#include "CanvasBase.h"

#include "CanvasRenderingContext.h"
#include "Chrome.h"
#include "Document.h"
#include "Element.h"
#include "FloatRect.h"
#include "GraphicsClient.h"
#include "GraphicsContext.h"
#include "HTMLCanvasElement.h"
#include "HostWindow.h"
#include "ImageBuffer.h"
#include "InspectorInstrumentation.h"
#include "StyleCanvasImage.h"
#include "WebCoreOpaqueRoot.h"
#include "WorkerClient.h"
#include "WorkerGlobalScope.h"
#include <JavaScriptCore/JSCInlines.h>
#include <JavaScriptCore/JSLock.h>
#include <atomic>
@@ -51,6 +58,9 @@ const InterpolationQuality defaultInterpolationQuality = InterpolationQuality::L
const InterpolationQuality defaultInterpolationQuality = InterpolationQuality::Default;
#endif

static std::optional<size_t> maxCanvasAreaForTesting;
static std::optional<size_t> maxActivePixelMemoryForTesting;

CanvasBase::CanvasBase(IntSize size)
: m_size(size)
{
@@ -121,6 +131,49 @@ size_t CanvasBase::externalMemoryCost() const
return m_imageBuffer->externalMemoryCost();
}

size_t CanvasBase::maxActivePixelMemory()
{
if (maxActivePixelMemoryForTesting)
return *maxActivePixelMemoryForTesting;

static size_t maxPixelMemory;
static std::once_flag onceFlag;
std::call_once(onceFlag, [] {
#if PLATFORM(IOS_FAMILY)
maxPixelMemory = ramSize() / 4;
#else
maxPixelMemory = std::max(ramSize() / 4, 2151 * MB);
#endif
});

return maxPixelMemory;
}

void CanvasBase::setMaxPixelMemoryForTesting(std::optional<size_t> size)
{
maxActivePixelMemoryForTesting = size;
}

static inline size_t maxCanvasArea()
{
if (maxCanvasAreaForTesting)
return *maxCanvasAreaForTesting;

// Firefox limits width/height to 32767 pixels, but slows down dramatically before it
// reaches that limit. We limit by area instead, giving us larger maximum dimensions,
// in exchange for a smaller maximum canvas size. The maximum canvas size is in device pixels.
#if PLATFORM(IOS_FAMILY)
return 4096 * 4096;
#else
return 16384 * 16384;
#endif
}

void CanvasBase::setMaxCanvasAreaForTesting(std::optional<size_t> size)
{
maxCanvasAreaForTesting = size;
}

void CanvasBase::addObserver(CanvasObserver& observer)
{
m_observers.add(observer);
@@ -231,6 +284,77 @@ RefPtr<ImageBuffer> CanvasBase::setImageBuffer(RefPtr<ImageBuffer>&& buffer) con
return returnBuffer;
}

GraphicsClient* CanvasBase::graphicsClient() const
{
if (scriptExecutionContext()->isDocument() && downcast<Document>(scriptExecutionContext())->page())
return &downcast<Document>(scriptExecutionContext())->page()->chrome();
if (is<WorkerGlobalScope>(scriptExecutionContext()))
return downcast<WorkerGlobalScope>(scriptExecutionContext())->workerClient();

return nullptr;
}

bool CanvasBase::shouldAccelerate(const IntSize& size) const
{
auto checkedArea = size.area<RecordOverflow>();
if (checkedArea.hasOverflowed())
return false;

return shouldAccelerate(checkedArea.value());
}

bool CanvasBase::shouldAccelerate(unsigned area) const
{
if (area > scriptExecutionContext()->settingsValues().maximumAccelerated2dCanvasSize)
return false;

#if USE(IOSURFACE_CANVAS_BACKING_STORE)
return scriptExecutionContext()->settingsValues().canvasUsesAcceleratedDrawing;
#else
return false;
#endif
}

void CanvasBase::createImageBuffer(bool usesDisplayListDrawing, bool avoidBackendSizeCheckForTesting) const
{
auto checkedArea = size().area<RecordOverflow>();

if (checkedArea.hasOverflowed() || checkedArea > maxCanvasArea()) {
auto message = makeString("Canvas area exceeds the maximum limit (width * height > ", maxCanvasArea(), ").");
scriptExecutionContext()->addConsoleMessage(MessageSource::JS, MessageLevel::Warning, message);
return;
}

// Make sure we don't use more pixel memory than the system can support.
auto checkedRequestedPixelMemory = (4 * checkedArea) + activePixelMemory();
if (checkedRequestedPixelMemory.hasOverflowed() || checkedRequestedPixelMemory > maxActivePixelMemory()) {
auto message = makeString("Total canvas memory use exceeds the maximum limit (", maxActivePixelMemory() / 1024 / 1024, " MB).");
scriptExecutionContext()->addConsoleMessage(MessageSource::JS, MessageLevel::Warning, message);
return;
}

unsigned area = checkedArea.value();
if (!area)
return;

OptionSet<ImageBufferOptions> bufferOptions;
if (shouldAccelerate(area))
bufferOptions.add(ImageBufferOptions::Accelerated);
// FIXME: Add a new setting for DisplayList drawing on canvas.
if (usesDisplayListDrawing || scriptExecutionContext()->settingsValues().displayListDrawingEnabled)
bufferOptions.add(ImageBufferOptions::UseDisplayList);

auto [colorSpace, pixelFormat] = [&] {
if (renderingContext())
return std::pair { renderingContext()->colorSpace(), renderingContext()->pixelFormat() };
return std::pair { DestinationColorSpace::SRGB(), PixelFormat::BGRA8 };
}();
ImageBufferCreationContext context = { };
context.graphicsClient = graphicsClient();
context.avoidIOSurfaceSizeCheckInWebProcessForTesting = avoidBackendSizeCheckForTesting;
setImageBuffer(ImageBuffer::create(size(), RenderingPurpose::Canvas, 1, colorSpace, pixelFormat, bufferOptions, context));
}

size_t CanvasBase::activePixelMemory()
{
return s_activePixelMemory.load();
@@ -36,6 +36,7 @@ class AffineTransform;
class CanvasBase;
class CanvasRenderingContext;
class Element;
class GraphicsClient;
class GraphicsContext;
class GraphicsContextStateSaver;
class Image;
@@ -113,13 +114,22 @@ class CanvasBase {
virtual GraphicsContext* drawingContext() const;
virtual GraphicsContext* existingDrawingContext() const;

GraphicsClient* graphicsClient() const;

virtual void didDraw(const std::optional<FloatRect>&) = 0;

virtual Image* copiedImage() const = 0;
virtual void clearCopiedImage() const = 0;

bool hasActiveInspectorCanvasCallTracer() const;

bool shouldAccelerate(const IntSize&) const;
bool shouldAccelerate(unsigned area) const;

WEBCORE_EXPORT static size_t maxActivePixelMemory();
WEBCORE_EXPORT static void setMaxPixelMemoryForTesting(std::optional<size_t>);
WEBCORE_EXPORT static void setMaxCanvasAreaForTesting(std::optional<size_t>);

protected:
explicit CanvasBase(IntSize);

@@ -133,6 +143,8 @@ class CanvasBase {

void resetGraphicsContextState() const;

void createImageBuffer(bool usesDisplayListDrawing, bool avoidBackendSizeCheckForTesting) const;

private:
virtual void createImageBuffer() const { }

@@ -118,9 +118,6 @@ using namespace HTMLNames;
const int defaultWidth = 300;
const int defaultHeight = 150;

static std::optional<size_t> maxCanvasAreaForTesting;
static std::optional<size_t> maxActivePixelMemoryForTesting;

HTMLCanvasElement::HTMLCanvasElement(const QualifiedName& tagName, Document& document)
: HTMLElement(tagName, document)
, CanvasBase(IntSize(defaultWidth, defaultHeight))
@@ -225,49 +222,6 @@ void HTMLCanvasElement::setSize(const IntSize& newSize)
reset();
}

size_t HTMLCanvasElement::maxActivePixelMemory()
{
if (maxActivePixelMemoryForTesting)
return *maxActivePixelMemoryForTesting;

static size_t maxPixelMemory;
static std::once_flag onceFlag;
std::call_once(onceFlag, [] {
#if PLATFORM(IOS_FAMILY)
maxPixelMemory = ramSize() / 4;
#else
maxPixelMemory = std::max(ramSize() / 4, 2151 * MB);
#endif
});

return maxPixelMemory;
}

void HTMLCanvasElement::setMaxPixelMemoryForTesting(std::optional<size_t> size)
{
maxActivePixelMemoryForTesting = size;
}

static inline size_t maxCanvasArea()
{
if (maxCanvasAreaForTesting)
return *maxCanvasAreaForTesting;

// Firefox limits width/height to 32767 pixels, but slows down dramatically before it
// reaches that limit. We limit by area instead, giving us larger maximum dimensions,
// in exchange for a smaller maximum canvas size. The maximum canvas size is in device pixels.
#if PLATFORM(IOS_FAMILY)
return 4096 * 4096;
#else
return 16384 * 16384;
#endif
}

void HTMLCanvasElement::setMaxCanvasAreaForTesting(std::optional<size_t> size)
{
maxCanvasAreaForTesting = size;
}

ExceptionOr<std::optional<RenderingContext>> HTMLCanvasElement::getContext(JSC::JSGlobalObject& state, const String& contextId, FixedVector<JSC::Strong<JSC::Unknown>>&& arguments)
{
if (m_context) {
@@ -945,29 +899,6 @@ SecurityOrigin* HTMLCanvasElement::securityOrigin() const
return &document().securityOrigin();
}

bool HTMLCanvasElement::shouldAccelerate(const IntSize& size) const
{
auto checkedArea = size.area<RecordOverflow>();
if (checkedArea.hasOverflowed())
return false;

return shouldAccelerate(checkedArea.value());
}

bool HTMLCanvasElement::shouldAccelerate(unsigned area) const
{
auto& settings = document().settings();

if (area > settings.maximumAccelerated2dCanvasSize())
return false;

#if USE(IOSURFACE_CANVAS_BACKING_STORE)
return settings.canvasUsesAcceleratedDrawing();
#else
return false;
#endif
}

void HTMLCanvasElement::setUsesDisplayListDrawing(bool usesDisplayListDrawing)
{
m_usesDisplayListDrawing = usesDisplayListDrawing;
@@ -984,45 +915,7 @@ void HTMLCanvasElement::createImageBuffer() const

m_hasCreatedImageBuffer = true;
m_didClearImageBuffer = true;

auto checkedArea = size().area<RecordOverflow>();

if (checkedArea.hasOverflowed() || checkedArea > maxCanvasArea()) {
auto message = makeString("Canvas area exceeds the maximum limit (width * height > ", maxCanvasArea(), ").");
document().addConsoleMessage(MessageSource::JS, MessageLevel::Warning, message);
return;
}

// Make sure we don't use more pixel memory than the system can support.
auto checkedRequestedPixelMemory = (4 * checkedArea) + activePixelMemory();
if (checkedRequestedPixelMemory.hasOverflowed() || checkedRequestedPixelMemory > maxActivePixelMemory()) {
auto message = makeString("Total canvas memory use exceeds the maximum limit (", maxActivePixelMemory() / 1024 / 1024, " MB).");
document().addConsoleMessage(MessageSource::JS, MessageLevel::Warning, message);
return;
}

unsigned area = checkedArea.value();
if (!area)
return;

auto hostWindow = (document().view() && document().view()->root()) ? document().view()->root()->hostWindow() : nullptr;

OptionSet<ImageBufferOptions> bufferOptions;
if (shouldAccelerate(area))
bufferOptions.add(ImageBufferOptions::Accelerated);
// FIXME: Add a new setting for DisplayList drawing on canvas.
if (m_usesDisplayListDrawing.value_or(document().settings().displayListDrawingEnabled()))
bufferOptions.add(ImageBufferOptions::UseDisplayList);

auto [colorSpace, pixelFormat] = [&] {
if (m_context)
return std::pair { m_context->colorSpace(), m_context->pixelFormat() };
return std::pair { DestinationColorSpace::SRGB(), PixelFormat::BGRA8 };
}();
ImageBufferCreationContext context = { };
context.graphicsClient = hostWindow;
context.avoidIOSurfaceSizeCheckInWebProcessForTesting = m_avoidBackendSizeCheckForTesting;
setImageBuffer(ImageBuffer::create(size(), RenderingPurpose::Canvas, 1, colorSpace, pixelFormat, bufferOptions, context));
CanvasBase::createImageBuffer(m_usesDisplayListDrawing.value_or(false), m_avoidBackendSizeCheckForTesting);

#if USE(IOSURFACE_CANVAS_BACKING_STORE)
if (m_context && m_context->is2d()) {
@@ -126,18 +126,12 @@ class HTMLCanvasElement final : public HTMLElement, public CanvasBase, public Ac

SecurityOrigin* securityOrigin() const final;

bool shouldAccelerate(const IntSize&) const;
bool shouldAccelerate(unsigned area) const;

WEBCORE_EXPORT void setUsesDisplayListDrawing(bool);

// FIXME: Only some canvas rendering contexts need an ImageBuffer.
// It would be better to have the contexts own the buffers.
void setImageBufferAndMarkDirty(RefPtr<ImageBuffer>&&) final;

WEBCORE_EXPORT static void setMaxPixelMemoryForTesting(std::optional<size_t>);
WEBCORE_EXPORT static void setMaxCanvasAreaForTesting(std::optional<size_t>);

bool needsPreparationForDisplay();
void prepareForDisplay();

@@ -146,8 +140,6 @@ class HTMLCanvasElement final : public HTMLElement, public CanvasBase, public Ac

bool isControlledByOffscreen() const;

WEBCORE_EXPORT static size_t maxActivePixelMemory();

#if PLATFORM(COCOA)
GraphicsContext* drawingContext() const final;
#endif
@@ -538,12 +538,7 @@ CSSValuePool& OffscreenCanvas::cssValuePool()
void OffscreenCanvas::createImageBuffer() const
{
m_hasCreatedImageBuffer = true;

if (!width() || !height())
return;

auto colorSpace = m_context ? m_context->colorSpace() : DestinationColorSpace::SRGB();
setImageBuffer(ImageBitmap::createImageBuffer(*canvasBaseScriptExecutionContext(), size(), colorSpace));
CanvasBase::createImageBuffer(false, false);
}

void OffscreenCanvas::setImageBufferAndMarkDirty(RefPtr<ImageBuffer>&& buffer)

0 comments on commit d21c61e

Please sign in to comment.