1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/devtools/server/actors/canvas.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,759 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 +"use strict"; 1.8 + 1.9 +const {Cc, Ci, Cu, Cr} = require("chrome"); 1.10 +const events = require("sdk/event/core"); 1.11 +const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); 1.12 +const protocol = require("devtools/server/protocol"); 1.13 +const {CallWatcherActor, CallWatcherFront} = require("devtools/server/actors/call-watcher"); 1.14 +const DevToolsUtils = require("devtools/toolkit/DevToolsUtils.js"); 1.15 + 1.16 +const {on, once, off, emit} = events; 1.17 +const {method, custom, Arg, Option, RetVal} = protocol; 1.18 + 1.19 +const CANVAS_CONTEXTS = [ 1.20 + "CanvasRenderingContext2D", 1.21 + "WebGLRenderingContext" 1.22 +]; 1.23 + 1.24 +const ANIMATION_GENERATORS = [ 1.25 + "requestAnimationFrame", 1.26 + "mozRequestAnimationFrame" 1.27 +]; 1.28 + 1.29 +const DRAW_CALLS = [ 1.30 + // 2D canvas 1.31 + "fill", 1.32 + "stroke", 1.33 + "clearRect", 1.34 + "fillRect", 1.35 + "strokeRect", 1.36 + "fillText", 1.37 + "strokeText", 1.38 + "drawImage", 1.39 + 1.40 + // WebGL 1.41 + "clear", 1.42 + "drawArrays", 1.43 + "drawElements", 1.44 + "finish", 1.45 + "flush" 1.46 +]; 1.47 + 1.48 +const INTERESTING_CALLS = [ 1.49 + // 2D canvas 1.50 + "save", 1.51 + "restore", 1.52 + 1.53 + // WebGL 1.54 + "useProgram" 1.55 +]; 1.56 + 1.57 +exports.register = function(handle) { 1.58 + handle.addTabActor(CanvasActor, "canvasActor"); 1.59 +}; 1.60 + 1.61 +exports.unregister = function(handle) { 1.62 + handle.removeTabActor(CanvasActor); 1.63 +}; 1.64 + 1.65 +/** 1.66 + * Type representing an Uint32Array buffer, serialized fast(er). 1.67 + * 1.68 + * XXX: It would be nice if on local connections (only), we could just *give* 1.69 + * the buffer directly to the front, instead of going through all this 1.70 + * serialization redundancy. 1.71 + */ 1.72 +protocol.types.addType("uint32-array", { 1.73 + write: (v) => "[" + Array.join(v, ",") + "]", 1.74 + read: (v) => new Uint32Array(JSON.parse(v)) 1.75 +}); 1.76 + 1.77 +/** 1.78 + * Type describing a thumbnail or screenshot in a recorded animation frame. 1.79 + */ 1.80 +protocol.types.addDictType("snapshot-image", { 1.81 + index: "number", 1.82 + width: "number", 1.83 + height: "number", 1.84 + flipped: "boolean", 1.85 + pixels: "uint32-array" 1.86 +}); 1.87 + 1.88 +/** 1.89 + * Type describing an overview of a recorded animation frame. 1.90 + */ 1.91 +protocol.types.addDictType("snapshot-overview", { 1.92 + calls: "array:function-call", 1.93 + thumbnails: "array:snapshot-image", 1.94 + screenshot: "snapshot-image" 1.95 +}); 1.96 + 1.97 +/** 1.98 + * This actor represents a recorded animation frame snapshot, along with 1.99 + * all the corresponding canvas' context methods invoked in that frame, 1.100 + * thumbnails for each draw call and a screenshot of the end result. 1.101 + */ 1.102 +let FrameSnapshotActor = protocol.ActorClass({ 1.103 + typeName: "frame-snapshot", 1.104 + 1.105 + /** 1.106 + * Creates the frame snapshot call actor. 1.107 + * 1.108 + * @param DebuggerServerConnection conn 1.109 + * The server connection. 1.110 + * @param HTMLCanvasElement canvas 1.111 + * A reference to the content canvas. 1.112 + * @param array calls 1.113 + * An array of "function-call" actor instances. 1.114 + * @param object screenshot 1.115 + * A single "snapshot-image" type instance. 1.116 + */ 1.117 + initialize: function(conn, { canvas, calls, screenshot }) { 1.118 + protocol.Actor.prototype.initialize.call(this, conn); 1.119 + this._contentCanvas = canvas; 1.120 + this._functionCalls = calls; 1.121 + this._lastDrawCallScreenshot = screenshot; 1.122 + }, 1.123 + 1.124 + /** 1.125 + * Gets as much data about this snapshot without computing anything costly. 1.126 + */ 1.127 + getOverview: method(function() { 1.128 + return { 1.129 + calls: this._functionCalls, 1.130 + thumbnails: this._functionCalls.map(e => e._thumbnail).filter(e => !!e), 1.131 + screenshot: this._lastDrawCallScreenshot 1.132 + }; 1.133 + }, { 1.134 + response: { overview: RetVal("snapshot-overview") } 1.135 + }), 1.136 + 1.137 + /** 1.138 + * Gets a screenshot of the canvas's contents after the specified 1.139 + * function was called. 1.140 + */ 1.141 + generateScreenshotFor: method(function(functionCall) { 1.142 + let caller = functionCall.details.caller; 1.143 + let global = functionCall.meta.global; 1.144 + 1.145 + let canvas = this._contentCanvas; 1.146 + let calls = this._functionCalls; 1.147 + let index = calls.indexOf(functionCall); 1.148 + 1.149 + // To get a screenshot, replay all the steps necessary to render the frame, 1.150 + // by invoking the context calls up to and including the specified one. 1.151 + // This will be done in a custom framebuffer in case of a WebGL context. 1.152 + let { replayContext, lastDrawCallIndex } = ContextUtils.replayAnimationFrame({ 1.153 + contextType: global, 1.154 + canvas: canvas, 1.155 + calls: calls, 1.156 + first: 0, 1.157 + last: index 1.158 + }); 1.159 + 1.160 + // To keep things fast, generate an image that's relatively small. 1.161 + let dimensions = Math.min(CanvasFront.SCREENSHOT_HEIGHT_MAX, canvas.height); 1.162 + let screenshot; 1.163 + 1.164 + // Depending on the canvas' context, generating a screenshot is done 1.165 + // in different ways. In case of the WebGL context, we also need to reset 1.166 + // the framebuffer binding to the default value. 1.167 + if (global == CallWatcherFront.CANVAS_WEBGL_CONTEXT) { 1.168 + screenshot = ContextUtils.getPixelsForWebGL(replayContext); 1.169 + replayContext.bindFramebuffer(replayContext.FRAMEBUFFER, null); 1.170 + screenshot.flipped = true; 1.171 + } 1.172 + // In case of 2D contexts, no additional special treatment is necessary. 1.173 + else if (global == CallWatcherFront.CANVAS_2D_CONTEXT) { 1.174 + screenshot = ContextUtils.getPixelsFor2D(replayContext); 1.175 + screenshot.flipped = false; 1.176 + } 1.177 + 1.178 + screenshot.index = lastDrawCallIndex; 1.179 + return screenshot; 1.180 + }, { 1.181 + request: { call: Arg(0, "function-call") }, 1.182 + response: { screenshot: RetVal("snapshot-image") } 1.183 + }) 1.184 +}); 1.185 + 1.186 +/** 1.187 + * The corresponding Front object for the FrameSnapshotActor. 1.188 + */ 1.189 +let FrameSnapshotFront = protocol.FrontClass(FrameSnapshotActor, { 1.190 + initialize: function(client, form) { 1.191 + protocol.Front.prototype.initialize.call(this, client, form); 1.192 + this._lastDrawCallScreenshot = null; 1.193 + this._cachedScreenshots = new WeakMap(); 1.194 + }, 1.195 + 1.196 + /** 1.197 + * This implementation caches the last draw call screenshot to optimize 1.198 + * frontend requests to `generateScreenshotFor`. 1.199 + */ 1.200 + getOverview: custom(function() { 1.201 + return this._getOverview().then(data => { 1.202 + this._lastDrawCallScreenshot = data.screenshot; 1.203 + return data; 1.204 + }); 1.205 + }, { 1.206 + impl: "_getOverview" 1.207 + }), 1.208 + 1.209 + /** 1.210 + * This implementation saves a roundtrip to the backend if the screenshot 1.211 + * was already generated and retrieved once. 1.212 + */ 1.213 + generateScreenshotFor: custom(function(functionCall) { 1.214 + if (CanvasFront.ANIMATION_GENERATORS.has(functionCall.name)) { 1.215 + return promise.resolve(this._lastDrawCallScreenshot); 1.216 + } 1.217 + let cachedScreenshot = this._cachedScreenshots.get(functionCall); 1.218 + if (cachedScreenshot) { 1.219 + return cachedScreenshot; 1.220 + } 1.221 + let screenshot = this._generateScreenshotFor(functionCall); 1.222 + this._cachedScreenshots.set(functionCall, screenshot); 1.223 + return screenshot; 1.224 + }, { 1.225 + impl: "_generateScreenshotFor" 1.226 + }) 1.227 +}); 1.228 + 1.229 +/** 1.230 + * This Canvas Actor handles simple instrumentation of all the methods 1.231 + * of a 2D or WebGL context, to provide information regarding all the calls 1.232 + * made when drawing frame inside an animation loop. 1.233 + */ 1.234 +let CanvasActor = exports.CanvasActor = protocol.ActorClass({ 1.235 + typeName: "canvas", 1.236 + initialize: function(conn, tabActor) { 1.237 + protocol.Actor.prototype.initialize.call(this, conn); 1.238 + this.tabActor = tabActor; 1.239 + this._onContentFunctionCall = this._onContentFunctionCall.bind(this); 1.240 + }, 1.241 + destroy: function(conn) { 1.242 + protocol.Actor.prototype.destroy.call(this, conn); 1.243 + this.finalize(); 1.244 + }, 1.245 + 1.246 + /** 1.247 + * Starts listening for function calls. 1.248 + */ 1.249 + setup: method(function({ reload }) { 1.250 + if (this._initialized) { 1.251 + return; 1.252 + } 1.253 + this._initialized = true; 1.254 + 1.255 + this._callWatcher = new CallWatcherActor(this.conn, this.tabActor); 1.256 + this._callWatcher.onCall = this._onContentFunctionCall; 1.257 + this._callWatcher.setup({ 1.258 + tracedGlobals: CANVAS_CONTEXTS, 1.259 + tracedFunctions: ANIMATION_GENERATORS, 1.260 + performReload: reload 1.261 + }); 1.262 + }, { 1.263 + request: { reload: Option(0, "boolean") }, 1.264 + oneway: true 1.265 + }), 1.266 + 1.267 + /** 1.268 + * Stops listening for function calls. 1.269 + */ 1.270 + finalize: method(function() { 1.271 + if (!this._initialized) { 1.272 + return; 1.273 + } 1.274 + this._initialized = false; 1.275 + 1.276 + this._callWatcher.finalize(); 1.277 + this._callWatcher = null; 1.278 + }, { 1.279 + oneway: true 1.280 + }), 1.281 + 1.282 + /** 1.283 + * Returns whether this actor has been set up. 1.284 + */ 1.285 + isInitialized: method(function() { 1.286 + return !!this._initialized; 1.287 + }, { 1.288 + response: { initialized: RetVal("boolean") } 1.289 + }), 1.290 + 1.291 + /** 1.292 + * Records a snapshot of all the calls made during the next animation frame. 1.293 + * The animation should be implemented via the de-facto requestAnimationFrame 1.294 + * utility, not inside a `setInterval` or recursive `setTimeout`. 1.295 + * 1.296 + * XXX: Currently only supporting requestAnimationFrame. When this isn't used, 1.297 + * it'd be a good idea to display a huge red flashing banner telling people to 1.298 + * STOP USING `setInterval` OR `setTimeout` FOR ANIMATION. Bug 978948. 1.299 + */ 1.300 + recordAnimationFrame: method(function() { 1.301 + if (this._callWatcher.isRecording()) { 1.302 + return this._currentAnimationFrameSnapshot.promise; 1.303 + } 1.304 + 1.305 + this._callWatcher.eraseRecording(); 1.306 + this._callWatcher.resumeRecording(); 1.307 + 1.308 + let deferred = this._currentAnimationFrameSnapshot = promise.defer(); 1.309 + return deferred.promise; 1.310 + }, { 1.311 + response: { snapshot: RetVal("frame-snapshot") } 1.312 + }), 1.313 + 1.314 + /** 1.315 + * Invoked whenever an instrumented function is called, be it on a 1.316 + * 2d or WebGL context, or an animation generator like requestAnimationFrame. 1.317 + */ 1.318 + _onContentFunctionCall: function(functionCall) { 1.319 + let { window, name, args } = functionCall.details; 1.320 + 1.321 + // The function call arguments are required to replay animation frames, 1.322 + // in order to generate screenshots. However, simply storing references to 1.323 + // every kind of object is a bad idea, since their properties may change. 1.324 + // Consider transformation matrices for example, which are typically 1.325 + // Float32Arrays whose values can easily change across context calls. 1.326 + // They need to be cloned. 1.327 + inplaceShallowCloneArrays(args, window); 1.328 + 1.329 + if (CanvasFront.ANIMATION_GENERATORS.has(name)) { 1.330 + this._handleAnimationFrame(functionCall); 1.331 + return; 1.332 + } 1.333 + if (CanvasFront.DRAW_CALLS.has(name) && this._animationStarted) { 1.334 + this._handleDrawCall(functionCall); 1.335 + return; 1.336 + } 1.337 + }, 1.338 + 1.339 + /** 1.340 + * Handle animations generated using requestAnimationFrame. 1.341 + */ 1.342 + _handleAnimationFrame: function(functionCall) { 1.343 + if (!this._animationStarted) { 1.344 + this._handleAnimationFrameBegin(); 1.345 + } else { 1.346 + this._handleAnimationFrameEnd(functionCall); 1.347 + } 1.348 + }, 1.349 + 1.350 + /** 1.351 + * Called whenever an animation frame rendering begins. 1.352 + */ 1.353 + _handleAnimationFrameBegin: function() { 1.354 + this._callWatcher.eraseRecording(); 1.355 + this._animationStarted = true; 1.356 + }, 1.357 + 1.358 + /** 1.359 + * Called whenever an animation frame rendering ends. 1.360 + */ 1.361 + _handleAnimationFrameEnd: function() { 1.362 + // Get a hold of all the function calls made during this animation frame. 1.363 + // Since only one snapshot can be recorded at a time, erase all the 1.364 + // previously recorded calls. 1.365 + let functionCalls = this._callWatcher.pauseRecording(); 1.366 + this._callWatcher.eraseRecording(); 1.367 + 1.368 + // Since the animation frame finished, get a hold of the (already retrieved) 1.369 + // canvas pixels to conveniently create a screenshot of the final rendering. 1.370 + let index = this._lastDrawCallIndex; 1.371 + let width = this._lastContentCanvasWidth; 1.372 + let height = this._lastContentCanvasHeight; 1.373 + let flipped = this._lastThumbnailFlipped; 1.374 + let pixels = ContextUtils.getPixelStorage()["32bit"]; 1.375 + let lastDrawCallScreenshot = { 1.376 + index: index, 1.377 + width: width, 1.378 + height: height, 1.379 + flipped: flipped, 1.380 + pixels: pixels.subarray(0, width * height) 1.381 + }; 1.382 + 1.383 + // Wrap the function calls and screenshot in a FrameSnapshotActor instance, 1.384 + // which will resolve the promise returned by `recordAnimationFrame`. 1.385 + let frameSnapshot = new FrameSnapshotActor(this.conn, { 1.386 + canvas: this._lastDrawCallCanvas, 1.387 + calls: functionCalls, 1.388 + screenshot: lastDrawCallScreenshot 1.389 + }); 1.390 + 1.391 + this._currentAnimationFrameSnapshot.resolve(frameSnapshot); 1.392 + this._currentAnimationFrameSnapshot = null; 1.393 + this._animationStarted = false; 1.394 + }, 1.395 + 1.396 + /** 1.397 + * Invoked whenever a draw call is detected in the animation frame which is 1.398 + * currently being recorded. 1.399 + */ 1.400 + _handleDrawCall: function(functionCall) { 1.401 + let functionCalls = this._callWatcher.pauseRecording(); 1.402 + let caller = functionCall.details.caller; 1.403 + let global = functionCall.meta.global; 1.404 + 1.405 + let contentCanvas = this._lastDrawCallCanvas = caller.canvas; 1.406 + let index = this._lastDrawCallIndex = functionCalls.indexOf(functionCall); 1.407 + let w = this._lastContentCanvasWidth = contentCanvas.width; 1.408 + let h = this._lastContentCanvasHeight = contentCanvas.height; 1.409 + 1.410 + // To keep things fast, generate images of small and fixed dimensions. 1.411 + let dimensions = CanvasFront.THUMBNAIL_HEIGHT; 1.412 + let thumbnail; 1.413 + 1.414 + // Create a thumbnail on every draw call on the canvas context, to augment 1.415 + // the respective function call actor with this additional data. 1.416 + if (global == CallWatcherFront.CANVAS_WEBGL_CONTEXT) { 1.417 + // Check if drawing to a custom framebuffer (when rendering to texture). 1.418 + // Don't create a thumbnail in this particular case. 1.419 + let framebufferBinding = caller.getParameter(caller.FRAMEBUFFER_BINDING); 1.420 + if (framebufferBinding == null) { 1.421 + thumbnail = ContextUtils.getPixelsForWebGL(caller, 0, 0, w, h, dimensions); 1.422 + thumbnail.flipped = this._lastThumbnailFlipped = true; 1.423 + thumbnail.index = index; 1.424 + } 1.425 + } else if (global == CallWatcherFront.CANVAS_2D_CONTEXT) { 1.426 + thumbnail = ContextUtils.getPixelsFor2D(caller, 0, 0, w, h, dimensions); 1.427 + thumbnail.flipped = this._lastThumbnailFlipped = false; 1.428 + thumbnail.index = index; 1.429 + } 1.430 + 1.431 + functionCall._thumbnail = thumbnail; 1.432 + this._callWatcher.resumeRecording(); 1.433 + } 1.434 +}); 1.435 + 1.436 +/** 1.437 + * A collection of methods for manipulating canvas contexts. 1.438 + */ 1.439 +let ContextUtils = { 1.440 + /** 1.441 + * WebGL contexts are sensitive to how they're queried. Use this function 1.442 + * to make sure the right context is always retrieved, if available. 1.443 + * 1.444 + * @param HTMLCanvasElement canvas 1.445 + * The canvas element for which to get a WebGL context. 1.446 + * @param WebGLRenderingContext gl 1.447 + * The queried WebGL context, or null if unavailable. 1.448 + */ 1.449 + getWebGLContext: function(canvas) { 1.450 + return canvas.getContext("webgl") || 1.451 + canvas.getContext("experimental-webgl"); 1.452 + }, 1.453 + 1.454 + /** 1.455 + * Gets a hold of the rendered pixels in the most efficient way possible for 1.456 + * a canvas with a WebGL context. 1.457 + * 1.458 + * @param WebGLRenderingContext gl 1.459 + * The WebGL context to get a screenshot from. 1.460 + * @param number srcX [optional] 1.461 + * The first left pixel that is read from the framebuffer. 1.462 + * @param number srcY [optional] 1.463 + * The first top pixel that is read from the framebuffer. 1.464 + * @param number srcWidth [optional] 1.465 + * The number of pixels to read on the X axis. 1.466 + * @param number srcHeight [optional] 1.467 + * The number of pixels to read on the Y axis. 1.468 + * @param number dstHeight [optional] 1.469 + * The desired generated screenshot height. 1.470 + * @return object 1.471 + * An objet containing the screenshot's width, height and pixel data. 1.472 + */ 1.473 + getPixelsForWebGL: function(gl, 1.474 + srcX = 0, srcY = 0, 1.475 + srcWidth = gl.canvas.width, 1.476 + srcHeight = gl.canvas.height, 1.477 + dstHeight = srcHeight) 1.478 + { 1.479 + let contentPixels = ContextUtils.getPixelStorage(srcWidth, srcHeight); 1.480 + let { "8bit": charView, "32bit": intView } = contentPixels; 1.481 + gl.readPixels(srcX, srcY, srcWidth, srcHeight, gl.RGBA, gl.UNSIGNED_BYTE, charView); 1.482 + return this.resizePixels(intView, srcWidth, srcHeight, dstHeight); 1.483 + }, 1.484 + 1.485 + /** 1.486 + * Gets a hold of the rendered pixels in the most efficient way possible for 1.487 + * a canvas with a 2D context. 1.488 + * 1.489 + * @param CanvasRenderingContext2D ctx 1.490 + * The 2D context to get a screenshot from. 1.491 + * @param number srcX [optional] 1.492 + * The first left pixel that is read from the canvas. 1.493 + * @param number srcY [optional] 1.494 + * The first top pixel that is read from the canvas. 1.495 + * @param number srcWidth [optional] 1.496 + * The number of pixels to read on the X axis. 1.497 + * @param number srcHeight [optional] 1.498 + * The number of pixels to read on the Y axis. 1.499 + * @param number dstHeight [optional] 1.500 + * The desired generated screenshot height. 1.501 + * @return object 1.502 + * An objet containing the screenshot's width, height and pixel data. 1.503 + */ 1.504 + getPixelsFor2D: function(ctx, 1.505 + srcX = 0, srcY = 0, 1.506 + srcWidth = ctx.canvas.width, 1.507 + srcHeight = ctx.canvas.height, 1.508 + dstHeight = srcHeight) 1.509 + { 1.510 + let { data } = ctx.getImageData(srcX, srcY, srcWidth, srcHeight); 1.511 + let { "32bit": intView } = ContextUtils.usePixelStorage(data.buffer); 1.512 + return this.resizePixels(intView, srcWidth, srcHeight, dstHeight); 1.513 + }, 1.514 + 1.515 + /** 1.516 + * Resizes the provided pixels to fit inside a rectangle with the specified 1.517 + * height and the same aspect ratio as the source. 1.518 + * 1.519 + * @param Uint32Array srcPixels 1.520 + * The source pixel data, assuming 32bit/pixel and 4 color components. 1.521 + * @param number srcWidth 1.522 + * The source pixel data width. 1.523 + * @param number srcHeight 1.524 + * The source pixel data height. 1.525 + * @param number dstHeight [optional] 1.526 + * The desired resized pixel data height. 1.527 + * @return object 1.528 + * An objet containing the resized pixels width, height and data. 1.529 + */ 1.530 + resizePixels: function(srcPixels, srcWidth, srcHeight, dstHeight) { 1.531 + let screenshotRatio = dstHeight / srcHeight; 1.532 + let dstWidth = Math.floor(srcWidth * screenshotRatio); 1.533 + 1.534 + // Use a plain array instead of a Uint32Array to make serializing faster. 1.535 + let dstPixels = new Array(dstWidth * dstHeight); 1.536 + 1.537 + // If the resized image ends up being completely transparent, returning 1.538 + // an empty array will skip some redundant serialization cycles. 1.539 + let isTransparent = true; 1.540 + 1.541 + for (let dstX = 0; dstX < dstWidth; dstX++) { 1.542 + for (let dstY = 0; dstY < dstHeight; dstY++) { 1.543 + let srcX = Math.floor(dstX / screenshotRatio); 1.544 + let srcY = Math.floor(dstY / screenshotRatio); 1.545 + let cPos = srcX + srcWidth * srcY; 1.546 + let dPos = dstX + dstWidth * dstY; 1.547 + let color = dstPixels[dPos] = srcPixels[cPos]; 1.548 + if (color) { 1.549 + isTransparent = false; 1.550 + } 1.551 + } 1.552 + } 1.553 + 1.554 + return { 1.555 + width: dstWidth, 1.556 + height: dstHeight, 1.557 + pixels: isTransparent ? [] : dstPixels 1.558 + }; 1.559 + }, 1.560 + 1.561 + /** 1.562 + * Invokes a series of canvas context calls, to "replay" an animation frame 1.563 + * and generate a screenshot. 1.564 + * 1.565 + * In case of a WebGL context, an offscreen framebuffer is created for 1.566 + * the respective canvas, and the rendering will be performed into it. 1.567 + * This is necessary because some state (like shaders, textures etc.) can't 1.568 + * be shared between two different WebGL contexts. 1.569 + * Hopefully, once SharedResources are a thing this won't be necessary: 1.570 + * http://www.khronos.org/webgl/wiki/SharedResouces 1.571 + * 1.572 + * In case of a 2D context, a new canvas is created, since there's no 1.573 + * intrinsic state that can't be easily duplicated. 1.574 + * 1.575 + * @param number contexType 1.576 + * The type of context to use. See the CallWatcherFront scope types. 1.577 + * @param HTMLCanvasElement canvas 1.578 + * The canvas element which is the source of all context calls. 1.579 + * @param array calls 1.580 + * An array of function call actors. 1.581 + * @param number first 1.582 + * The first function call to start from. 1.583 + * @param number last 1.584 + * The last (inclusive) function call to end at. 1.585 + * @return object 1.586 + * The context on which the specified calls were invoked and the 1.587 + * last registered draw call's index. 1.588 + */ 1.589 + replayAnimationFrame: function({ contextType, canvas, calls, first, last }) { 1.590 + let w = canvas.width; 1.591 + let h = canvas.height; 1.592 + 1.593 + let replayCanvas; 1.594 + let replayContext; 1.595 + let customFramebuffer; 1.596 + let lastDrawCallIndex = -1; 1.597 + 1.598 + // In case of WebGL contexts, rendering will be done offscreen, in a 1.599 + // custom framebuffer, but on the provided canvas context. 1.600 + if (contextType == CallWatcherFront.CANVAS_WEBGL_CONTEXT) { 1.601 + replayCanvas = canvas; 1.602 + replayContext = this.getWebGLContext(replayCanvas); 1.603 + customFramebuffer = this.createBoundFramebuffer(replayContext, w, h); 1.604 + } 1.605 + // In case of 2D contexts, draw everything on a separate canvas context. 1.606 + else if (contextType == CallWatcherFront.CANVAS_2D_CONTEXT) { 1.607 + let contentDocument = canvas.ownerDocument; 1.608 + replayCanvas = contentDocument.createElement("canvas"); 1.609 + replayCanvas.width = w; 1.610 + replayCanvas.height = h; 1.611 + replayContext = replayCanvas.getContext("2d"); 1.612 + replayContext.clearRect(0, 0, w, h); 1.613 + } 1.614 + 1.615 + // Replay all the context calls up to and including the specified one. 1.616 + for (let i = first; i <= last; i++) { 1.617 + let { type, name, args } = calls[i].details; 1.618 + 1.619 + // Prevent WebGL context calls that try to reset the framebuffer binding 1.620 + // to the default value, since we want to perform the rendering offscreen. 1.621 + if (name == "bindFramebuffer" && args[1] == null) { 1.622 + replayContext.bindFramebuffer(replayContext.FRAMEBUFFER, customFramebuffer); 1.623 + } else { 1.624 + if (type == CallWatcherFront.METHOD_FUNCTION) { 1.625 + replayContext[name].apply(replayContext, args); 1.626 + } else if (type == CallWatcherFront.SETTER_FUNCTION) { 1.627 + replayContext[name] = args; 1.628 + } else { 1.629 + // Ignore getter calls. 1.630 + } 1.631 + if (CanvasFront.DRAW_CALLS.has(name)) { 1.632 + lastDrawCallIndex = i; 1.633 + } 1.634 + } 1.635 + } 1.636 + 1.637 + return { 1.638 + replayContext: replayContext, 1.639 + lastDrawCallIndex: lastDrawCallIndex 1.640 + }; 1.641 + }, 1.642 + 1.643 + /** 1.644 + * Gets an object containing a buffer large enough to hold width * height 1.645 + * pixels, assuming 32bit/pixel and 4 color components. 1.646 + * 1.647 + * This method avoids allocating memory and tries to reuse a common buffer 1.648 + * as much as possible. 1.649 + * 1.650 + * @param number w 1.651 + * The desired pixel array storage width. 1.652 + * @param number h 1.653 + * The desired pixel array storage height. 1.654 + * @return object 1.655 + * The requested pixel array buffer. 1.656 + */ 1.657 + getPixelStorage: function(w = 0, h = 0) { 1.658 + let storage = this._currentPixelStorage; 1.659 + if (storage && storage["32bit"].length >= w * h) { 1.660 + return storage; 1.661 + } 1.662 + return this.usePixelStorage(new ArrayBuffer(w * h * 4)); 1.663 + }, 1.664 + 1.665 + /** 1.666 + * Creates and saves the array buffer views used by `getPixelStorage`. 1.667 + * 1.668 + * @param ArrayBuffer buffer 1.669 + * The raw buffer used as storage for various array buffer views. 1.670 + */ 1.671 + usePixelStorage: function(buffer) { 1.672 + let array8bit = new Uint8Array(buffer); 1.673 + let array32bit = new Uint32Array(buffer); 1.674 + return this._currentPixelStorage = { 1.675 + "8bit": array8bit, 1.676 + "32bit": array32bit 1.677 + }; 1.678 + }, 1.679 + 1.680 + /** 1.681 + * Creates a framebuffer of the specified dimensions for a WebGL context, 1.682 + * assuming a RGBA color buffer, a depth buffer and no stencil buffer. 1.683 + * 1.684 + * @param WebGLRenderingContext gl 1.685 + * The WebGL context to create and bind a framebuffer for. 1.686 + * @param number width 1.687 + * The desired width of the renderbuffers. 1.688 + * @param number height 1.689 + * The desired height of the renderbuffers. 1.690 + * @return WebGLFramebuffer 1.691 + * The generated framebuffer object. 1.692 + */ 1.693 + createBoundFramebuffer: function(gl, width, height) { 1.694 + let framebuffer = gl.createFramebuffer(); 1.695 + gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); 1.696 + 1.697 + // Use a texture as the color rendebuffer attachment, since consumenrs of 1.698 + // this function will most likely want to read the rendered pixels back. 1.699 + let colorBuffer = gl.createTexture(); 1.700 + gl.bindTexture(gl.TEXTURE_2D, colorBuffer); 1.701 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 1.702 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); 1.703 + gl.generateMipmap(gl.TEXTURE_2D); 1.704 + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); 1.705 + 1.706 + let depthBuffer = gl.createRenderbuffer(); 1.707 + gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer); 1.708 + gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height); 1.709 + 1.710 + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, colorBuffer, 0); 1.711 + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer); 1.712 + 1.713 + gl.bindTexture(gl.TEXTURE_2D, null); 1.714 + gl.bindRenderbuffer(gl.RENDERBUFFER, null); 1.715 + 1.716 + return framebuffer; 1.717 + } 1.718 +}; 1.719 + 1.720 +/** 1.721 + * The corresponding Front object for the CanvasActor. 1.722 + */ 1.723 +let CanvasFront = exports.CanvasFront = protocol.FrontClass(CanvasActor, { 1.724 + initialize: function(client, { canvasActor }) { 1.725 + protocol.Front.prototype.initialize.call(this, client, { actor: canvasActor }); 1.726 + client.addActorPool(this); 1.727 + this.manage(this); 1.728 + } 1.729 +}); 1.730 + 1.731 +/** 1.732 + * Constants. 1.733 + */ 1.734 +CanvasFront.CANVAS_CONTEXTS = new Set(CANVAS_CONTEXTS); 1.735 +CanvasFront.ANIMATION_GENERATORS = new Set(ANIMATION_GENERATORS); 1.736 +CanvasFront.DRAW_CALLS = new Set(DRAW_CALLS); 1.737 +CanvasFront.INTERESTING_CALLS = new Set(INTERESTING_CALLS); 1.738 +CanvasFront.THUMBNAIL_HEIGHT = 50; // px 1.739 +CanvasFront.SCREENSHOT_HEIGHT_MAX = 256; // px 1.740 +CanvasFront.INVALID_SNAPSHOT_IMAGE = { 1.741 + index: -1, 1.742 + width: 0, 1.743 + height: 0, 1.744 + pixels: [] 1.745 +}; 1.746 + 1.747 +/** 1.748 + * Goes through all the arguments and creates a one-level shallow copy 1.749 + * of all arrays and array buffers. 1.750 + */ 1.751 +function inplaceShallowCloneArrays(functionArguments, contentWindow) { 1.752 + let { Object, Array, ArrayBuffer } = contentWindow; 1.753 + 1.754 + functionArguments.forEach((arg, index, store) => { 1.755 + if (arg instanceof Array) { 1.756 + store[index] = arg.slice(); 1.757 + } 1.758 + if (arg instanceof Object && arg.buffer instanceof ArrayBuffer) { 1.759 + store[index] = new arg.constructor(arg); 1.760 + } 1.761 + }); 1.762 +}