diff options
Diffstat (limited to '')
-rw-r--r-- | toolkit/components/gfx/SanityTest.sys.mjs | 447 |
1 files changed, 447 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..9071c682c4 --- /dev/null +++ b/toolkit/components/gfx/SanityTest.sys.mjs @@ -0,0 +1,447 @@ +/* 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, + disabledPrefs: [], + + 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. + if (Services.prefs.getBoolPref(MEDIA_ENGINE_PREF, false)) { + Services.prefs.setBoolPref(MEDIA_ENGINE_PREF, false); + this.disabledPrefs.push(MEDIA_ENGINE_PREF); + } + + 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; + } + + for (let pref of this.disabledPrefs) { + Services.prefs.setBoolPref(pref, true); + } + this.disabledPrefs = null; + + // 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,popup=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); + }, +}; |