Sat, 03 Jan 2015 20:18:00 +0100
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 | } |