toolkit/devtools/server/actors/canvas.js

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4 "use strict";
michael@0 5
michael@0 6 const {Cc, Ci, Cu, Cr} = require("chrome");
michael@0 7 const events = require("sdk/event/core");
michael@0 8 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
michael@0 9 const protocol = require("devtools/server/protocol");
michael@0 10 const {CallWatcherActor, CallWatcherFront} = require("devtools/server/actors/call-watcher");
michael@0 11 const DevToolsUtils = require("devtools/toolkit/DevToolsUtils.js");
michael@0 12
michael@0 13 const {on, once, off, emit} = events;
michael@0 14 const {method, custom, Arg, Option, RetVal} = protocol;
michael@0 15
michael@0 16 const CANVAS_CONTEXTS = [
michael@0 17 "CanvasRenderingContext2D",
michael@0 18 "WebGLRenderingContext"
michael@0 19 ];
michael@0 20
michael@0 21 const ANIMATION_GENERATORS = [
michael@0 22 "requestAnimationFrame",
michael@0 23 "mozRequestAnimationFrame"
michael@0 24 ];
michael@0 25
michael@0 26 const DRAW_CALLS = [
michael@0 27 // 2D canvas
michael@0 28 "fill",
michael@0 29 "stroke",
michael@0 30 "clearRect",
michael@0 31 "fillRect",
michael@0 32 "strokeRect",
michael@0 33 "fillText",
michael@0 34 "strokeText",
michael@0 35 "drawImage",
michael@0 36
michael@0 37 // WebGL
michael@0 38 "clear",
michael@0 39 "drawArrays",
michael@0 40 "drawElements",
michael@0 41 "finish",
michael@0 42 "flush"
michael@0 43 ];
michael@0 44
michael@0 45 const INTERESTING_CALLS = [
michael@0 46 // 2D canvas
michael@0 47 "save",
michael@0 48 "restore",
michael@0 49
michael@0 50 // WebGL
michael@0 51 "useProgram"
michael@0 52 ];
michael@0 53
michael@0 54 exports.register = function(handle) {
michael@0 55 handle.addTabActor(CanvasActor, "canvasActor");
michael@0 56 };
michael@0 57
michael@0 58 exports.unregister = function(handle) {
michael@0 59 handle.removeTabActor(CanvasActor);
michael@0 60 };
michael@0 61
michael@0 62 /**
michael@0 63 * Type representing an Uint32Array buffer, serialized fast(er).
michael@0 64 *
michael@0 65 * XXX: It would be nice if on local connections (only), we could just *give*
michael@0 66 * the buffer directly to the front, instead of going through all this
michael@0 67 * serialization redundancy.
michael@0 68 */
michael@0 69 protocol.types.addType("uint32-array", {
michael@0 70 write: (v) => "[" + Array.join(v, ",") + "]",
michael@0 71 read: (v) => new Uint32Array(JSON.parse(v))
michael@0 72 });
michael@0 73
michael@0 74 /**
michael@0 75 * Type describing a thumbnail or screenshot in a recorded animation frame.
michael@0 76 */
michael@0 77 protocol.types.addDictType("snapshot-image", {
michael@0 78 index: "number",
michael@0 79 width: "number",
michael@0 80 height: "number",
michael@0 81 flipped: "boolean",
michael@0 82 pixels: "uint32-array"
michael@0 83 });
michael@0 84
michael@0 85 /**
michael@0 86 * Type describing an overview of a recorded animation frame.
michael@0 87 */
michael@0 88 protocol.types.addDictType("snapshot-overview", {
michael@0 89 calls: "array:function-call",
michael@0 90 thumbnails: "array:snapshot-image",
michael@0 91 screenshot: "snapshot-image"
michael@0 92 });
michael@0 93
michael@0 94 /**
michael@0 95 * This actor represents a recorded animation frame snapshot, along with
michael@0 96 * all the corresponding canvas' context methods invoked in that frame,
michael@0 97 * thumbnails for each draw call and a screenshot of the end result.
michael@0 98 */
michael@0 99 let FrameSnapshotActor = protocol.ActorClass({
michael@0 100 typeName: "frame-snapshot",
michael@0 101
michael@0 102 /**
michael@0 103 * Creates the frame snapshot call actor.
michael@0 104 *
michael@0 105 * @param DebuggerServerConnection conn
michael@0 106 * The server connection.
michael@0 107 * @param HTMLCanvasElement canvas
michael@0 108 * A reference to the content canvas.
michael@0 109 * @param array calls
michael@0 110 * An array of "function-call" actor instances.
michael@0 111 * @param object screenshot
michael@0 112 * A single "snapshot-image" type instance.
michael@0 113 */
michael@0 114 initialize: function(conn, { canvas, calls, screenshot }) {
michael@0 115 protocol.Actor.prototype.initialize.call(this, conn);
michael@0 116 this._contentCanvas = canvas;
michael@0 117 this._functionCalls = calls;
michael@0 118 this._lastDrawCallScreenshot = screenshot;
michael@0 119 },
michael@0 120
michael@0 121 /**
michael@0 122 * Gets as much data about this snapshot without computing anything costly.
michael@0 123 */
michael@0 124 getOverview: method(function() {
michael@0 125 return {
michael@0 126 calls: this._functionCalls,
michael@0 127 thumbnails: this._functionCalls.map(e => e._thumbnail).filter(e => !!e),
michael@0 128 screenshot: this._lastDrawCallScreenshot
michael@0 129 };
michael@0 130 }, {
michael@0 131 response: { overview: RetVal("snapshot-overview") }
michael@0 132 }),
michael@0 133
michael@0 134 /**
michael@0 135 * Gets a screenshot of the canvas's contents after the specified
michael@0 136 * function was called.
michael@0 137 */
michael@0 138 generateScreenshotFor: method(function(functionCall) {
michael@0 139 let caller = functionCall.details.caller;
michael@0 140 let global = functionCall.meta.global;
michael@0 141
michael@0 142 let canvas = this._contentCanvas;
michael@0 143 let calls = this._functionCalls;
michael@0 144 let index = calls.indexOf(functionCall);
michael@0 145
michael@0 146 // To get a screenshot, replay all the steps necessary to render the frame,
michael@0 147 // by invoking the context calls up to and including the specified one.
michael@0 148 // This will be done in a custom framebuffer in case of a WebGL context.
michael@0 149 let { replayContext, lastDrawCallIndex } = ContextUtils.replayAnimationFrame({
michael@0 150 contextType: global,
michael@0 151 canvas: canvas,
michael@0 152 calls: calls,
michael@0 153 first: 0,
michael@0 154 last: index
michael@0 155 });
michael@0 156
michael@0 157 // To keep things fast, generate an image that's relatively small.
michael@0 158 let dimensions = Math.min(CanvasFront.SCREENSHOT_HEIGHT_MAX, canvas.height);
michael@0 159 let screenshot;
michael@0 160
michael@0 161 // Depending on the canvas' context, generating a screenshot is done
michael@0 162 // in different ways. In case of the WebGL context, we also need to reset
michael@0 163 // the framebuffer binding to the default value.
michael@0 164 if (global == CallWatcherFront.CANVAS_WEBGL_CONTEXT) {
michael@0 165 screenshot = ContextUtils.getPixelsForWebGL(replayContext);
michael@0 166 replayContext.bindFramebuffer(replayContext.FRAMEBUFFER, null);
michael@0 167 screenshot.flipped = true;
michael@0 168 }
michael@0 169 // In case of 2D contexts, no additional special treatment is necessary.
michael@0 170 else if (global == CallWatcherFront.CANVAS_2D_CONTEXT) {
michael@0 171 screenshot = ContextUtils.getPixelsFor2D(replayContext);
michael@0 172 screenshot.flipped = false;
michael@0 173 }
michael@0 174
michael@0 175 screenshot.index = lastDrawCallIndex;
michael@0 176 return screenshot;
michael@0 177 }, {
michael@0 178 request: { call: Arg(0, "function-call") },
michael@0 179 response: { screenshot: RetVal("snapshot-image") }
michael@0 180 })
michael@0 181 });
michael@0 182
michael@0 183 /**
michael@0 184 * The corresponding Front object for the FrameSnapshotActor.
michael@0 185 */
michael@0 186 let FrameSnapshotFront = protocol.FrontClass(FrameSnapshotActor, {
michael@0 187 initialize: function(client, form) {
michael@0 188 protocol.Front.prototype.initialize.call(this, client, form);
michael@0 189 this._lastDrawCallScreenshot = null;
michael@0 190 this._cachedScreenshots = new WeakMap();
michael@0 191 },
michael@0 192
michael@0 193 /**
michael@0 194 * This implementation caches the last draw call screenshot to optimize
michael@0 195 * frontend requests to `generateScreenshotFor`.
michael@0 196 */
michael@0 197 getOverview: custom(function() {
michael@0 198 return this._getOverview().then(data => {
michael@0 199 this._lastDrawCallScreenshot = data.screenshot;
michael@0 200 return data;
michael@0 201 });
michael@0 202 }, {
michael@0 203 impl: "_getOverview"
michael@0 204 }),
michael@0 205
michael@0 206 /**
michael@0 207 * This implementation saves a roundtrip to the backend if the screenshot
michael@0 208 * was already generated and retrieved once.
michael@0 209 */
michael@0 210 generateScreenshotFor: custom(function(functionCall) {
michael@0 211 if (CanvasFront.ANIMATION_GENERATORS.has(functionCall.name)) {
michael@0 212 return promise.resolve(this._lastDrawCallScreenshot);
michael@0 213 }
michael@0 214 let cachedScreenshot = this._cachedScreenshots.get(functionCall);
michael@0 215 if (cachedScreenshot) {
michael@0 216 return cachedScreenshot;
michael@0 217 }
michael@0 218 let screenshot = this._generateScreenshotFor(functionCall);
michael@0 219 this._cachedScreenshots.set(functionCall, screenshot);
michael@0 220 return screenshot;
michael@0 221 }, {
michael@0 222 impl: "_generateScreenshotFor"
michael@0 223 })
michael@0 224 });
michael@0 225
michael@0 226 /**
michael@0 227 * This Canvas Actor handles simple instrumentation of all the methods
michael@0 228 * of a 2D or WebGL context, to provide information regarding all the calls
michael@0 229 * made when drawing frame inside an animation loop.
michael@0 230 */
michael@0 231 let CanvasActor = exports.CanvasActor = protocol.ActorClass({
michael@0 232 typeName: "canvas",
michael@0 233 initialize: function(conn, tabActor) {
michael@0 234 protocol.Actor.prototype.initialize.call(this, conn);
michael@0 235 this.tabActor = tabActor;
michael@0 236 this._onContentFunctionCall = this._onContentFunctionCall.bind(this);
michael@0 237 },
michael@0 238 destroy: function(conn) {
michael@0 239 protocol.Actor.prototype.destroy.call(this, conn);
michael@0 240 this.finalize();
michael@0 241 },
michael@0 242
michael@0 243 /**
michael@0 244 * Starts listening for function calls.
michael@0 245 */
michael@0 246 setup: method(function({ reload }) {
michael@0 247 if (this._initialized) {
michael@0 248 return;
michael@0 249 }
michael@0 250 this._initialized = true;
michael@0 251
michael@0 252 this._callWatcher = new CallWatcherActor(this.conn, this.tabActor);
michael@0 253 this._callWatcher.onCall = this._onContentFunctionCall;
michael@0 254 this._callWatcher.setup({
michael@0 255 tracedGlobals: CANVAS_CONTEXTS,
michael@0 256 tracedFunctions: ANIMATION_GENERATORS,
michael@0 257 performReload: reload
michael@0 258 });
michael@0 259 }, {
michael@0 260 request: { reload: Option(0, "boolean") },
michael@0 261 oneway: true
michael@0 262 }),
michael@0 263
michael@0 264 /**
michael@0 265 * Stops listening for function calls.
michael@0 266 */
michael@0 267 finalize: method(function() {
michael@0 268 if (!this._initialized) {
michael@0 269 return;
michael@0 270 }
michael@0 271 this._initialized = false;
michael@0 272
michael@0 273 this._callWatcher.finalize();
michael@0 274 this._callWatcher = null;
michael@0 275 }, {
michael@0 276 oneway: true
michael@0 277 }),
michael@0 278
michael@0 279 /**
michael@0 280 * Returns whether this actor has been set up.
michael@0 281 */
michael@0 282 isInitialized: method(function() {
michael@0 283 return !!this._initialized;
michael@0 284 }, {
michael@0 285 response: { initialized: RetVal("boolean") }
michael@0 286 }),
michael@0 287
michael@0 288 /**
michael@0 289 * Records a snapshot of all the calls made during the next animation frame.
michael@0 290 * The animation should be implemented via the de-facto requestAnimationFrame
michael@0 291 * utility, not inside a `setInterval` or recursive `setTimeout`.
michael@0 292 *
michael@0 293 * XXX: Currently only supporting requestAnimationFrame. When this isn't used,
michael@0 294 * it'd be a good idea to display a huge red flashing banner telling people to
michael@0 295 * STOP USING `setInterval` OR `setTimeout` FOR ANIMATION. Bug 978948.
michael@0 296 */
michael@0 297 recordAnimationFrame: method(function() {
michael@0 298 if (this._callWatcher.isRecording()) {
michael@0 299 return this._currentAnimationFrameSnapshot.promise;
michael@0 300 }
michael@0 301
michael@0 302 this._callWatcher.eraseRecording();
michael@0 303 this._callWatcher.resumeRecording();
michael@0 304
michael@0 305 let deferred = this._currentAnimationFrameSnapshot = promise.defer();
michael@0 306 return deferred.promise;
michael@0 307 }, {
michael@0 308 response: { snapshot: RetVal("frame-snapshot") }
michael@0 309 }),
michael@0 310
michael@0 311 /**
michael@0 312 * Invoked whenever an instrumented function is called, be it on a
michael@0 313 * 2d or WebGL context, or an animation generator like requestAnimationFrame.
michael@0 314 */
michael@0 315 _onContentFunctionCall: function(functionCall) {
michael@0 316 let { window, name, args } = functionCall.details;
michael@0 317
michael@0 318 // The function call arguments are required to replay animation frames,
michael@0 319 // in order to generate screenshots. However, simply storing references to
michael@0 320 // every kind of object is a bad idea, since their properties may change.
michael@0 321 // Consider transformation matrices for example, which are typically
michael@0 322 // Float32Arrays whose values can easily change across context calls.
michael@0 323 // They need to be cloned.
michael@0 324 inplaceShallowCloneArrays(args, window);
michael@0 325
michael@0 326 if (CanvasFront.ANIMATION_GENERATORS.has(name)) {
michael@0 327 this._handleAnimationFrame(functionCall);
michael@0 328 return;
michael@0 329 }
michael@0 330 if (CanvasFront.DRAW_CALLS.has(name) && this._animationStarted) {
michael@0 331 this._handleDrawCall(functionCall);
michael@0 332 return;
michael@0 333 }
michael@0 334 },
michael@0 335
michael@0 336 /**
michael@0 337 * Handle animations generated using requestAnimationFrame.
michael@0 338 */
michael@0 339 _handleAnimationFrame: function(functionCall) {
michael@0 340 if (!this._animationStarted) {
michael@0 341 this._handleAnimationFrameBegin();
michael@0 342 } else {
michael@0 343 this._handleAnimationFrameEnd(functionCall);
michael@0 344 }
michael@0 345 },
michael@0 346
michael@0 347 /**
michael@0 348 * Called whenever an animation frame rendering begins.
michael@0 349 */
michael@0 350 _handleAnimationFrameBegin: function() {
michael@0 351 this._callWatcher.eraseRecording();
michael@0 352 this._animationStarted = true;
michael@0 353 },
michael@0 354
michael@0 355 /**
michael@0 356 * Called whenever an animation frame rendering ends.
michael@0 357 */
michael@0 358 _handleAnimationFrameEnd: function() {
michael@0 359 // Get a hold of all the function calls made during this animation frame.
michael@0 360 // Since only one snapshot can be recorded at a time, erase all the
michael@0 361 // previously recorded calls.
michael@0 362 let functionCalls = this._callWatcher.pauseRecording();
michael@0 363 this._callWatcher.eraseRecording();
michael@0 364
michael@0 365 // Since the animation frame finished, get a hold of the (already retrieved)
michael@0 366 // canvas pixels to conveniently create a screenshot of the final rendering.
michael@0 367 let index = this._lastDrawCallIndex;
michael@0 368 let width = this._lastContentCanvasWidth;
michael@0 369 let height = this._lastContentCanvasHeight;
michael@0 370 let flipped = this._lastThumbnailFlipped;
michael@0 371 let pixels = ContextUtils.getPixelStorage()["32bit"];
michael@0 372 let lastDrawCallScreenshot = {
michael@0 373 index: index,
michael@0 374 width: width,
michael@0 375 height: height,
michael@0 376 flipped: flipped,
michael@0 377 pixels: pixels.subarray(0, width * height)
michael@0 378 };
michael@0 379
michael@0 380 // Wrap the function calls and screenshot in a FrameSnapshotActor instance,
michael@0 381 // which will resolve the promise returned by `recordAnimationFrame`.
michael@0 382 let frameSnapshot = new FrameSnapshotActor(this.conn, {
michael@0 383 canvas: this._lastDrawCallCanvas,
michael@0 384 calls: functionCalls,
michael@0 385 screenshot: lastDrawCallScreenshot
michael@0 386 });
michael@0 387
michael@0 388 this._currentAnimationFrameSnapshot.resolve(frameSnapshot);
michael@0 389 this._currentAnimationFrameSnapshot = null;
michael@0 390 this._animationStarted = false;
michael@0 391 },
michael@0 392
michael@0 393 /**
michael@0 394 * Invoked whenever a draw call is detected in the animation frame which is
michael@0 395 * currently being recorded.
michael@0 396 */
michael@0 397 _handleDrawCall: function(functionCall) {
michael@0 398 let functionCalls = this._callWatcher.pauseRecording();
michael@0 399 let caller = functionCall.details.caller;
michael@0 400 let global = functionCall.meta.global;
michael@0 401
michael@0 402 let contentCanvas = this._lastDrawCallCanvas = caller.canvas;
michael@0 403 let index = this._lastDrawCallIndex = functionCalls.indexOf(functionCall);
michael@0 404 let w = this._lastContentCanvasWidth = contentCanvas.width;
michael@0 405 let h = this._lastContentCanvasHeight = contentCanvas.height;
michael@0 406
michael@0 407 // To keep things fast, generate images of small and fixed dimensions.
michael@0 408 let dimensions = CanvasFront.THUMBNAIL_HEIGHT;
michael@0 409 let thumbnail;
michael@0 410
michael@0 411 // Create a thumbnail on every draw call on the canvas context, to augment
michael@0 412 // the respective function call actor with this additional data.
michael@0 413 if (global == CallWatcherFront.CANVAS_WEBGL_CONTEXT) {
michael@0 414 // Check if drawing to a custom framebuffer (when rendering to texture).
michael@0 415 // Don't create a thumbnail in this particular case.
michael@0 416 let framebufferBinding = caller.getParameter(caller.FRAMEBUFFER_BINDING);
michael@0 417 if (framebufferBinding == null) {
michael@0 418 thumbnail = ContextUtils.getPixelsForWebGL(caller, 0, 0, w, h, dimensions);
michael@0 419 thumbnail.flipped = this._lastThumbnailFlipped = true;
michael@0 420 thumbnail.index = index;
michael@0 421 }
michael@0 422 } else if (global == CallWatcherFront.CANVAS_2D_CONTEXT) {
michael@0 423 thumbnail = ContextUtils.getPixelsFor2D(caller, 0, 0, w, h, dimensions);
michael@0 424 thumbnail.flipped = this._lastThumbnailFlipped = false;
michael@0 425 thumbnail.index = index;
michael@0 426 }
michael@0 427
michael@0 428 functionCall._thumbnail = thumbnail;
michael@0 429 this._callWatcher.resumeRecording();
michael@0 430 }
michael@0 431 });
michael@0 432
michael@0 433 /**
michael@0 434 * A collection of methods for manipulating canvas contexts.
michael@0 435 */
michael@0 436 let ContextUtils = {
michael@0 437 /**
michael@0 438 * WebGL contexts are sensitive to how they're queried. Use this function
michael@0 439 * to make sure the right context is always retrieved, if available.
michael@0 440 *
michael@0 441 * @param HTMLCanvasElement canvas
michael@0 442 * The canvas element for which to get a WebGL context.
michael@0 443 * @param WebGLRenderingContext gl
michael@0 444 * The queried WebGL context, or null if unavailable.
michael@0 445 */
michael@0 446 getWebGLContext: function(canvas) {
michael@0 447 return canvas.getContext("webgl") ||
michael@0 448 canvas.getContext("experimental-webgl");
michael@0 449 },
michael@0 450
michael@0 451 /**
michael@0 452 * Gets a hold of the rendered pixels in the most efficient way possible for
michael@0 453 * a canvas with a WebGL context.
michael@0 454 *
michael@0 455 * @param WebGLRenderingContext gl
michael@0 456 * The WebGL context to get a screenshot from.
michael@0 457 * @param number srcX [optional]
michael@0 458 * The first left pixel that is read from the framebuffer.
michael@0 459 * @param number srcY [optional]
michael@0 460 * The first top pixel that is read from the framebuffer.
michael@0 461 * @param number srcWidth [optional]
michael@0 462 * The number of pixels to read on the X axis.
michael@0 463 * @param number srcHeight [optional]
michael@0 464 * The number of pixels to read on the Y axis.
michael@0 465 * @param number dstHeight [optional]
michael@0 466 * The desired generated screenshot height.
michael@0 467 * @return object
michael@0 468 * An objet containing the screenshot's width, height and pixel data.
michael@0 469 */
michael@0 470 getPixelsForWebGL: function(gl,
michael@0 471 srcX = 0, srcY = 0,
michael@0 472 srcWidth = gl.canvas.width,
michael@0 473 srcHeight = gl.canvas.height,
michael@0 474 dstHeight = srcHeight)
michael@0 475 {
michael@0 476 let contentPixels = ContextUtils.getPixelStorage(srcWidth, srcHeight);
michael@0 477 let { "8bit": charView, "32bit": intView } = contentPixels;
michael@0 478 gl.readPixels(srcX, srcY, srcWidth, srcHeight, gl.RGBA, gl.UNSIGNED_BYTE, charView);
michael@0 479 return this.resizePixels(intView, srcWidth, srcHeight, dstHeight);
michael@0 480 },
michael@0 481
michael@0 482 /**
michael@0 483 * Gets a hold of the rendered pixels in the most efficient way possible for
michael@0 484 * a canvas with a 2D context.
michael@0 485 *
michael@0 486 * @param CanvasRenderingContext2D ctx
michael@0 487 * The 2D context to get a screenshot from.
michael@0 488 * @param number srcX [optional]
michael@0 489 * The first left pixel that is read from the canvas.
michael@0 490 * @param number srcY [optional]
michael@0 491 * The first top pixel that is read from the canvas.
michael@0 492 * @param number srcWidth [optional]
michael@0 493 * The number of pixels to read on the X axis.
michael@0 494 * @param number srcHeight [optional]
michael@0 495 * The number of pixels to read on the Y axis.
michael@0 496 * @param number dstHeight [optional]
michael@0 497 * The desired generated screenshot height.
michael@0 498 * @return object
michael@0 499 * An objet containing the screenshot's width, height and pixel data.
michael@0 500 */
michael@0 501 getPixelsFor2D: function(ctx,
michael@0 502 srcX = 0, srcY = 0,
michael@0 503 srcWidth = ctx.canvas.width,
michael@0 504 srcHeight = ctx.canvas.height,
michael@0 505 dstHeight = srcHeight)
michael@0 506 {
michael@0 507 let { data } = ctx.getImageData(srcX, srcY, srcWidth, srcHeight);
michael@0 508 let { "32bit": intView } = ContextUtils.usePixelStorage(data.buffer);
michael@0 509 return this.resizePixels(intView, srcWidth, srcHeight, dstHeight);
michael@0 510 },
michael@0 511
michael@0 512 /**
michael@0 513 * Resizes the provided pixels to fit inside a rectangle with the specified
michael@0 514 * height and the same aspect ratio as the source.
michael@0 515 *
michael@0 516 * @param Uint32Array srcPixels
michael@0 517 * The source pixel data, assuming 32bit/pixel and 4 color components.
michael@0 518 * @param number srcWidth
michael@0 519 * The source pixel data width.
michael@0 520 * @param number srcHeight
michael@0 521 * The source pixel data height.
michael@0 522 * @param number dstHeight [optional]
michael@0 523 * The desired resized pixel data height.
michael@0 524 * @return object
michael@0 525 * An objet containing the resized pixels width, height and data.
michael@0 526 */
michael@0 527 resizePixels: function(srcPixels, srcWidth, srcHeight, dstHeight) {
michael@0 528 let screenshotRatio = dstHeight / srcHeight;
michael@0 529 let dstWidth = Math.floor(srcWidth * screenshotRatio);
michael@0 530
michael@0 531 // Use a plain array instead of a Uint32Array to make serializing faster.
michael@0 532 let dstPixels = new Array(dstWidth * dstHeight);
michael@0 533
michael@0 534 // If the resized image ends up being completely transparent, returning
michael@0 535 // an empty array will skip some redundant serialization cycles.
michael@0 536 let isTransparent = true;
michael@0 537
michael@0 538 for (let dstX = 0; dstX < dstWidth; dstX++) {
michael@0 539 for (let dstY = 0; dstY < dstHeight; dstY++) {
michael@0 540 let srcX = Math.floor(dstX / screenshotRatio);
michael@0 541 let srcY = Math.floor(dstY / screenshotRatio);
michael@0 542 let cPos = srcX + srcWidth * srcY;
michael@0 543 let dPos = dstX + dstWidth * dstY;
michael@0 544 let color = dstPixels[dPos] = srcPixels[cPos];
michael@0 545 if (color) {
michael@0 546 isTransparent = false;
michael@0 547 }
michael@0 548 }
michael@0 549 }
michael@0 550
michael@0 551 return {
michael@0 552 width: dstWidth,
michael@0 553 height: dstHeight,
michael@0 554 pixels: isTransparent ? [] : dstPixels
michael@0 555 };
michael@0 556 },
michael@0 557
michael@0 558 /**
michael@0 559 * Invokes a series of canvas context calls, to "replay" an animation frame
michael@0 560 * and generate a screenshot.
michael@0 561 *
michael@0 562 * In case of a WebGL context, an offscreen framebuffer is created for
michael@0 563 * the respective canvas, and the rendering will be performed into it.
michael@0 564 * This is necessary because some state (like shaders, textures etc.) can't
michael@0 565 * be shared between two different WebGL contexts.
michael@0 566 * Hopefully, once SharedResources are a thing this won't be necessary:
michael@0 567 * http://www.khronos.org/webgl/wiki/SharedResouces
michael@0 568 *
michael@0 569 * In case of a 2D context, a new canvas is created, since there's no
michael@0 570 * intrinsic state that can't be easily duplicated.
michael@0 571 *
michael@0 572 * @param number contexType
michael@0 573 * The type of context to use. See the CallWatcherFront scope types.
michael@0 574 * @param HTMLCanvasElement canvas
michael@0 575 * The canvas element which is the source of all context calls.
michael@0 576 * @param array calls
michael@0 577 * An array of function call actors.
michael@0 578 * @param number first
michael@0 579 * The first function call to start from.
michael@0 580 * @param number last
michael@0 581 * The last (inclusive) function call to end at.
michael@0 582 * @return object
michael@0 583 * The context on which the specified calls were invoked and the
michael@0 584 * last registered draw call's index.
michael@0 585 */
michael@0 586 replayAnimationFrame: function({ contextType, canvas, calls, first, last }) {
michael@0 587 let w = canvas.width;
michael@0 588 let h = canvas.height;
michael@0 589
michael@0 590 let replayCanvas;
michael@0 591 let replayContext;
michael@0 592 let customFramebuffer;
michael@0 593 let lastDrawCallIndex = -1;
michael@0 594
michael@0 595 // In case of WebGL contexts, rendering will be done offscreen, in a
michael@0 596 // custom framebuffer, but on the provided canvas context.
michael@0 597 if (contextType == CallWatcherFront.CANVAS_WEBGL_CONTEXT) {
michael@0 598 replayCanvas = canvas;
michael@0 599 replayContext = this.getWebGLContext(replayCanvas);
michael@0 600 customFramebuffer = this.createBoundFramebuffer(replayContext, w, h);
michael@0 601 }
michael@0 602 // In case of 2D contexts, draw everything on a separate canvas context.
michael@0 603 else if (contextType == CallWatcherFront.CANVAS_2D_CONTEXT) {
michael@0 604 let contentDocument = canvas.ownerDocument;
michael@0 605 replayCanvas = contentDocument.createElement("canvas");
michael@0 606 replayCanvas.width = w;
michael@0 607 replayCanvas.height = h;
michael@0 608 replayContext = replayCanvas.getContext("2d");
michael@0 609 replayContext.clearRect(0, 0, w, h);
michael@0 610 }
michael@0 611
michael@0 612 // Replay all the context calls up to and including the specified one.
michael@0 613 for (let i = first; i <= last; i++) {
michael@0 614 let { type, name, args } = calls[i].details;
michael@0 615
michael@0 616 // Prevent WebGL context calls that try to reset the framebuffer binding
michael@0 617 // to the default value, since we want to perform the rendering offscreen.
michael@0 618 if (name == "bindFramebuffer" && args[1] == null) {
michael@0 619 replayContext.bindFramebuffer(replayContext.FRAMEBUFFER, customFramebuffer);
michael@0 620 } else {
michael@0 621 if (type == CallWatcherFront.METHOD_FUNCTION) {
michael@0 622 replayContext[name].apply(replayContext, args);
michael@0 623 } else if (type == CallWatcherFront.SETTER_FUNCTION) {
michael@0 624 replayContext[name] = args;
michael@0 625 } else {
michael@0 626 // Ignore getter calls.
michael@0 627 }
michael@0 628 if (CanvasFront.DRAW_CALLS.has(name)) {
michael@0 629 lastDrawCallIndex = i;
michael@0 630 }
michael@0 631 }
michael@0 632 }
michael@0 633
michael@0 634 return {
michael@0 635 replayContext: replayContext,
michael@0 636 lastDrawCallIndex: lastDrawCallIndex
michael@0 637 };
michael@0 638 },
michael@0 639
michael@0 640 /**
michael@0 641 * Gets an object containing a buffer large enough to hold width * height
michael@0 642 * pixels, assuming 32bit/pixel and 4 color components.
michael@0 643 *
michael@0 644 * This method avoids allocating memory and tries to reuse a common buffer
michael@0 645 * as much as possible.
michael@0 646 *
michael@0 647 * @param number w
michael@0 648 * The desired pixel array storage width.
michael@0 649 * @param number h
michael@0 650 * The desired pixel array storage height.
michael@0 651 * @return object
michael@0 652 * The requested pixel array buffer.
michael@0 653 */
michael@0 654 getPixelStorage: function(w = 0, h = 0) {
michael@0 655 let storage = this._currentPixelStorage;
michael@0 656 if (storage && storage["32bit"].length >= w * h) {
michael@0 657 return storage;
michael@0 658 }
michael@0 659 return this.usePixelStorage(new ArrayBuffer(w * h * 4));
michael@0 660 },
michael@0 661
michael@0 662 /**
michael@0 663 * Creates and saves the array buffer views used by `getPixelStorage`.
michael@0 664 *
michael@0 665 * @param ArrayBuffer buffer
michael@0 666 * The raw buffer used as storage for various array buffer views.
michael@0 667 */
michael@0 668 usePixelStorage: function(buffer) {
michael@0 669 let array8bit = new Uint8Array(buffer);
michael@0 670 let array32bit = new Uint32Array(buffer);
michael@0 671 return this._currentPixelStorage = {
michael@0 672 "8bit": array8bit,
michael@0 673 "32bit": array32bit
michael@0 674 };
michael@0 675 },
michael@0 676
michael@0 677 /**
michael@0 678 * Creates a framebuffer of the specified dimensions for a WebGL context,
michael@0 679 * assuming a RGBA color buffer, a depth buffer and no stencil buffer.
michael@0 680 *
michael@0 681 * @param WebGLRenderingContext gl
michael@0 682 * The WebGL context to create and bind a framebuffer for.
michael@0 683 * @param number width
michael@0 684 * The desired width of the renderbuffers.
michael@0 685 * @param number height
michael@0 686 * The desired height of the renderbuffers.
michael@0 687 * @return WebGLFramebuffer
michael@0 688 * The generated framebuffer object.
michael@0 689 */
michael@0 690 createBoundFramebuffer: function(gl, width, height) {
michael@0 691 let framebuffer = gl.createFramebuffer();
michael@0 692 gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
michael@0 693
michael@0 694 // Use a texture as the color rendebuffer attachment, since consumenrs of
michael@0 695 // this function will most likely want to read the rendered pixels back.
michael@0 696 let colorBuffer = gl.createTexture();
michael@0 697 gl.bindTexture(gl.TEXTURE_2D, colorBuffer);
michael@0 698 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
michael@0 699 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
michael@0 700 gl.generateMipmap(gl.TEXTURE_2D);
michael@0 701 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
michael@0 702
michael@0 703 let depthBuffer = gl.createRenderbuffer();
michael@0 704 gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);
michael@0 705 gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height);
michael@0 706
michael@0 707 gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, colorBuffer, 0);
michael@0 708 gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer);
michael@0 709
michael@0 710 gl.bindTexture(gl.TEXTURE_2D, null);
michael@0 711 gl.bindRenderbuffer(gl.RENDERBUFFER, null);
michael@0 712
michael@0 713 return framebuffer;
michael@0 714 }
michael@0 715 };
michael@0 716
michael@0 717 /**
michael@0 718 * The corresponding Front object for the CanvasActor.
michael@0 719 */
michael@0 720 let CanvasFront = exports.CanvasFront = protocol.FrontClass(CanvasActor, {
michael@0 721 initialize: function(client, { canvasActor }) {
michael@0 722 protocol.Front.prototype.initialize.call(this, client, { actor: canvasActor });
michael@0 723 client.addActorPool(this);
michael@0 724 this.manage(this);
michael@0 725 }
michael@0 726 });
michael@0 727
michael@0 728 /**
michael@0 729 * Constants.
michael@0 730 */
michael@0 731 CanvasFront.CANVAS_CONTEXTS = new Set(CANVAS_CONTEXTS);
michael@0 732 CanvasFront.ANIMATION_GENERATORS = new Set(ANIMATION_GENERATORS);
michael@0 733 CanvasFront.DRAW_CALLS = new Set(DRAW_CALLS);
michael@0 734 CanvasFront.INTERESTING_CALLS = new Set(INTERESTING_CALLS);
michael@0 735 CanvasFront.THUMBNAIL_HEIGHT = 50; // px
michael@0 736 CanvasFront.SCREENSHOT_HEIGHT_MAX = 256; // px
michael@0 737 CanvasFront.INVALID_SNAPSHOT_IMAGE = {
michael@0 738 index: -1,
michael@0 739 width: 0,
michael@0 740 height: 0,
michael@0 741 pixels: []
michael@0 742 };
michael@0 743
michael@0 744 /**
michael@0 745 * Goes through all the arguments and creates a one-level shallow copy
michael@0 746 * of all arrays and array buffers.
michael@0 747 */
michael@0 748 function inplaceShallowCloneArrays(functionArguments, contentWindow) {
michael@0 749 let { Object, Array, ArrayBuffer } = contentWindow;
michael@0 750
michael@0 751 functionArguments.forEach((arg, index, store) => {
michael@0 752 if (arg instanceof Array) {
michael@0 753 store[index] = arg.slice();
michael@0 754 }
michael@0 755 if (arg instanceof Object && arg.buffer instanceof ArrayBuffer) {
michael@0 756 store[index] = new arg.constructor(arg);
michael@0 757 }
michael@0 758 });
michael@0 759 }

mercurial