michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: "use strict"; michael@0: michael@0: const {Cc, Ci, Cu, Cr} = require("chrome"); michael@0: const events = require("sdk/event/core"); michael@0: const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); michael@0: const protocol = require("devtools/server/protocol"); michael@0: const {CallWatcherActor, CallWatcherFront} = require("devtools/server/actors/call-watcher"); michael@0: const DevToolsUtils = require("devtools/toolkit/DevToolsUtils.js"); michael@0: michael@0: const {on, once, off, emit} = events; michael@0: const {method, custom, Arg, Option, RetVal} = protocol; michael@0: michael@0: const CANVAS_CONTEXTS = [ michael@0: "CanvasRenderingContext2D", michael@0: "WebGLRenderingContext" michael@0: ]; michael@0: michael@0: const ANIMATION_GENERATORS = [ michael@0: "requestAnimationFrame", michael@0: "mozRequestAnimationFrame" michael@0: ]; michael@0: michael@0: const DRAW_CALLS = [ michael@0: // 2D canvas michael@0: "fill", michael@0: "stroke", michael@0: "clearRect", michael@0: "fillRect", michael@0: "strokeRect", michael@0: "fillText", michael@0: "strokeText", michael@0: "drawImage", michael@0: michael@0: // WebGL michael@0: "clear", michael@0: "drawArrays", michael@0: "drawElements", michael@0: "finish", michael@0: "flush" michael@0: ]; michael@0: michael@0: const INTERESTING_CALLS = [ michael@0: // 2D canvas michael@0: "save", michael@0: "restore", michael@0: michael@0: // WebGL michael@0: "useProgram" michael@0: ]; michael@0: michael@0: exports.register = function(handle) { michael@0: handle.addTabActor(CanvasActor, "canvasActor"); michael@0: }; michael@0: michael@0: exports.unregister = function(handle) { michael@0: handle.removeTabActor(CanvasActor); michael@0: }; michael@0: michael@0: /** michael@0: * Type representing an Uint32Array buffer, serialized fast(er). michael@0: * michael@0: * XXX: It would be nice if on local connections (only), we could just *give* michael@0: * the buffer directly to the front, instead of going through all this michael@0: * serialization redundancy. michael@0: */ michael@0: protocol.types.addType("uint32-array", { michael@0: write: (v) => "[" + Array.join(v, ",") + "]", michael@0: read: (v) => new Uint32Array(JSON.parse(v)) michael@0: }); michael@0: michael@0: /** michael@0: * Type describing a thumbnail or screenshot in a recorded animation frame. michael@0: */ michael@0: protocol.types.addDictType("snapshot-image", { michael@0: index: "number", michael@0: width: "number", michael@0: height: "number", michael@0: flipped: "boolean", michael@0: pixels: "uint32-array" michael@0: }); michael@0: michael@0: /** michael@0: * Type describing an overview of a recorded animation frame. michael@0: */ michael@0: protocol.types.addDictType("snapshot-overview", { michael@0: calls: "array:function-call", michael@0: thumbnails: "array:snapshot-image", michael@0: screenshot: "snapshot-image" michael@0: }); michael@0: michael@0: /** michael@0: * This actor represents a recorded animation frame snapshot, along with michael@0: * all the corresponding canvas' context methods invoked in that frame, michael@0: * thumbnails for each draw call and a screenshot of the end result. michael@0: */ michael@0: let FrameSnapshotActor = protocol.ActorClass({ michael@0: typeName: "frame-snapshot", michael@0: michael@0: /** michael@0: * Creates the frame snapshot call actor. michael@0: * michael@0: * @param DebuggerServerConnection conn michael@0: * The server connection. michael@0: * @param HTMLCanvasElement canvas michael@0: * A reference to the content canvas. michael@0: * @param array calls michael@0: * An array of "function-call" actor instances. michael@0: * @param object screenshot michael@0: * A single "snapshot-image" type instance. michael@0: */ michael@0: initialize: function(conn, { canvas, calls, screenshot }) { michael@0: protocol.Actor.prototype.initialize.call(this, conn); michael@0: this._contentCanvas = canvas; michael@0: this._functionCalls = calls; michael@0: this._lastDrawCallScreenshot = screenshot; michael@0: }, michael@0: michael@0: /** michael@0: * Gets as much data about this snapshot without computing anything costly. michael@0: */ michael@0: getOverview: method(function() { michael@0: return { michael@0: calls: this._functionCalls, michael@0: thumbnails: this._functionCalls.map(e => e._thumbnail).filter(e => !!e), michael@0: screenshot: this._lastDrawCallScreenshot michael@0: }; michael@0: }, { michael@0: response: { overview: RetVal("snapshot-overview") } michael@0: }), michael@0: michael@0: /** michael@0: * Gets a screenshot of the canvas's contents after the specified michael@0: * function was called. michael@0: */ michael@0: generateScreenshotFor: method(function(functionCall) { michael@0: let caller = functionCall.details.caller; michael@0: let global = functionCall.meta.global; michael@0: michael@0: let canvas = this._contentCanvas; michael@0: let calls = this._functionCalls; michael@0: let index = calls.indexOf(functionCall); michael@0: michael@0: // To get a screenshot, replay all the steps necessary to render the frame, michael@0: // by invoking the context calls up to and including the specified one. michael@0: // This will be done in a custom framebuffer in case of a WebGL context. michael@0: let { replayContext, lastDrawCallIndex } = ContextUtils.replayAnimationFrame({ michael@0: contextType: global, michael@0: canvas: canvas, michael@0: calls: calls, michael@0: first: 0, michael@0: last: index michael@0: }); michael@0: michael@0: // To keep things fast, generate an image that's relatively small. michael@0: let dimensions = Math.min(CanvasFront.SCREENSHOT_HEIGHT_MAX, canvas.height); michael@0: let screenshot; michael@0: michael@0: // Depending on the canvas' context, generating a screenshot is done michael@0: // in different ways. In case of the WebGL context, we also need to reset michael@0: // the framebuffer binding to the default value. michael@0: if (global == CallWatcherFront.CANVAS_WEBGL_CONTEXT) { michael@0: screenshot = ContextUtils.getPixelsForWebGL(replayContext); michael@0: replayContext.bindFramebuffer(replayContext.FRAMEBUFFER, null); michael@0: screenshot.flipped = true; michael@0: } michael@0: // In case of 2D contexts, no additional special treatment is necessary. michael@0: else if (global == CallWatcherFront.CANVAS_2D_CONTEXT) { michael@0: screenshot = ContextUtils.getPixelsFor2D(replayContext); michael@0: screenshot.flipped = false; michael@0: } michael@0: michael@0: screenshot.index = lastDrawCallIndex; michael@0: return screenshot; michael@0: }, { michael@0: request: { call: Arg(0, "function-call") }, michael@0: response: { screenshot: RetVal("snapshot-image") } michael@0: }) michael@0: }); michael@0: michael@0: /** michael@0: * The corresponding Front object for the FrameSnapshotActor. michael@0: */ michael@0: let FrameSnapshotFront = protocol.FrontClass(FrameSnapshotActor, { michael@0: initialize: function(client, form) { michael@0: protocol.Front.prototype.initialize.call(this, client, form); michael@0: this._lastDrawCallScreenshot = null; michael@0: this._cachedScreenshots = new WeakMap(); michael@0: }, michael@0: michael@0: /** michael@0: * This implementation caches the last draw call screenshot to optimize michael@0: * frontend requests to `generateScreenshotFor`. michael@0: */ michael@0: getOverview: custom(function() { michael@0: return this._getOverview().then(data => { michael@0: this._lastDrawCallScreenshot = data.screenshot; michael@0: return data; michael@0: }); michael@0: }, { michael@0: impl: "_getOverview" michael@0: }), michael@0: michael@0: /** michael@0: * This implementation saves a roundtrip to the backend if the screenshot michael@0: * was already generated and retrieved once. michael@0: */ michael@0: generateScreenshotFor: custom(function(functionCall) { michael@0: if (CanvasFront.ANIMATION_GENERATORS.has(functionCall.name)) { michael@0: return promise.resolve(this._lastDrawCallScreenshot); michael@0: } michael@0: let cachedScreenshot = this._cachedScreenshots.get(functionCall); michael@0: if (cachedScreenshot) { michael@0: return cachedScreenshot; michael@0: } michael@0: let screenshot = this._generateScreenshotFor(functionCall); michael@0: this._cachedScreenshots.set(functionCall, screenshot); michael@0: return screenshot; michael@0: }, { michael@0: impl: "_generateScreenshotFor" michael@0: }) michael@0: }); michael@0: michael@0: /** michael@0: * This Canvas Actor handles simple instrumentation of all the methods michael@0: * of a 2D or WebGL context, to provide information regarding all the calls michael@0: * made when drawing frame inside an animation loop. michael@0: */ michael@0: let CanvasActor = exports.CanvasActor = protocol.ActorClass({ michael@0: typeName: "canvas", michael@0: initialize: function(conn, tabActor) { michael@0: protocol.Actor.prototype.initialize.call(this, conn); michael@0: this.tabActor = tabActor; michael@0: this._onContentFunctionCall = this._onContentFunctionCall.bind(this); michael@0: }, michael@0: destroy: function(conn) { michael@0: protocol.Actor.prototype.destroy.call(this, conn); michael@0: this.finalize(); michael@0: }, michael@0: michael@0: /** michael@0: * Starts listening for function calls. michael@0: */ michael@0: setup: method(function({ reload }) { michael@0: if (this._initialized) { michael@0: return; michael@0: } michael@0: this._initialized = true; michael@0: michael@0: this._callWatcher = new CallWatcherActor(this.conn, this.tabActor); michael@0: this._callWatcher.onCall = this._onContentFunctionCall; michael@0: this._callWatcher.setup({ michael@0: tracedGlobals: CANVAS_CONTEXTS, michael@0: tracedFunctions: ANIMATION_GENERATORS, michael@0: performReload: reload michael@0: }); michael@0: }, { michael@0: request: { reload: Option(0, "boolean") }, michael@0: oneway: true michael@0: }), michael@0: michael@0: /** michael@0: * Stops listening for function calls. michael@0: */ michael@0: finalize: method(function() { michael@0: if (!this._initialized) { michael@0: return; michael@0: } michael@0: this._initialized = false; michael@0: michael@0: this._callWatcher.finalize(); michael@0: this._callWatcher = null; michael@0: }, { michael@0: oneway: true michael@0: }), michael@0: michael@0: /** michael@0: * Returns whether this actor has been set up. michael@0: */ michael@0: isInitialized: method(function() { michael@0: return !!this._initialized; michael@0: }, { michael@0: response: { initialized: RetVal("boolean") } michael@0: }), michael@0: michael@0: /** michael@0: * Records a snapshot of all the calls made during the next animation frame. michael@0: * The animation should be implemented via the de-facto requestAnimationFrame michael@0: * utility, not inside a `setInterval` or recursive `setTimeout`. michael@0: * michael@0: * XXX: Currently only supporting requestAnimationFrame. When this isn't used, michael@0: * it'd be a good idea to display a huge red flashing banner telling people to michael@0: * STOP USING `setInterval` OR `setTimeout` FOR ANIMATION. Bug 978948. michael@0: */ michael@0: recordAnimationFrame: method(function() { michael@0: if (this._callWatcher.isRecording()) { michael@0: return this._currentAnimationFrameSnapshot.promise; michael@0: } michael@0: michael@0: this._callWatcher.eraseRecording(); michael@0: this._callWatcher.resumeRecording(); michael@0: michael@0: let deferred = this._currentAnimationFrameSnapshot = promise.defer(); michael@0: return deferred.promise; michael@0: }, { michael@0: response: { snapshot: RetVal("frame-snapshot") } michael@0: }), michael@0: michael@0: /** michael@0: * Invoked whenever an instrumented function is called, be it on a michael@0: * 2d or WebGL context, or an animation generator like requestAnimationFrame. michael@0: */ michael@0: _onContentFunctionCall: function(functionCall) { michael@0: let { window, name, args } = functionCall.details; michael@0: michael@0: // The function call arguments are required to replay animation frames, michael@0: // in order to generate screenshots. However, simply storing references to michael@0: // every kind of object is a bad idea, since their properties may change. michael@0: // Consider transformation matrices for example, which are typically michael@0: // Float32Arrays whose values can easily change across context calls. michael@0: // They need to be cloned. michael@0: inplaceShallowCloneArrays(args, window); michael@0: michael@0: if (CanvasFront.ANIMATION_GENERATORS.has(name)) { michael@0: this._handleAnimationFrame(functionCall); michael@0: return; michael@0: } michael@0: if (CanvasFront.DRAW_CALLS.has(name) && this._animationStarted) { michael@0: this._handleDrawCall(functionCall); michael@0: return; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Handle animations generated using requestAnimationFrame. michael@0: */ michael@0: _handleAnimationFrame: function(functionCall) { michael@0: if (!this._animationStarted) { michael@0: this._handleAnimationFrameBegin(); michael@0: } else { michael@0: this._handleAnimationFrameEnd(functionCall); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Called whenever an animation frame rendering begins. michael@0: */ michael@0: _handleAnimationFrameBegin: function() { michael@0: this._callWatcher.eraseRecording(); michael@0: this._animationStarted = true; michael@0: }, michael@0: michael@0: /** michael@0: * Called whenever an animation frame rendering ends. michael@0: */ michael@0: _handleAnimationFrameEnd: function() { michael@0: // Get a hold of all the function calls made during this animation frame. michael@0: // Since only one snapshot can be recorded at a time, erase all the michael@0: // previously recorded calls. michael@0: let functionCalls = this._callWatcher.pauseRecording(); michael@0: this._callWatcher.eraseRecording(); michael@0: michael@0: // Since the animation frame finished, get a hold of the (already retrieved) michael@0: // canvas pixels to conveniently create a screenshot of the final rendering. michael@0: let index = this._lastDrawCallIndex; michael@0: let width = this._lastContentCanvasWidth; michael@0: let height = this._lastContentCanvasHeight; michael@0: let flipped = this._lastThumbnailFlipped; michael@0: let pixels = ContextUtils.getPixelStorage()["32bit"]; michael@0: let lastDrawCallScreenshot = { michael@0: index: index, michael@0: width: width, michael@0: height: height, michael@0: flipped: flipped, michael@0: pixels: pixels.subarray(0, width * height) michael@0: }; michael@0: michael@0: // Wrap the function calls and screenshot in a FrameSnapshotActor instance, michael@0: // which will resolve the promise returned by `recordAnimationFrame`. michael@0: let frameSnapshot = new FrameSnapshotActor(this.conn, { michael@0: canvas: this._lastDrawCallCanvas, michael@0: calls: functionCalls, michael@0: screenshot: lastDrawCallScreenshot michael@0: }); michael@0: michael@0: this._currentAnimationFrameSnapshot.resolve(frameSnapshot); michael@0: this._currentAnimationFrameSnapshot = null; michael@0: this._animationStarted = false; michael@0: }, michael@0: michael@0: /** michael@0: * Invoked whenever a draw call is detected in the animation frame which is michael@0: * currently being recorded. michael@0: */ michael@0: _handleDrawCall: function(functionCall) { michael@0: let functionCalls = this._callWatcher.pauseRecording(); michael@0: let caller = functionCall.details.caller; michael@0: let global = functionCall.meta.global; michael@0: michael@0: let contentCanvas = this._lastDrawCallCanvas = caller.canvas; michael@0: let index = this._lastDrawCallIndex = functionCalls.indexOf(functionCall); michael@0: let w = this._lastContentCanvasWidth = contentCanvas.width; michael@0: let h = this._lastContentCanvasHeight = contentCanvas.height; michael@0: michael@0: // To keep things fast, generate images of small and fixed dimensions. michael@0: let dimensions = CanvasFront.THUMBNAIL_HEIGHT; michael@0: let thumbnail; michael@0: michael@0: // Create a thumbnail on every draw call on the canvas context, to augment michael@0: // the respective function call actor with this additional data. michael@0: if (global == CallWatcherFront.CANVAS_WEBGL_CONTEXT) { michael@0: // Check if drawing to a custom framebuffer (when rendering to texture). michael@0: // Don't create a thumbnail in this particular case. michael@0: let framebufferBinding = caller.getParameter(caller.FRAMEBUFFER_BINDING); michael@0: if (framebufferBinding == null) { michael@0: thumbnail = ContextUtils.getPixelsForWebGL(caller, 0, 0, w, h, dimensions); michael@0: thumbnail.flipped = this._lastThumbnailFlipped = true; michael@0: thumbnail.index = index; michael@0: } michael@0: } else if (global == CallWatcherFront.CANVAS_2D_CONTEXT) { michael@0: thumbnail = ContextUtils.getPixelsFor2D(caller, 0, 0, w, h, dimensions); michael@0: thumbnail.flipped = this._lastThumbnailFlipped = false; michael@0: thumbnail.index = index; michael@0: } michael@0: michael@0: functionCall._thumbnail = thumbnail; michael@0: this._callWatcher.resumeRecording(); michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * A collection of methods for manipulating canvas contexts. michael@0: */ michael@0: let ContextUtils = { michael@0: /** michael@0: * WebGL contexts are sensitive to how they're queried. Use this function michael@0: * to make sure the right context is always retrieved, if available. michael@0: * michael@0: * @param HTMLCanvasElement canvas michael@0: * The canvas element for which to get a WebGL context. michael@0: * @param WebGLRenderingContext gl michael@0: * The queried WebGL context, or null if unavailable. michael@0: */ michael@0: getWebGLContext: function(canvas) { michael@0: return canvas.getContext("webgl") || michael@0: canvas.getContext("experimental-webgl"); michael@0: }, michael@0: michael@0: /** michael@0: * Gets a hold of the rendered pixels in the most efficient way possible for michael@0: * a canvas with a WebGL context. michael@0: * michael@0: * @param WebGLRenderingContext gl michael@0: * The WebGL context to get a screenshot from. michael@0: * @param number srcX [optional] michael@0: * The first left pixel that is read from the framebuffer. michael@0: * @param number srcY [optional] michael@0: * The first top pixel that is read from the framebuffer. michael@0: * @param number srcWidth [optional] michael@0: * The number of pixels to read on the X axis. michael@0: * @param number srcHeight [optional] michael@0: * The number of pixels to read on the Y axis. michael@0: * @param number dstHeight [optional] michael@0: * The desired generated screenshot height. michael@0: * @return object michael@0: * An objet containing the screenshot's width, height and pixel data. michael@0: */ michael@0: getPixelsForWebGL: function(gl, michael@0: srcX = 0, srcY = 0, michael@0: srcWidth = gl.canvas.width, michael@0: srcHeight = gl.canvas.height, michael@0: dstHeight = srcHeight) michael@0: { michael@0: let contentPixels = ContextUtils.getPixelStorage(srcWidth, srcHeight); michael@0: let { "8bit": charView, "32bit": intView } = contentPixels; michael@0: gl.readPixels(srcX, srcY, srcWidth, srcHeight, gl.RGBA, gl.UNSIGNED_BYTE, charView); michael@0: return this.resizePixels(intView, srcWidth, srcHeight, dstHeight); michael@0: }, michael@0: michael@0: /** michael@0: * Gets a hold of the rendered pixels in the most efficient way possible for michael@0: * a canvas with a 2D context. michael@0: * michael@0: * @param CanvasRenderingContext2D ctx michael@0: * The 2D context to get a screenshot from. michael@0: * @param number srcX [optional] michael@0: * The first left pixel that is read from the canvas. michael@0: * @param number srcY [optional] michael@0: * The first top pixel that is read from the canvas. michael@0: * @param number srcWidth [optional] michael@0: * The number of pixels to read on the X axis. michael@0: * @param number srcHeight [optional] michael@0: * The number of pixels to read on the Y axis. michael@0: * @param number dstHeight [optional] michael@0: * The desired generated screenshot height. michael@0: * @return object michael@0: * An objet containing the screenshot's width, height and pixel data. michael@0: */ michael@0: getPixelsFor2D: function(ctx, michael@0: srcX = 0, srcY = 0, michael@0: srcWidth = ctx.canvas.width, michael@0: srcHeight = ctx.canvas.height, michael@0: dstHeight = srcHeight) michael@0: { michael@0: let { data } = ctx.getImageData(srcX, srcY, srcWidth, srcHeight); michael@0: let { "32bit": intView } = ContextUtils.usePixelStorage(data.buffer); michael@0: return this.resizePixels(intView, srcWidth, srcHeight, dstHeight); michael@0: }, michael@0: michael@0: /** michael@0: * Resizes the provided pixels to fit inside a rectangle with the specified michael@0: * height and the same aspect ratio as the source. michael@0: * michael@0: * @param Uint32Array srcPixels michael@0: * The source pixel data, assuming 32bit/pixel and 4 color components. michael@0: * @param number srcWidth michael@0: * The source pixel data width. michael@0: * @param number srcHeight michael@0: * The source pixel data height. michael@0: * @param number dstHeight [optional] michael@0: * The desired resized pixel data height. michael@0: * @return object michael@0: * An objet containing the resized pixels width, height and data. michael@0: */ michael@0: resizePixels: function(srcPixels, srcWidth, srcHeight, dstHeight) { michael@0: let screenshotRatio = dstHeight / srcHeight; michael@0: let dstWidth = Math.floor(srcWidth * screenshotRatio); michael@0: michael@0: // Use a plain array instead of a Uint32Array to make serializing faster. michael@0: let dstPixels = new Array(dstWidth * dstHeight); michael@0: michael@0: // If the resized image ends up being completely transparent, returning michael@0: // an empty array will skip some redundant serialization cycles. michael@0: let isTransparent = true; michael@0: michael@0: for (let dstX = 0; dstX < dstWidth; dstX++) { michael@0: for (let dstY = 0; dstY < dstHeight; dstY++) { michael@0: let srcX = Math.floor(dstX / screenshotRatio); michael@0: let srcY = Math.floor(dstY / screenshotRatio); michael@0: let cPos = srcX + srcWidth * srcY; michael@0: let dPos = dstX + dstWidth * dstY; michael@0: let color = dstPixels[dPos] = srcPixels[cPos]; michael@0: if (color) { michael@0: isTransparent = false; michael@0: } michael@0: } michael@0: } michael@0: michael@0: return { michael@0: width: dstWidth, michael@0: height: dstHeight, michael@0: pixels: isTransparent ? [] : dstPixels michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Invokes a series of canvas context calls, to "replay" an animation frame michael@0: * and generate a screenshot. michael@0: * michael@0: * In case of a WebGL context, an offscreen framebuffer is created for michael@0: * the respective canvas, and the rendering will be performed into it. michael@0: * This is necessary because some state (like shaders, textures etc.) can't michael@0: * be shared between two different WebGL contexts. michael@0: * Hopefully, once SharedResources are a thing this won't be necessary: michael@0: * http://www.khronos.org/webgl/wiki/SharedResouces michael@0: * michael@0: * In case of a 2D context, a new canvas is created, since there's no michael@0: * intrinsic state that can't be easily duplicated. michael@0: * michael@0: * @param number contexType michael@0: * The type of context to use. See the CallWatcherFront scope types. michael@0: * @param HTMLCanvasElement canvas michael@0: * The canvas element which is the source of all context calls. michael@0: * @param array calls michael@0: * An array of function call actors. michael@0: * @param number first michael@0: * The first function call to start from. michael@0: * @param number last michael@0: * The last (inclusive) function call to end at. michael@0: * @return object michael@0: * The context on which the specified calls were invoked and the michael@0: * last registered draw call's index. michael@0: */ michael@0: replayAnimationFrame: function({ contextType, canvas, calls, first, last }) { michael@0: let w = canvas.width; michael@0: let h = canvas.height; michael@0: michael@0: let replayCanvas; michael@0: let replayContext; michael@0: let customFramebuffer; michael@0: let lastDrawCallIndex = -1; michael@0: michael@0: // In case of WebGL contexts, rendering will be done offscreen, in a michael@0: // custom framebuffer, but on the provided canvas context. michael@0: if (contextType == CallWatcherFront.CANVAS_WEBGL_CONTEXT) { michael@0: replayCanvas = canvas; michael@0: replayContext = this.getWebGLContext(replayCanvas); michael@0: customFramebuffer = this.createBoundFramebuffer(replayContext, w, h); michael@0: } michael@0: // In case of 2D contexts, draw everything on a separate canvas context. michael@0: else if (contextType == CallWatcherFront.CANVAS_2D_CONTEXT) { michael@0: let contentDocument = canvas.ownerDocument; michael@0: replayCanvas = contentDocument.createElement("canvas"); michael@0: replayCanvas.width = w; michael@0: replayCanvas.height = h; michael@0: replayContext = replayCanvas.getContext("2d"); michael@0: replayContext.clearRect(0, 0, w, h); michael@0: } michael@0: michael@0: // Replay all the context calls up to and including the specified one. michael@0: for (let i = first; i <= last; i++) { michael@0: let { type, name, args } = calls[i].details; michael@0: michael@0: // Prevent WebGL context calls that try to reset the framebuffer binding michael@0: // to the default value, since we want to perform the rendering offscreen. michael@0: if (name == "bindFramebuffer" && args[1] == null) { michael@0: replayContext.bindFramebuffer(replayContext.FRAMEBUFFER, customFramebuffer); michael@0: } else { michael@0: if (type == CallWatcherFront.METHOD_FUNCTION) { michael@0: replayContext[name].apply(replayContext, args); michael@0: } else if (type == CallWatcherFront.SETTER_FUNCTION) { michael@0: replayContext[name] = args; michael@0: } else { michael@0: // Ignore getter calls. michael@0: } michael@0: if (CanvasFront.DRAW_CALLS.has(name)) { michael@0: lastDrawCallIndex = i; michael@0: } michael@0: } michael@0: } michael@0: michael@0: return { michael@0: replayContext: replayContext, michael@0: lastDrawCallIndex: lastDrawCallIndex michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Gets an object containing a buffer large enough to hold width * height michael@0: * pixels, assuming 32bit/pixel and 4 color components. michael@0: * michael@0: * This method avoids allocating memory and tries to reuse a common buffer michael@0: * as much as possible. michael@0: * michael@0: * @param number w michael@0: * The desired pixel array storage width. michael@0: * @param number h michael@0: * The desired pixel array storage height. michael@0: * @return object michael@0: * The requested pixel array buffer. michael@0: */ michael@0: getPixelStorage: function(w = 0, h = 0) { michael@0: let storage = this._currentPixelStorage; michael@0: if (storage && storage["32bit"].length >= w * h) { michael@0: return storage; michael@0: } michael@0: return this.usePixelStorage(new ArrayBuffer(w * h * 4)); michael@0: }, michael@0: michael@0: /** michael@0: * Creates and saves the array buffer views used by `getPixelStorage`. michael@0: * michael@0: * @param ArrayBuffer buffer michael@0: * The raw buffer used as storage for various array buffer views. michael@0: */ michael@0: usePixelStorage: function(buffer) { michael@0: let array8bit = new Uint8Array(buffer); michael@0: let array32bit = new Uint32Array(buffer); michael@0: return this._currentPixelStorage = { michael@0: "8bit": array8bit, michael@0: "32bit": array32bit michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Creates a framebuffer of the specified dimensions for a WebGL context, michael@0: * assuming a RGBA color buffer, a depth buffer and no stencil buffer. michael@0: * michael@0: * @param WebGLRenderingContext gl michael@0: * The WebGL context to create and bind a framebuffer for. michael@0: * @param number width michael@0: * The desired width of the renderbuffers. michael@0: * @param number height michael@0: * The desired height of the renderbuffers. michael@0: * @return WebGLFramebuffer michael@0: * The generated framebuffer object. michael@0: */ michael@0: createBoundFramebuffer: function(gl, width, height) { michael@0: let framebuffer = gl.createFramebuffer(); michael@0: gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); michael@0: michael@0: // Use a texture as the color rendebuffer attachment, since consumenrs of michael@0: // this function will most likely want to read the rendered pixels back. michael@0: let colorBuffer = gl.createTexture(); michael@0: gl.bindTexture(gl.TEXTURE_2D, colorBuffer); michael@0: gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); michael@0: gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); michael@0: gl.generateMipmap(gl.TEXTURE_2D); michael@0: gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); michael@0: michael@0: let depthBuffer = gl.createRenderbuffer(); michael@0: gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer); michael@0: gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height); michael@0: michael@0: gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, colorBuffer, 0); michael@0: gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer); michael@0: michael@0: gl.bindTexture(gl.TEXTURE_2D, null); michael@0: gl.bindRenderbuffer(gl.RENDERBUFFER, null); michael@0: michael@0: return framebuffer; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * The corresponding Front object for the CanvasActor. michael@0: */ michael@0: let CanvasFront = exports.CanvasFront = protocol.FrontClass(CanvasActor, { michael@0: initialize: function(client, { canvasActor }) { michael@0: protocol.Front.prototype.initialize.call(this, client, { actor: canvasActor }); michael@0: client.addActorPool(this); michael@0: this.manage(this); michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * Constants. michael@0: */ michael@0: CanvasFront.CANVAS_CONTEXTS = new Set(CANVAS_CONTEXTS); michael@0: CanvasFront.ANIMATION_GENERATORS = new Set(ANIMATION_GENERATORS); michael@0: CanvasFront.DRAW_CALLS = new Set(DRAW_CALLS); michael@0: CanvasFront.INTERESTING_CALLS = new Set(INTERESTING_CALLS); michael@0: CanvasFront.THUMBNAIL_HEIGHT = 50; // px michael@0: CanvasFront.SCREENSHOT_HEIGHT_MAX = 256; // px michael@0: CanvasFront.INVALID_SNAPSHOT_IMAGE = { michael@0: index: -1, michael@0: width: 0, michael@0: height: 0, michael@0: pixels: [] michael@0: }; michael@0: michael@0: /** michael@0: * Goes through all the arguments and creates a one-level shallow copy michael@0: * of all arrays and array buffers. michael@0: */ michael@0: function inplaceShallowCloneArrays(functionArguments, contentWindow) { michael@0: let { Object, Array, ArrayBuffer } = contentWindow; michael@0: michael@0: functionArguments.forEach((arg, index, store) => { michael@0: if (arg instanceof Array) { michael@0: store[index] = arg.slice(); michael@0: } michael@0: if (arg instanceof Object && arg.buffer instanceof ArrayBuffer) { michael@0: store[index] = new arg.constructor(arg); michael@0: } michael@0: }); michael@0: }