summaryrefslogtreecommitdiffstats
path: root/toolkit/components/gfx
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/gfx')
-rw-r--r--toolkit/components/gfx/SanityTest.sys.mjs448
-rw-r--r--toolkit/components/gfx/components.conf16
-rw-r--r--toolkit/components/gfx/content/gfxFrameScript.js73
-rw-r--r--toolkit/components/gfx/content/sanityparent.html8
-rw-r--r--toolkit/components/gfx/content/sanitytest.html10
-rw-r--r--toolkit/components/gfx/content/videotest.mp4bin0 -> 1721 bytes
-rw-r--r--toolkit/components/gfx/jar.mn10
-rw-r--r--toolkit/components/gfx/moz.build18
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
new file mode 100644
index 0000000000..425c1cd5c5
--- /dev/null
+++ b/toolkit/components/gfx/content/videotest.mp4
Binary files differ
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"]