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