Commit 1d585b90 authored by Kenneth Russell's avatar Kenneth Russell Committed by Commit Bot

Reland: Allow OffscreenCanvas to access the high-performance GPU.

(Relanding with new tests skipped on Mac NVIDIA Retina, which forces
the integrated GPU due to graphics driver bugs.)

Instead of only checking whether the containing frame is same-origin
with respect to the top-level document during
transferControlToOffscreen, check in the OffscreenCanvas constructor,
both for the main thread as well as dedicated workers. Dedicated workers
are same-origin per specification, so always grant access to the
discrete GPU in this scenario.

Add new pixel tests that verify WebGL-rendered OffscreenCanvas into an
ImageBitmapRenderingContext (i.e., where the OffscreenCanvas was not
originally tied to a canvas element). On dual-GPU macOS devices, also
verify the active state of the low-power and high-performance GPUs.

Simplify some of the tests most recently written for low/high-power GPU
switching.

Bug: 1048892
Change-Id: I20b7150029fa83d7fe066b8347bf150a0ed45d21
Tbr: kainino@chromium.org
Tbr: fserb@chromium.org
Tbr: jdarpinian@chromium.org
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2068645Reviewed-by: default avatarKenneth Russell <kbr@chromium.org>
Reviewed-by: default avatarJames Darpinian <jdarpinian@chromium.org>
Commit-Queue: Kenneth Russell <kbr@chromium.org>
Cr-Commit-Position: refs/heads/master@{#743686}
parent fe291bfe
// Copyright 2020 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.
let oc;
let gl;
const sz = 256;
let canvas;
let ibrc;
let workerMode = false;
let worker;
//---------------------------------------------------------------------------
// Functions to be called either on the main thread or worker
function setupOffscreenCanvas(highPerformance) {
oc = new OffscreenCanvas(sz, sz);
let attribs = {};
if (highPerformance)
attribs['powerPreference'] = 'high-performance';
gl = oc.getContext('webgl', attribs);
}
function doRender() {
gl.clearColor(0.0, 1.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
let ib = oc.transferToImageBitmap();
if (workerMode) {
self.postMessage({frame: ib}, [ib]);
} else {
ibrc.transferFromImageBitmap(ib);
}
}
//---------------------------------------------------------------------------
// Functions to be called only on the main thread
function initialize(useWorker, selfURL) {
canvas = document.createElement('canvas');
canvas.style = 'position: absolute; top: 0px; left: 0px;';
canvas.width = 256;
canvas.height = 256;
document.body.appendChild(canvas);
ibrc = canvas.getContext('bitmaprenderer');
workerMode = useWorker;
if (workerMode) {
worker = new Worker(selfURL);
worker.onmessage = function(e) {
ibrc.transferFromImageBitmap(e.data.frame);
waitForFinish();
}
}
sendResult("READY");
}
function setup(highPerformance) {
if (workerMode) {
worker.postMessage({command: 'setup', highPerformance: highPerformance});
} else {
setupOffscreenCanvas(highPerformance);
}
}
function render() {
if (workerMode) {
worker.postMessage({command: 'render'});
} else {
doRender();
waitForFinish();
}
}
function logOutput(s) {
if (window.domAutomationController)
window.domAutomationController.log(s);
else
console.log(s);
}
function sendResult(status, detail) {
logOutput(status + ' ' + detail);
if (window.domAutomationController) {
window.domAutomationController.send(status);
}
}
function waitForFinish()
{
let numFramesBeforeEnd = 15;
function waitForFinishImpl() {
if (--numFramesBeforeEnd == 0) {
sendResult("SUCCESS", "Test complete");
} else {
window.requestAnimationFrame(waitForFinishImpl);
}
}
window.requestAnimationFrame(waitForFinishImpl);
}
//---------------------------------------------------------------------------
// Code to be executed only on the worker
if (this.document === undefined) {
workerMode = true;
// We're on a worker - set up postMessage handler.
self.onmessage = function(e) {
switch (e.data.command) {
case 'setup':
setupOffscreenCanvas(e.data.highPerformance);
break;
case 'render':
doRender();
break;
}
}
}
<!DOCTYPE HTML>
<html>
<head>
<meta name="viewport" content="initial-scale=1">
<title>WebGL OffscreenCanvas to ImageBitmapRenderingContext on main thread test</title>
<style type="text/css">
.nomargin {
margin: 0px auto;
}
</style>
<script src="pixel_offscreen_canvas_ibrc_test.js"></script>
</head>
<body onload="initialize(false, 'pixel_offscreen_canvas_ibrc_test.js')">
</div>
</body>
</html>
<!DOCTYPE HTML>
<html>
<head>
<meta name="viewport" content="initial-scale=1">
<title>WebGL OffscreenCanvas to ImageBitmapRenderingContext on worker thread test</title>
<style type="text/css">
.nomargin {
margin: 0px auto;
}
</style>
<script src="pixel_offscreen_canvas_ibrc_test.js"></script>
</head>
<body onload="initialize(true, 'pixel_offscreen_canvas_ibrc_test.js')">
</div>
</body>
</html>
......@@ -24,19 +24,12 @@ function initialize(runningOnDualGPUMacBookPro) {
}
function notifyHarness() {
if (!assertRunningOnHighPerformanceGpu())
return;
assertRunningOnHighPerformanceGpu();
sendResult("READY", "Ready for second tab to be closed");
}
function runToCompletion() {
if (!assertRunningOnLowPowerGpu()) {
// This test expects to be notified even on non-dual-GPU systems.
if (isRunningOnDualGpuSystem()) {
// Failure has been reported.
return;
}
}
assertRunningOnLowPowerGpu();
drawSomeFrames(waitForFinish);
}
......@@ -58,6 +51,5 @@ function waitForFinish()
</head>
<body onload="ready()">
<canvas id="c" width="300" height="300" class="nomargin" style="position:absolute; top:0px; left:0px;"></canvas>
</div>
</body>
</html>
......@@ -24,22 +24,14 @@ function initialize(runningOnDualGPUMacBookPro) {
}
function allocateHighPowerContext() {
if (!assertRunningOnLowPowerGpu()) {
// This test expects to be notified even on non-dual-GPU systems.
if (!isRunningOnDualGpuSystem()) {
// Will override failure report on dual-GPU systems.
waitForFinish();
}
return;
}
assertRunningOnLowPowerGpu();
c2.getContext("webgl", { powerPreference: "high-performance" });
// On macOS there is an approximately two second delay between activating the
// discrete GPU and applications receiving a notification of its activation.
// Avoid a race condition, and test flakiness, by waiting for twice as long as
// this delay.
setTimeout(() => {
if (!assertRunningOnHighPerformanceGpu())
return;
assertRunningOnHighPerformanceGpu();
drawSomeFrames(waitForFinish);
}, 4000);
}
......@@ -63,6 +55,5 @@ function waitForFinish()
<body onload="ready()">
<canvas id="c" width="300" height="300" class="nomargin" style="position:absolute; top:0px; left:0px;"></canvas>
<canvas id="c2" width="1" height="1" class="nomargin"></canvas>
</div>
</body>
</html>
......@@ -24,22 +24,14 @@ function initialize(runningOnDualGPUMacBookPro) {
}
function allocateHighPowerContext() {
if (!assertRunningOnLowPowerGpu()) {
// This test expects to be notified even on non-dual-GPU systems.
if (!isRunningOnDualGpuSystem()) {
// Will override failure report on dual-GPU systems.
waitForFinish();
}
return;
}
assertRunningOnLowPowerGpu();
c2.getContext("webgl", { powerPreference: "high-performance" });
// On macOS there is an approximately two second delay between activating the
// discrete GPU and applications receiving a notification of its activation.
// Avoid a race condition, and test flakiness, by waiting for twice as long as
// this delay.
setTimeout(() => {
if (!assertRunningOnHighPerformanceGpu())
return;
assertRunningOnHighPerformanceGpu();
drawSomeFrames(waitForFinish);
}, 4000);
}
......@@ -63,6 +55,5 @@ function waitForFinish()
<body onload="ready()">
<canvas id="c" width="300" height="300" class="nomargin" style="position:absolute; top:0px; left:0px;"></canvas>
<canvas id="c2" width="1" height="1" class="nomargin"></canvas>
</div>
</body>
</html>
......@@ -19,7 +19,7 @@ test_harness_script = r"""
domAutomationController._proceed = false;
domAutomationController._readyForActions = false;
domAutomationController._succeeded = false;
domAutomationController._succeeded = undefined;
domAutomationController._finished = false;
domAutomationController._originalLog = window.console.log;
domAutomationController._messages = '';
......@@ -36,11 +36,10 @@ test_harness_script = r"""
domAutomationController._readyForActions = true;
} else {
domAutomationController._finished = true;
if (lmsg == "success") {
domAutomationController._succeeded = true;
} else {
domAutomationController._succeeded = false;
}
// Do not squelch any previous failures. Show any new ones.
if (domAutomationController._succeeded === undefined ||
domAutomationController._succeeded)
domAutomationController._succeeded = (lmsg == "success");
}
}
......@@ -99,9 +98,23 @@ class PixelIntegrationTest(
'domAutomationController._proceed', timeout=300)
do_page_action = tab.EvaluateJavaScript(
'domAutomationController._readyForActions')
if do_page_action:
self._DoPageAction(tab, page)
self._RunSkiaGoldBasedPixelTest(do_page_action, page)
try:
if do_page_action:
# The page action may itself signal test failure via self.fail().
self._DoPageAction(tab, page)
self._RunSkiaGoldBasedPixelTest(do_page_action, page)
finally:
test_messages = self._TestHarnessMessages(tab)
if test_messages:
logging.info('Logging messages from the test:\n' + test_messages)
if do_page_action or page.restart_browser_after_test:
self._RestartBrowser(
'Must restart after page actions or if required by test')
if do_page_action and self._IsDualGPUMacLaptop():
# Give the system a few seconds to reliably indicate that the
# low-power GPU is active again, to avoid race conditions if the next
# test makes assertions about the active GPU.
time.sleep(4)
def _RunSkiaGoldBasedPixelTest(self, do_page_action, page):
"""Captures and compares a test image using Skia Gold.
......@@ -113,40 +126,32 @@ class PixelIntegrationTest(
page: the GPU PixelTestPage object for the test.
"""
tab = self.tab
try:
# Actually run the test and capture the screenshot.
if not tab.EvaluateJavaScript('domAutomationController._succeeded'):
self.fail('page indicated test failure')
screenshot = tab.Screenshot(5)
if screenshot is None:
self.fail('Could not capture screenshot')
dpr = tab.EvaluateJavaScript('window.devicePixelRatio')
if page.test_rect:
screenshot = image_util.Crop(
screenshot, int(page.test_rect[0] * dpr),
int(page.test_rect[1] * dpr), int(page.test_rect[2] * dpr),
int(page.test_rect[3] * dpr))
build_id_args = self._GetBuildIdArgs()
# Compare images against approved images/colors.
if page.expected_colors:
# Use expected colors instead of hash comparison for validation.
self._ValidateScreenshotSamplesWithSkiaGold(
tab, page, screenshot, dpr, build_id_args)
return
image_name = self._UrlToImageName(page.name)
self._UploadTestResultToSkiaGold(
image_name, screenshot,
tab, page,
build_id_args=build_id_args)
finally:
test_messages = self._TestHarnessMessages(tab)
if test_messages:
logging.info('Logging messages from the test:\n' + test_messages)
if do_page_action or page.restart_browser_after_test:
self._RestartBrowser(
'Must restart after page actions or if required by test')
# Actually run the test and capture the screenshot.
if not tab.EvaluateJavaScript('domAutomationController._succeeded'):
self.fail('page indicated test failure')
screenshot = tab.Screenshot(5)
if screenshot is None:
self.fail('Could not capture screenshot')
dpr = tab.EvaluateJavaScript('window.devicePixelRatio')
if page.test_rect:
screenshot = image_util.Crop(
screenshot, int(page.test_rect[0] * dpr),
int(page.test_rect[1] * dpr), int(page.test_rect[2] * dpr),
int(page.test_rect[3] * dpr))
build_id_args = self._GetBuildIdArgs()
# Compare images against approved images/colors.
if page.expected_colors:
# Use expected colors instead of hash comparison for validation.
self._ValidateScreenshotSamplesWithSkiaGold(
tab, page, screenshot, dpr, build_id_args)
return
image_name = self._UrlToImageName(page.name)
self._UploadTestResultToSkiaGold(
image_name, screenshot,
tab, page,
build_id_args=build_id_args)
def _DoPageAction(self, tab, page):
getattr(self, '_' + page.optional_action)(tab, page)
......@@ -158,6 +163,16 @@ class PixelIntegrationTest(
def _TestHarnessMessages(self, tab):
return tab.EvaluateJavaScript('domAutomationController._messages')
def _AssertLowPowerGPU(self):
if self._IsDualGPUMacLaptop():
if not self._IsIntelGPUActive():
self.fail('Low power GPU should have been active but wasn\'t')
def _AssertHighPerformanceGPU(self):
if self._IsDualGPUMacLaptop():
if self._IsIntelGPUActive():
self.fail('High performance GPU should have been active but wasn\'t')
#
# Optional actions pages can take.
# These are specified as methods taking the tab and the page as
......@@ -226,6 +241,24 @@ class PixelIntegrationTest(
# The harness above will take care of waiting for the test to
# complete with either a success or failure.
def _RunOffscreenCanvasIBRCWebGLTest(self, tab, page):
self._AssertLowPowerGPU()
tab.EvaluateJavaScript('setup()')
# Wait a few seconds for any (incorrect) GPU switched
# notifications to propagate throughout the system.
time.sleep(5)
self._AssertLowPowerGPU()
tab.EvaluateJavaScript('render()')
def _RunOffscreenCanvasIBRCWebGLHighPerfTest(self, tab, page):
self._AssertLowPowerGPU()
tab.EvaluateJavaScript('setup(true)')
# Wait a few seconds for any (incorrect) GPU switched
# notifications to propagate throughout the system.
time.sleep(5)
self._AssertHighPerformanceGPU()
tab.EvaluateJavaScript('render()')
@classmethod
def ExpectationsFiles(cls):
return [
......
......@@ -757,6 +757,36 @@ class PixelTestPages(object):
},
]),
PixelTestPage(
'pixel_offscreen_canvas_ibrc_webgl_main.html',
base_name + '_OffscreenCanvasIBRCWebGLMain',
test_rect=[0, 0, 300, 300],
tolerance=3,
expected_colors=[
{
'comment': 'solid green',
'location': [100, 100],
'size': [100, 100],
'color': [0, 255, 0],
}
],
optional_action='RunOffscreenCanvasIBRCWebGLTest'),
PixelTestPage(
'pixel_offscreen_canvas_ibrc_webgl_worker.html',
base_name + '_OffscreenCanvasIBRCWebGLWorker',
test_rect=[0, 0, 300, 300],
tolerance=3,
expected_colors=[
{
'comment': 'solid green',
'location': [100, 100],
'size': [100, 100],
'color': [0, 255, 0],
}
],
optional_action='RunOffscreenCanvasIBRCWebGLTest'),
]
......@@ -1336,6 +1366,36 @@ class PixelTestPages(object):
}
],
optional_action='RunLowToHighPowerTest'),
PixelTestPage(
'pixel_offscreen_canvas_ibrc_webgl_main.html',
base_name + '_OffscreenCanvasIBRCWebGLHighPerfMain',
test_rect=[0, 0, 300, 300],
tolerance=3,
expected_colors=[
{
'comment': 'solid green',
'location': [100, 100],
'size': [100, 100],
'color': [0, 255, 0],
}
],
optional_action='RunOffscreenCanvasIBRCWebGLHighPerfTest'),
PixelTestPage(
'pixel_offscreen_canvas_ibrc_webgl_worker.html',
base_name + '_OffscreenCanvasIBRCWebGLHighPerfWorker',
test_rect=[0, 0, 300, 300],
tolerance=3,
expected_colors=[
{
'comment': 'solid green',
'location': [100, 100],
'size': [100, 100],
'color': [0, 255, 0],
}
],
optional_action='RunOffscreenCanvasIBRCWebGLHighPerfTest'),
]
@staticmethod
......
......@@ -229,6 +229,8 @@ crbug.com/1019462 [ android nvidia no-skia-renderer ] Pixel_WebGL2_BlitFramebuff
crbug.com/1047573 [ mac nvidia ] Pixel_WebGLLowToHighPower [ Skip ]
crbug.com/1047573 [ mac nvidia ] Pixel_WebGLLowToHighPowerAlphaFalse [ Skip ]
crbug.com/1047573 [ mac nvidia ] Pixel_WebGLHighToLowPower [ Skip ]
crbug.com/1048892 [ mac nvidia ] Pixel_OffscreenCanvasIBRCWebGLHighPerfMain [ Skip ]
crbug.com/1048892 [ mac nvidia ] Pixel_OffscreenCanvasIBRCWebGLHighPerfWorker [ Skip ]
# Flakes on Linux, Mac and Windows
crbug.com/1028975 [ linux ] Pixel_PrecisionRoundedCorner [ RetryOnFailure ]
......
......@@ -10,6 +10,10 @@ crbug.com/978516 [ win10 nvidia-0x1cb3 ] TraceTest_* [ RetryOnFailure ]
# Device traces are not supported on all machines.
DeviceTraceTest_* [ Skip ]
# Require page actions - which are only supported in pixel tests.
crbug.com/1048892 TraceTest_OffscreenCanvasIBRCWebGLMain [ Skip ]
crbug.com/1048892 TraceTest_OffscreenCanvasIBRCWebGLWorker [ Skip ]
# The TraceTest versions of the pixel tests' Video_Context_Loss tests
# appear flaky on multiple platforms. These are mainly meant to run as
# pixel tests so they're being skipped in this test suite.
......
......@@ -11,8 +11,10 @@
#include "third_party/blink/renderer/core/css/css_font_selector.h"
#include "third_party/blink/renderer/core/css/offscreen_font_selector.h"
#include "third_party/blink/renderer/core/css/style_engine.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/fileapi/blob.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/html/canvas/canvas_async_blob_creator.h"
#include "third_party/blink/renderer/core/html/canvas/canvas_context_creation_attributes_core.h"
#include "third_party/blink/renderer/core/html/canvas/canvas_rendering_context.h"
......@@ -42,6 +44,27 @@ OffscreenCanvas::OffscreenCanvas(ExecutionContext* context, const IntSize& size)
CanvasRenderingContextHost::HostType::kOffscreenCanvasHost),
execution_context_(context),
size_(size) {
// Other code in Blink watches for destruction of the context; be
// robust here as well.
if (!context->IsContextDestroyed()) {
if (context->IsDocument()) {
// If this OffscreenCanvas is being created in the context of a
// cross-origin iframe, it should prefer to use the low-power GPU.
LocalFrame* frame = Document::From(context)->GetFrame();
if (!(frame && frame->IsCrossOriginToMainFrame())) {
AllowHighPerformancePowerPreference();
}
} else if (context->IsDedicatedWorkerGlobalScope()) {
// Per spec, dedicated workers can only load same-origin top-level
// scripts, so grant them access to the high-performance GPU.
//
// TODO(crbug.com/1050739): refine this logic. If the worker was
// spawned from an iframe, keep track of whether that iframe was
// itself cross-origin.
AllowHighPerformancePowerPreference();
}
}
UpdateMemoryUsage();
}
......
......@@ -7,7 +7,6 @@
#include "base/metrics/histogram_functions.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_canvas_context_creation_attributes_module.h"
#include "third_party/blink/renderer/core/dom/dom_node_ids.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/html/canvas/canvas_rendering_context.h"
#include "third_party/blink/renderer/core/offscreencanvas/offscreen_canvas.h"
#include "third_party/blink/renderer/modules/canvas/htmlcanvas/canvas_context_creation_attributes_helpers.h"
......@@ -70,12 +69,6 @@ OffscreenCanvas* HTMLCanvasElementModule::TransferControlToOffscreenInternal(
execution_context, canvas.width(), canvas.height());
offscreen_canvas->SetFilterQuality(canvas.FilterQuality());
// If this canvas is cross-origin, then the associated offscreen canvas
// should prefer using the low-power GPU.
LocalFrame* frame = canvas.GetDocument().GetFrame();
if (!(frame && frame->IsCrossOriginToMainFrame()))
offscreen_canvas->AllowHighPerformancePowerPreference();
DOMNodeId canvas_id = DOMNodeIds::IdForNode(&canvas);
canvas.RegisterPlaceholderCanvas(static_cast<int>(canvas_id));
offscreen_canvas->SetPlaceholderCanvasId(canvas_id);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment