diff options
Diffstat (limited to 'toolkit/components/gfx')
-rw-r--r-- | toolkit/components/gfx/SanityTest.sys.mjs | 448 | ||||
-rw-r--r-- | toolkit/components/gfx/components.conf | 16 | ||||
-rw-r--r-- | toolkit/components/gfx/content/gfxFrameScript.js | 73 | ||||
-rw-r--r-- | toolkit/components/gfx/content/sanityparent.html | 8 | ||||
-rw-r--r-- | toolkit/components/gfx/content/sanitytest.html | 10 | ||||
-rw-r--r-- | toolkit/components/gfx/content/videotest.mp4 | bin | 0 -> 1721 bytes | |||
-rw-r--r-- | toolkit/components/gfx/jar.mn | 10 | ||||
-rw-r--r-- | toolkit/components/gfx/moz.build | 18 |
8 files changed, 583 insertions, 0 deletions
diff --git a/toolkit/components/gfx/SanityTest.sys.mjs b/toolkit/components/gfx/SanityTest.sys.mjs new file mode 100644 index 0000000000..f519735a1a --- /dev/null +++ b/toolkit/components/gfx/SanityTest.sys.mjs @@ -0,0 +1,448 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const FRAME_SCRIPT_URL = "chrome://gfxsanity/content/gfxFrameScript.js"; + +const TEST_DISABLED_PREF = "media.sanity-test.disabled"; +const PAGE_WIDTH = 160; +const PAGE_HEIGHT = 234; +const LEFT_EDGE = 8; +const TOP_EDGE = 8; +const CANVAS_WIDTH = 32; +const CANVAS_HEIGHT = 64; +// If those values are ever changed, make sure to update +// WMFVideoMFTManager::CanUseDXVA accordingly. +const VIDEO_WIDTH = 132; +const VIDEO_HEIGHT = 132; +const DRIVER_PREF = "sanity-test.driver-version"; +const DEVICE_PREF = "sanity-test.device-id"; +const VERSION_PREF = "sanity-test.version"; +const DISABLE_VIDEO_PREF = "media.hardware-video-decoding.failed"; +const RUNNING_PREF = "sanity-test.running"; +const TIMEOUT_SEC = 20; + +const MEDIA_ENGINE_PREF = "media.wmf.media-engine.enabled"; + +// GRAPHICS_SANITY_TEST histogram enumeration values +const TEST_PASSED = 0; +const TEST_FAILED_RENDER = 1; +const TEST_FAILED_VIDEO = 2; +const TEST_CRASHED = 3; +const TEST_TIMEOUT = 4; + +// GRAPHICS_SANITY_TEST_REASON enumeration values. +const REASON_FIRST_RUN = 0; +const REASON_FIREFOX_CHANGED = 1; +const REASON_DEVICE_CHANGED = 2; +const REASON_DRIVER_CHANGED = 3; + +function testPixel(ctx, x, y, r, g, b, a, fuzz) { + var data = ctx.getImageData(x, y, 1, 1); + + if ( + Math.abs(data.data[0] - r) <= fuzz && + Math.abs(data.data[1] - g) <= fuzz && + Math.abs(data.data[2] - b) <= fuzz && + Math.abs(data.data[3] - a) <= fuzz + ) { + return true; + } + return false; +} + +function reportResult(val) { + try { + let histogram = Services.telemetry.getHistogramById("GRAPHICS_SANITY_TEST"); + histogram.add(val); + } catch (e) {} + + Services.prefs.setBoolPref(RUNNING_PREF, false); + Services.prefs.savePrefFile(null); +} + +function reportTestReason(val) { + let histogram = Services.telemetry.getHistogramById( + "GRAPHICS_SANITY_TEST_REASON" + ); + histogram.add(val); +} + +function annotateCrashReport() { + try { + Services.appinfo.annotateCrashReport("TestKey", "1"); + } catch (e) {} +} + +function removeCrashReportAnnotation() { + try { + Services.appinfo.removeCrashReportAnnotation("TestKey"); + } catch (e) {} +} + +function setTimeout(aMs, aCallback) { + var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback(aCallback, aMs, Ci.nsITimer.TYPE_ONE_SHOT); +} + +function takeWindowSnapshot(win, ctx) { + // TODO: drawWindow reads back from the gpu's backbuffer, which won't catch issues with presenting + // the front buffer via the window manager. Ideally we'd use an OS level API for reading back + // from the desktop itself to get a more accurate test. + var flags = + ctx.DRAWWINDOW_DRAW_CARET | + ctx.DRAWWINDOW_DRAW_VIEW | + ctx.DRAWWINDOW_USE_WIDGET_LAYERS; + ctx.drawWindow( + win.ownerGlobal, + 0, + 0, + PAGE_WIDTH, + PAGE_HEIGHT, + "rgb(255,255,255)", + flags + ); +} + +// Verify that all the 4 coloured squares of the video +// render as expected (with a tolerance of 64 to allow for +// yuv->rgb differences between platforms). +// +// The video is VIDEO_WIDTH*VIDEO_HEIGHT, and is split into quadrants of +// different colours. The top left of the video is LEFT_EDGE,TOP_EDGE+CANVAS_HEIGHT +// and we test a pixel into the middle of each quadrant to avoid +// blending differences at the edges. +// +// We allow massive amounts of fuzz for the colours since +// it can depend hugely on the yuv -> rgb conversion, and +// we don't want to fail unnecessarily. +function verifyVideoRendering(ctx) { + return ( + testPixel( + ctx, + LEFT_EDGE + VIDEO_WIDTH / 4, + TOP_EDGE + CANVAS_HEIGHT + VIDEO_HEIGHT / 4, + 255, + 255, + 255, + 255, + 64 + ) && + testPixel( + ctx, + LEFT_EDGE + (3 * VIDEO_WIDTH) / 4, + TOP_EDGE + CANVAS_HEIGHT + VIDEO_HEIGHT / 4, + 0, + 255, + 0, + 255, + 64 + ) && + testPixel( + ctx, + LEFT_EDGE + VIDEO_WIDTH / 4, + TOP_EDGE + CANVAS_HEIGHT + (3 * VIDEO_HEIGHT) / 4, + 0, + 0, + 255, + 255, + 64 + ) && + testPixel( + ctx, + LEFT_EDGE + (3 * VIDEO_WIDTH) / 4, + TOP_EDGE + CANVAS_HEIGHT + (3 * VIDEO_HEIGHT) / 4, + 255, + 0, + 0, + 255, + 64 + ) + ); +} + +// Verify that the middle of the layers test is the color we expect. +// It's a red CANVAS_WIDTHxCANVAS_HEIGHT square, test a pixel deep into the +// square to prevent fuzzing, and another outside the expected limit of the +// square to check that scaling occurred properly. The square is drawn LEFT_EDGE +// pixels from the window's left edge and TOP_EDGE from the window's top edge. +function verifyLayersRendering(ctx) { + return ( + testPixel( + ctx, + LEFT_EDGE + CANVAS_WIDTH / 2, + TOP_EDGE + CANVAS_HEIGHT / 2, + 255, + 0, + 0, + 255, + 64 + ) && + testPixel( + ctx, + LEFT_EDGE + CANVAS_WIDTH, + TOP_EDGE + CANVAS_HEIGHT / 2, + 255, + 255, + 255, + 255, + 64 + ) + ); +} + +function testCompositor(test, win, ctx) { + takeWindowSnapshot(win, ctx); + var testPassed = true; + + if (!verifyLayersRendering(ctx)) { + reportResult(TEST_FAILED_RENDER); + testPassed = false; + } else if (!verifyVideoRendering(ctx)) { + reportResult(TEST_FAILED_VIDEO); + Services.prefs.setBoolPref(DISABLE_VIDEO_PREF, true); + testPassed = false; + } + + if (testPassed) { + reportResult(TEST_PASSED); + } + + return testPassed; +} + +var listener = { + win: null, + utils: null, + canvas: null, + ctx: null, + mm: null, + mediaEnginePrefVal: 0, + + messages: ["gfxSanity:ContentLoaded"], + + scheduleTest(win) { + this.win = win; + this.win.onload = this.onWindowLoaded.bind(this); + this.utils = this.win.windowUtils; + setTimeout(TIMEOUT_SEC * 1000, () => { + if (this.win) { + reportResult(TEST_TIMEOUT); + this.endTest(); + } + }); + }, + + runSanityTest() { + this.canvas = this.win.document.createElementNS( + "http://www.w3.org/1999/xhtml", + "canvas" + ); + this.canvas.setAttribute("width", PAGE_WIDTH); + this.canvas.setAttribute("height", PAGE_HEIGHT); + this.ctx = this.canvas.getContext("2d"); + + // Perform the compositor backbuffer test, which currently we use for + // actually deciding whether to enable hardware media decoding. + testCompositor(this, this.win, this.ctx); + + this.endTest(); + }, + + receiveMessage(message) { + switch (message.name) { + case "gfxSanity:ContentLoaded": + this.runSanityTest(); + break; + } + }, + + onWindowLoaded() { + // Disable media engine pref if it's enabled because it doesn't support + // capturing image to canvas. + const prefVal = Services.prefs.getIntPref(MEDIA_ENGINE_PREF, 0); + if (prefVal != 0) { + Services.prefs.setIntPref(MEDIA_ENGINE_PREF, 0); + this.mediaEnginePrefVal = prefVal; + } + + let browser = this.win.document.createXULElement("browser"); + browser.setAttribute("type", "content"); + browser.setAttribute("disableglobalhistory", "true"); + + let remoteBrowser = Services.appinfo.browserTabsRemoteAutostart; + browser.setAttribute("remote", remoteBrowser); + + browser.style.width = PAGE_WIDTH + "px"; + browser.style.height = PAGE_HEIGHT + "px"; + + this.win.document.documentElement.appendChild(browser); + // Have to set the mm after we append the child + this.mm = browser.messageManager; + + this.messages.forEach(msgName => { + this.mm.addMessageListener(msgName, this); + }); + + this.mm.loadFrameScript(FRAME_SCRIPT_URL, false); + }, + + endTest() { + if (!this.win) { + return; + } + + this.win.ownerGlobal.close(); + this.win = null; + this.utils = null; + this.canvas = null; + this.ctx = null; + + if (this.mm) { + // We don't have a MessageManager if onWindowLoaded never fired. + this.messages.forEach(msgName => { + this.mm.removeMessageListener(msgName, this); + }); + + this.mm = null; + } + + if (this.mediaEnginePrefVal != 0) { + Services.prefs.setIntPref(MEDIA_ENGINE_PREF, this.mediaEnginePrefVal); + this.mediaEnginePrefVal = 0; + } + + // Remove the annotation after we've cleaned everything up, to catch any + // incidental crashes from having performed the sanity test. + removeCrashReportAnnotation(); + }, +}; + +export function SanityTest() {} +SanityTest.prototype = { + classID: Components.ID("{f3a8ca4d-4c83-456b-aee2-6a2cbf11e9bd}"), + QueryInterface: ChromeUtils.generateQI([ + "nsIObserver", + "nsISupportsWeakReference", + ]), + + shouldRunTest() { + // Only test gfx features if firefox has updated, or if the user has a new + // gpu or drivers. + var buildId = Services.appinfo.platformBuildID; + var gfxinfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo); + + if (Services.prefs.getBoolPref(RUNNING_PREF, false)) { + Services.prefs.setBoolPref(DISABLE_VIDEO_PREF, true); + reportResult(TEST_CRASHED); + return false; + } + + function checkPref(pref, value, reason) { + let prefValue; + let prefType = Services.prefs.getPrefType(pref); + + switch (prefType) { + case Ci.nsIPrefBranch.PREF_INVALID: + reportTestReason(REASON_FIRST_RUN); + return false; + + case Ci.nsIPrefBranch.PREF_STRING: + prefValue = Services.prefs.getStringPref(pref); + break; + + case Ci.nsIPrefBranch.PREF_BOOL: + prefValue = Services.prefs.getBoolPref(pref); + break; + + case Ci.nsIPrefBranch.PREF_INT: + prefValue = Services.prefs.getIntPref(pref); + break; + + default: + throw new Error("Unexpected preference type."); + } + + if (prefValue != value) { + reportTestReason(reason); + return false; + } + + return true; + } + + // TODO: Handle dual GPU setups + if ( + checkPref( + DRIVER_PREF, + gfxinfo.adapterDriverVersion, + REASON_DRIVER_CHANGED + ) && + checkPref(DEVICE_PREF, gfxinfo.adapterDeviceID, REASON_DEVICE_CHANGED) && + checkPref(VERSION_PREF, buildId, REASON_FIREFOX_CHANGED) + ) { + return false; + } + + // Enable hardware decoding so we can test again + // and record the driver version to detect if the driver changes. + Services.prefs.setBoolPref(DISABLE_VIDEO_PREF, false); + Services.prefs.setStringPref(DRIVER_PREF, gfxinfo.adapterDriverVersion); + Services.prefs.setStringPref(DEVICE_PREF, gfxinfo.adapterDeviceID); + Services.prefs.setStringPref(VERSION_PREF, buildId); + + // Update the prefs so that this test doesn't run again until the next update. + Services.prefs.setBoolPref(RUNNING_PREF, true); + Services.prefs.savePrefFile(null); + return true; + }, + + observe(subject, topic, data) { + if (topic != "profile-after-change") { + return; + } + if (Services.prefs.getBoolPref(TEST_DISABLED_PREF, false)) { + return; + } + + // profile-after-change fires only at startup, so we won't need + // to use the listener again. + let tester = listener; + listener = null; + + if (!this.shouldRunTest()) { + return; + } + + annotateCrashReport(); + + // Open a tiny window to render our test page, and notify us when it's loaded + var sanityTest = Services.ww.openWindow( + null, + "chrome://gfxsanity/content/sanityparent.html", + "Test Page", + "width=" + + PAGE_WIDTH + + ",height=" + + PAGE_HEIGHT + + ",chrome,titlebar=0,scrollbars=0,dialog=1", + null + ); + + let appWin = sanityTest.docShell.treeOwner + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIAppWindow); + + // Request fast snapshot at RenderCompositor of WebRender. + // Since readback of Windows DirectComposition is very slow. + appWin.needFastSnaphot(); + + // There's no clean way to have an invisible window and ensure it's always painted. + // Instead, move the window far offscreen so it doesn't show up during launch. + sanityTest.moveTo(100000000, 1000000000); + // In multi-screens with different dpi setup, the window may have been + // incorrectly resized. + sanityTest.resizeTo(PAGE_WIDTH, PAGE_HEIGHT); + tester.scheduleTest(sanityTest); + }, +}; diff --git a/toolkit/components/gfx/components.conf b/toolkit/components/gfx/components.conf new file mode 100644 index 0000000000..62523080b9 --- /dev/null +++ b/toolkit/components/gfx/components.conf @@ -0,0 +1,16 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Classes = [ + { + 'cid': '{f3a8ca4d-4c83-456b-aee2-6a2cbf11e9bd}', + 'contract_ids': ['@mozilla.org/sanity-test;1'], + 'esModule': 'resource://gre/modules/SanityTest.sys.mjs', + 'constructor': 'SanityTest', + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + 'categories': {'profile-after-change': 'SanityTest'}, + }, +] diff --git a/toolkit/components/gfx/content/gfxFrameScript.js b/toolkit/components/gfx/content/gfxFrameScript.js new file mode 100644 index 0000000000..c423fb8a2d --- /dev/null +++ b/toolkit/components/gfx/content/gfxFrameScript.js @@ -0,0 +1,73 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* eslint-env mozilla/frame-script */ + +const gfxFrameScript = { + domUtils: null, + + init() { + let webNav = docShell.QueryInterface(Ci.nsIWebNavigation); + let webProgress = docShell + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress); + webProgress.addProgressListener( + this, + Ci.nsIWebProgress.NOTIFY_STATE_WINDOW + ); + + this.domUtils = content.windowUtils; + + let loadURIOptions = { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }; + webNav.loadURI( + Services.io.newURI("chrome://gfxsanity/content/sanitytest.html"), + loadURIOptions + ); + }, + + handleEvent(aEvent) { + switch (aEvent.type) { + case "MozAfterPaint": + sendAsyncMessage("gfxSanity:ContentLoaded"); + removeEventListener("MozAfterPaint", this); + break; + } + }, + + isSanityTest(aUri) { + if (!aUri) { + return false; + } + + return aUri.endsWith("/sanitytest.html"); + }, + + onStateChange(webProgress, req, flags, status) { + if ( + webProgress.isTopLevel && + flags & Ci.nsIWebProgressListener.STATE_STOP && + this.isSanityTest(req.name) + ) { + webProgress.removeProgressListener(this); + + // If no paint is pending, then the test already painted + if (this.domUtils.isMozAfterPaintPending) { + addEventListener("MozAfterPaint", this); + } else { + sendAsyncMessage("gfxSanity:ContentLoaded"); + } + } + }, + + // Needed to support web progress listener + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + "nsIObserver", + ]), +}; + +gfxFrameScript.init(); diff --git a/toolkit/components/gfx/content/sanityparent.html b/toolkit/components/gfx/content/sanityparent.html new file mode 100644 index 0000000000..3f1dc4a894 --- /dev/null +++ b/toolkit/components/gfx/content/sanityparent.html @@ -0,0 +1,8 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this file, + - You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<html> + <head> </head> + <body></body> +</html> diff --git a/toolkit/components/gfx/content/sanitytest.html b/toolkit/components/gfx/content/sanitytest.html new file mode 100644 index 0000000000..a99fbc98cf --- /dev/null +++ b/toolkit/components/gfx/content/sanitytest.html @@ -0,0 +1,10 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this file, + - You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<html> + <body> + <div style="width: 32px; height: 64px; background-color: red"></div> + <video src="videotest.mp4"></video> + </body> +</html> diff --git a/toolkit/components/gfx/content/videotest.mp4 b/toolkit/components/gfx/content/videotest.mp4 Binary files differnew file mode 100644 index 0000000000..425c1cd5c5 --- /dev/null +++ b/toolkit/components/gfx/content/videotest.mp4 diff --git a/toolkit/components/gfx/jar.mn b/toolkit/components/gfx/jar.mn new file mode 100644 index 0000000000..4794e7d3df --- /dev/null +++ b/toolkit/components/gfx/jar.mn @@ -0,0 +1,10 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +toolkit.jar: +% content gfxsanity %content/gfxsanity/ + content/gfxsanity/gfxFrameScript.js (content/gfxFrameScript.js) + content/gfxsanity/sanityparent.html (content/sanityparent.html) + content/gfxsanity/sanitytest.html (content/sanitytest.html) + content/gfxsanity/videotest.mp4 (content/videotest.mp4) diff --git a/toolkit/components/gfx/moz.build b/toolkit/components/gfx/moz.build new file mode 100644 index 0000000000..29d2144d4e --- /dev/null +++ b/toolkit/components/gfx/moz.build @@ -0,0 +1,18 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Graphics") + +EXTRA_JS_MODULES += [ + "SanityTest.sys.mjs", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +JAR_MANIFESTS += ["jar.mn"] |