canvas.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729
  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. const {Cc, Ci, Cu, Cr} = require("chrome");
  6. const events = require("sdk/event/core");
  7. const promise = require("promise");
  8. const protocol = require("devtools/shared/protocol");
  9. const {CallWatcherActor} = require("devtools/server/actors/call-watcher");
  10. const {CallWatcherFront} = require("devtools/shared/fronts/call-watcher");
  11. const DevToolsUtils = require("devtools/shared/DevToolsUtils");
  12. const {WebGLPrimitiveCounter} = require("devtools/server/primitive");
  13. const {
  14. frameSnapshotSpec,
  15. canvasSpec,
  16. CANVAS_CONTEXTS,
  17. ANIMATION_GENERATORS,
  18. LOOP_GENERATORS,
  19. DRAW_CALLS,
  20. INTERESTING_CALLS,
  21. } = require("devtools/shared/specs/canvas");
  22. const {CanvasFront} = require("devtools/shared/fronts/canvas");
  23. const {on, once, off, emit} = events;
  24. const {method, custom, Arg, Option, RetVal} = protocol;
  25. /**
  26. * This actor represents a recorded animation frame snapshot, along with
  27. * all the corresponding canvas' context methods invoked in that frame,
  28. * thumbnails for each draw call and a screenshot of the end result.
  29. */
  30. var FrameSnapshotActor = protocol.ActorClassWithSpec(frameSnapshotSpec, {
  31. /**
  32. * Creates the frame snapshot call actor.
  33. *
  34. * @param DebuggerServerConnection conn
  35. * The server connection.
  36. * @param HTMLCanvasElement canvas
  37. * A reference to the content canvas.
  38. * @param array calls
  39. * An array of "function-call" actor instances.
  40. * @param object screenshot
  41. * A single "snapshot-image" type instance.
  42. */
  43. initialize: function (conn, { canvas, calls, screenshot, primitive }) {
  44. protocol.Actor.prototype.initialize.call(this, conn);
  45. this._contentCanvas = canvas;
  46. this._functionCalls = calls;
  47. this._animationFrameEndScreenshot = screenshot;
  48. this._primitive = primitive;
  49. },
  50. /**
  51. * Gets as much data about this snapshot without computing anything costly.
  52. */
  53. getOverview: function () {
  54. return {
  55. calls: this._functionCalls,
  56. thumbnails: this._functionCalls.map(e => e._thumbnail).filter(e => !!e),
  57. screenshot: this._animationFrameEndScreenshot,
  58. primitive: {
  59. tris: this._primitive.tris,
  60. vertices: this._primitive.vertices,
  61. points: this._primitive.points,
  62. lines: this._primitive.lines
  63. }
  64. };
  65. },
  66. /**
  67. * Gets a screenshot of the canvas's contents after the specified
  68. * function was called.
  69. */
  70. generateScreenshotFor: function (functionCall) {
  71. let caller = functionCall.details.caller;
  72. let global = functionCall.details.global;
  73. let canvas = this._contentCanvas;
  74. let calls = this._functionCalls;
  75. let index = calls.indexOf(functionCall);
  76. // To get a screenshot, replay all the steps necessary to render the frame,
  77. // by invoking the context calls up to and including the specified one.
  78. // This will be done in a custom framebuffer in case of a WebGL context.
  79. let replayData = ContextUtils.replayAnimationFrame({
  80. contextType: global,
  81. canvas: canvas,
  82. calls: calls,
  83. first: 0,
  84. last: index
  85. });
  86. let { replayContext, replayContextScaling, lastDrawCallIndex, doCleanup } = replayData;
  87. let [left, top, width, height] = replayData.replayViewport;
  88. let screenshot;
  89. // Depending on the canvas' context, generating a screenshot is done
  90. // in different ways.
  91. if (global == "WebGLRenderingContext") {
  92. screenshot = ContextUtils.getPixelsForWebGL(replayContext, left, top, width, height);
  93. screenshot.flipped = true;
  94. } else if (global == "CanvasRenderingContext2D") {
  95. screenshot = ContextUtils.getPixelsFor2D(replayContext, left, top, width, height);
  96. screenshot.flipped = false;
  97. }
  98. // In case of the WebGL context, we also need to reset the framebuffer
  99. // binding to the original value, after generating the screenshot.
  100. doCleanup();
  101. screenshot.scaling = replayContextScaling;
  102. screenshot.index = lastDrawCallIndex;
  103. return screenshot;
  104. }
  105. });
  106. /**
  107. * This Canvas Actor handles simple instrumentation of all the methods
  108. * of a 2D or WebGL context, to provide information regarding all the calls
  109. * made when drawing frame inside an animation loop.
  110. */
  111. var CanvasActor = exports.CanvasActor = protocol.ActorClassWithSpec(canvasSpec, {
  112. // Reset for each recording, boolean indicating whether or not
  113. // any draw calls were called for a recording.
  114. _animationContainsDrawCall: false,
  115. initialize: function (conn, tabActor) {
  116. protocol.Actor.prototype.initialize.call(this, conn);
  117. this.tabActor = tabActor;
  118. this._webGLPrimitiveCounter = new WebGLPrimitiveCounter(tabActor);
  119. this._onContentFunctionCall = this._onContentFunctionCall.bind(this);
  120. },
  121. destroy: function (conn) {
  122. protocol.Actor.prototype.destroy.call(this, conn);
  123. this._webGLPrimitiveCounter.destroy();
  124. this.finalize();
  125. },
  126. /**
  127. * Starts listening for function calls.
  128. */
  129. setup: function ({ reload }) {
  130. if (this._initialized) {
  131. if (reload) {
  132. this.tabActor.window.location.reload();
  133. }
  134. return;
  135. }
  136. this._initialized = true;
  137. this._callWatcher = new CallWatcherActor(this.conn, this.tabActor);
  138. this._callWatcher.onCall = this._onContentFunctionCall;
  139. this._callWatcher.setup({
  140. tracedGlobals: CANVAS_CONTEXTS,
  141. tracedFunctions: [...ANIMATION_GENERATORS, ...LOOP_GENERATORS],
  142. performReload: reload,
  143. storeCalls: true
  144. });
  145. },
  146. /**
  147. * Stops listening for function calls.
  148. */
  149. finalize: function () {
  150. if (!this._initialized) {
  151. return;
  152. }
  153. this._initialized = false;
  154. this._callWatcher.finalize();
  155. this._callWatcher = null;
  156. },
  157. /**
  158. * Returns whether this actor has been set up.
  159. */
  160. isInitialized: function () {
  161. return !!this._initialized;
  162. },
  163. /**
  164. * Returns whether or not the CanvasActor is recording an animation.
  165. * Used in tests.
  166. */
  167. isRecording: function () {
  168. return !!this._callWatcher.isRecording();
  169. },
  170. /**
  171. * Records a snapshot of all the calls made during the next animation frame.
  172. * The animation should be implemented via the de-facto requestAnimationFrame
  173. * utility, or inside recursive `setTimeout`s. `setInterval` at this time are not supported.
  174. */
  175. recordAnimationFrame: function () {
  176. if (this._callWatcher.isRecording()) {
  177. return this._currentAnimationFrameSnapshot.promise;
  178. }
  179. this._recordingContainsDrawCall = false;
  180. this._callWatcher.eraseRecording();
  181. this._callWatcher.initTimestampEpoch();
  182. this._webGLPrimitiveCounter.resetCounts();
  183. this._callWatcher.resumeRecording();
  184. let deferred = this._currentAnimationFrameSnapshot = promise.defer();
  185. return deferred.promise;
  186. },
  187. /**
  188. * Cease attempts to record an animation frame.
  189. */
  190. stopRecordingAnimationFrame: function () {
  191. if (!this._callWatcher.isRecording()) {
  192. return;
  193. }
  194. this._animationStarted = false;
  195. this._callWatcher.pauseRecording();
  196. this._callWatcher.eraseRecording();
  197. this._currentAnimationFrameSnapshot.resolve(null);
  198. this._currentAnimationFrameSnapshot = null;
  199. },
  200. /**
  201. * Invoked whenever an instrumented function is called, be it on a
  202. * 2d or WebGL context, or an animation generator like requestAnimationFrame.
  203. */
  204. _onContentFunctionCall: function (functionCall) {
  205. let { window, name, args } = functionCall.details;
  206. // The function call arguments are required to replay animation frames,
  207. // in order to generate screenshots. However, simply storing references to
  208. // every kind of object is a bad idea, since their properties may change.
  209. // Consider transformation matrices for example, which are typically
  210. // Float32Arrays whose values can easily change across context calls.
  211. // They need to be cloned.
  212. inplaceShallowCloneArrays(args, window);
  213. // Handle animations generated using requestAnimationFrame
  214. if (CanvasFront.ANIMATION_GENERATORS.has(name)) {
  215. this._handleAnimationFrame(functionCall);
  216. return;
  217. }
  218. // Handle animations generated using setTimeout. While using
  219. // those timers is considered extremely poor practice, they're still widely
  220. // used on the web, especially for old demos; it's nice to support them as well.
  221. if (CanvasFront.LOOP_GENERATORS.has(name)) {
  222. this._handleAnimationFrame(functionCall);
  223. return;
  224. }
  225. if (CanvasFront.DRAW_CALLS.has(name) && this._animationStarted) {
  226. this._handleDrawCall(functionCall);
  227. this._webGLPrimitiveCounter.handleDrawPrimitive(functionCall);
  228. return;
  229. }
  230. },
  231. /**
  232. * Handle animations generated using requestAnimationFrame.
  233. */
  234. _handleAnimationFrame: function (functionCall) {
  235. if (!this._animationStarted) {
  236. this._handleAnimationFrameBegin();
  237. }
  238. // Check to see if draw calls occurred yet, as it could be future frames,
  239. // like in the scenario where requestAnimationFrame is called to trigger an animation,
  240. // and rAF is at the beginning of the animate loop.
  241. else if (this._animationContainsDrawCall) {
  242. this._handleAnimationFrameEnd(functionCall);
  243. }
  244. },
  245. /**
  246. * Called whenever an animation frame rendering begins.
  247. */
  248. _handleAnimationFrameBegin: function () {
  249. this._callWatcher.eraseRecording();
  250. this._animationStarted = true;
  251. },
  252. /**
  253. * Called whenever an animation frame rendering ends.
  254. */
  255. _handleAnimationFrameEnd: function () {
  256. // Get a hold of all the function calls made during this animation frame.
  257. // Since only one snapshot can be recorded at a time, erase all the
  258. // previously recorded calls.
  259. let functionCalls = this._callWatcher.pauseRecording();
  260. this._callWatcher.eraseRecording();
  261. this._animationContainsDrawCall = false;
  262. // Since the animation frame finished, get a hold of the (already retrieved)
  263. // canvas pixels to conveniently create a screenshot of the final rendering.
  264. let index = this._lastDrawCallIndex;
  265. let width = this._lastContentCanvasWidth;
  266. let height = this._lastContentCanvasHeight;
  267. let flipped = !!this._lastThumbnailFlipped; // undefined -> false
  268. let pixels = ContextUtils.getPixelStorage()["8bit"];
  269. let primitiveResult = this._webGLPrimitiveCounter.getCounts();
  270. let animationFrameEndScreenshot = {
  271. index: index,
  272. width: width,
  273. height: height,
  274. scaling: 1,
  275. flipped: flipped,
  276. pixels: pixels.subarray(0, width * height * 4)
  277. };
  278. // Wrap the function calls and screenshot in a FrameSnapshotActor instance,
  279. // which will resolve the promise returned by `recordAnimationFrame`.
  280. let frameSnapshot = new FrameSnapshotActor(this.conn, {
  281. canvas: this._lastDrawCallCanvas,
  282. calls: functionCalls,
  283. screenshot: animationFrameEndScreenshot,
  284. primitive: {
  285. tris: primitiveResult.tris,
  286. vertices: primitiveResult.vertices,
  287. points: primitiveResult.points,
  288. lines: primitiveResult.lines
  289. }
  290. });
  291. this._currentAnimationFrameSnapshot.resolve(frameSnapshot);
  292. this._currentAnimationFrameSnapshot = null;
  293. this._animationStarted = false;
  294. },
  295. /**
  296. * Invoked whenever a draw call is detected in the animation frame which is
  297. * currently being recorded.
  298. */
  299. _handleDrawCall: function (functionCall) {
  300. let functionCalls = this._callWatcher.pauseRecording();
  301. let caller = functionCall.details.caller;
  302. let global = functionCall.details.global;
  303. let contentCanvas = this._lastDrawCallCanvas = caller.canvas;
  304. let index = this._lastDrawCallIndex = functionCalls.indexOf(functionCall);
  305. let w = this._lastContentCanvasWidth = contentCanvas.width;
  306. let h = this._lastContentCanvasHeight = contentCanvas.height;
  307. // To keep things fast, generate images of small and fixed dimensions.
  308. let dimensions = CanvasFront.THUMBNAIL_SIZE;
  309. let thumbnail;
  310. this._animationContainsDrawCall = true;
  311. // Create a thumbnail on every draw call on the canvas context, to augment
  312. // the respective function call actor with this additional data.
  313. if (global == "WebGLRenderingContext") {
  314. // Check if drawing to a custom framebuffer (when rendering to texture).
  315. // Don't create a thumbnail in this particular case.
  316. let framebufferBinding = caller.getParameter(caller.FRAMEBUFFER_BINDING);
  317. if (framebufferBinding == null) {
  318. thumbnail = ContextUtils.getPixelsForWebGL(caller, 0, 0, w, h, dimensions);
  319. thumbnail.flipped = this._lastThumbnailFlipped = true;
  320. thumbnail.index = index;
  321. }
  322. } else if (global == "CanvasRenderingContext2D") {
  323. thumbnail = ContextUtils.getPixelsFor2D(caller, 0, 0, w, h, dimensions);
  324. thumbnail.flipped = this._lastThumbnailFlipped = false;
  325. thumbnail.index = index;
  326. }
  327. functionCall._thumbnail = thumbnail;
  328. this._callWatcher.resumeRecording();
  329. }
  330. });
  331. /**
  332. * A collection of methods for manipulating canvas contexts.
  333. */
  334. var ContextUtils = {
  335. /**
  336. * WebGL contexts are sensitive to how they're queried. Use this function
  337. * to make sure the right context is always retrieved, if available.
  338. *
  339. * @param HTMLCanvasElement canvas
  340. * The canvas element for which to get a WebGL context.
  341. * @param WebGLRenderingContext gl
  342. * The queried WebGL context, or null if unavailable.
  343. */
  344. getWebGLContext: function (canvas) {
  345. return canvas.getContext("webgl") ||
  346. canvas.getContext("experimental-webgl");
  347. },
  348. /**
  349. * Gets a hold of the rendered pixels in the most efficient way possible for
  350. * a canvas with a WebGL context.
  351. *
  352. * @param WebGLRenderingContext gl
  353. * The WebGL context to get a screenshot from.
  354. * @param number srcX [optional]
  355. * The first left pixel that is read from the framebuffer.
  356. * @param number srcY [optional]
  357. * The first top pixel that is read from the framebuffer.
  358. * @param number srcWidth [optional]
  359. * The number of pixels to read on the X axis.
  360. * @param number srcHeight [optional]
  361. * The number of pixels to read on the Y axis.
  362. * @param number dstHeight [optional]
  363. * The desired generated screenshot height.
  364. * @return object
  365. * An objet containing the screenshot's width, height and pixel data,
  366. * represented as an 8-bit array buffer of r, g, b, a values.
  367. */
  368. getPixelsForWebGL: function (gl,
  369. srcX = 0, srcY = 0,
  370. srcWidth = gl.canvas.width,
  371. srcHeight = gl.canvas.height,
  372. dstHeight = srcHeight)
  373. {
  374. let contentPixels = ContextUtils.getPixelStorage(srcWidth, srcHeight);
  375. let { "8bit": charView, "32bit": intView } = contentPixels;
  376. gl.readPixels(srcX, srcY, srcWidth, srcHeight, gl.RGBA, gl.UNSIGNED_BYTE, charView);
  377. return this.resizePixels(intView, srcWidth, srcHeight, dstHeight);
  378. },
  379. /**
  380. * Gets a hold of the rendered pixels in the most efficient way possible for
  381. * a canvas with a 2D context.
  382. *
  383. * @param CanvasRenderingContext2D ctx
  384. * The 2D context to get a screenshot from.
  385. * @param number srcX [optional]
  386. * The first left pixel that is read from the canvas.
  387. * @param number srcY [optional]
  388. * The first top pixel that is read from the canvas.
  389. * @param number srcWidth [optional]
  390. * The number of pixels to read on the X axis.
  391. * @param number srcHeight [optional]
  392. * The number of pixels to read on the Y axis.
  393. * @param number dstHeight [optional]
  394. * The desired generated screenshot height.
  395. * @return object
  396. * An objet containing the screenshot's width, height and pixel data,
  397. * represented as an 8-bit array buffer of r, g, b, a values.
  398. */
  399. getPixelsFor2D: function (ctx,
  400. srcX = 0, srcY = 0,
  401. srcWidth = ctx.canvas.width,
  402. srcHeight = ctx.canvas.height,
  403. dstHeight = srcHeight)
  404. {
  405. let { data } = ctx.getImageData(srcX, srcY, srcWidth, srcHeight);
  406. let { "32bit": intView } = ContextUtils.usePixelStorage(data.buffer);
  407. return this.resizePixels(intView, srcWidth, srcHeight, dstHeight);
  408. },
  409. /**
  410. * Resizes the provided pixels to fit inside a rectangle with the specified
  411. * height and the same aspect ratio as the source.
  412. *
  413. * @param Uint32Array srcPixels
  414. * The source pixel data, assuming 32bit/pixel and 4 color components.
  415. * @param number srcWidth
  416. * The source pixel data width.
  417. * @param number srcHeight
  418. * The source pixel data height.
  419. * @param number dstHeight [optional]
  420. * The desired resized pixel data height.
  421. * @return object
  422. * An objet containing the resized pixels width, height and data,
  423. * represented as an 8-bit array buffer of r, g, b, a values.
  424. */
  425. resizePixels: function (srcPixels, srcWidth, srcHeight, dstHeight) {
  426. let screenshotRatio = dstHeight / srcHeight;
  427. let dstWidth = (srcWidth * screenshotRatio) | 0;
  428. let dstPixels = new Uint32Array(dstWidth * dstHeight);
  429. // If the resized image ends up being completely transparent, returning
  430. // an empty array will skip some redundant serialization cycles.
  431. let isTransparent = true;
  432. for (let dstX = 0; dstX < dstWidth; dstX++) {
  433. for (let dstY = 0; dstY < dstHeight; dstY++) {
  434. let srcX = (dstX / screenshotRatio) | 0;
  435. let srcY = (dstY / screenshotRatio) | 0;
  436. let cPos = srcX + srcWidth * srcY;
  437. let dPos = dstX + dstWidth * dstY;
  438. let color = dstPixels[dPos] = srcPixels[cPos];
  439. if (color) {
  440. isTransparent = false;
  441. }
  442. }
  443. }
  444. return {
  445. width: dstWidth,
  446. height: dstHeight,
  447. pixels: isTransparent ? [] : new Uint8Array(dstPixels.buffer)
  448. };
  449. },
  450. /**
  451. * Invokes a series of canvas context calls, to "replay" an animation frame
  452. * and generate a screenshot.
  453. *
  454. * In case of a WebGL context, an offscreen framebuffer is created for
  455. * the respective canvas, and the rendering will be performed into it.
  456. * This is necessary because some state (like shaders, textures etc.) can't
  457. * be shared between two different WebGL contexts.
  458. * - Hopefully, once SharedResources are a thing this won't be necessary:
  459. * http://www.khronos.org/webgl/wiki/SharedResouces
  460. * - Alternatively, we could pursue the idea of using the same context
  461. * for multiple canvases, instead of trying to share resources:
  462. * https://www.khronos.org/webgl/public-mailing-list/archives/1210/msg00058.html
  463. *
  464. * In case of a 2D context, a new canvas is created, since there's no
  465. * intrinsic state that can't be easily duplicated.
  466. *
  467. * @param number contexType
  468. * The type of context to use. See the CallWatcherFront scope types.
  469. * @param HTMLCanvasElement canvas
  470. * The canvas element which is the source of all context calls.
  471. * @param array calls
  472. * An array of function call actors.
  473. * @param number first
  474. * The first function call to start from.
  475. * @param number last
  476. * The last (inclusive) function call to end at.
  477. * @return object
  478. * The context on which the specified calls were invoked, the
  479. * last registered draw call's index and a cleanup function, which
  480. * needs to be called whenever any potential followup work is finished.
  481. */
  482. replayAnimationFrame: function ({ contextType, canvas, calls, first, last }) {
  483. let w = canvas.width;
  484. let h = canvas.height;
  485. let replayContext;
  486. let replayContextScaling;
  487. let customViewport;
  488. let customFramebuffer;
  489. let lastDrawCallIndex = -1;
  490. let doCleanup = () => {};
  491. // In case of WebGL contexts, rendering will be done offscreen, in a
  492. // custom framebuffer, but using the same provided context. This is
  493. // necessary because it's very memory-unfriendly to rebuild all the
  494. // required GL state (like recompiling shaders, setting global flags, etc.)
  495. // in an entirely new canvas. However, special care is needed to not
  496. // permanently affect the existing GL state in the process.
  497. if (contextType == "WebGLRenderingContext") {
  498. // To keep things fast, replay the context calls on a framebuffer
  499. // of smaller dimensions than the actual canvas (maximum 256x256 pixels).
  500. let scaling = Math.min(CanvasFront.WEBGL_SCREENSHOT_MAX_HEIGHT, h) / h;
  501. replayContextScaling = scaling;
  502. w = (w * scaling) | 0;
  503. h = (h * scaling) | 0;
  504. // Fetch the same WebGL context and bind a new framebuffer.
  505. let gl = replayContext = this.getWebGLContext(canvas);
  506. let { newFramebuffer, oldFramebuffer } = this.createBoundFramebuffer(gl, w, h);
  507. customFramebuffer = newFramebuffer;
  508. // Set the viewport to match the new framebuffer's dimensions.
  509. let { newViewport, oldViewport } = this.setCustomViewport(gl, w, h);
  510. customViewport = newViewport;
  511. // Revert the framebuffer and viewport to the original values.
  512. doCleanup = () => {
  513. gl.bindFramebuffer(gl.FRAMEBUFFER, oldFramebuffer);
  514. gl.viewport.apply(gl, oldViewport);
  515. };
  516. }
  517. // In case of 2D contexts, draw everything on a separate canvas context.
  518. else if (contextType == "CanvasRenderingContext2D") {
  519. let contentDocument = canvas.ownerDocument;
  520. let replayCanvas = contentDocument.createElement("canvas");
  521. replayCanvas.width = w;
  522. replayCanvas.height = h;
  523. replayContext = replayCanvas.getContext("2d");
  524. replayContextScaling = 1;
  525. customViewport = [0, 0, w, h];
  526. }
  527. // Replay all the context calls up to and including the specified one.
  528. for (let i = first; i <= last; i++) {
  529. let { type, name, args } = calls[i].details;
  530. // Prevent WebGL context calls that try to reset the framebuffer binding
  531. // to the default value, since we want to perform the rendering offscreen.
  532. if (name == "bindFramebuffer" && args[1] == null) {
  533. replayContext.bindFramebuffer(replayContext.FRAMEBUFFER, customFramebuffer);
  534. continue;
  535. }
  536. // Also prevent WebGL context calls that try to change the viewport
  537. // while our custom framebuffer is bound.
  538. if (name == "viewport") {
  539. let framebufferBinding = replayContext.getParameter(replayContext.FRAMEBUFFER_BINDING);
  540. if (framebufferBinding == customFramebuffer) {
  541. replayContext.viewport.apply(replayContext, customViewport);
  542. continue;
  543. }
  544. }
  545. if (type == CallWatcherFront.METHOD_FUNCTION) {
  546. replayContext[name].apply(replayContext, args);
  547. } else if (type == CallWatcherFront.SETTER_FUNCTION) {
  548. replayContext[name] = args;
  549. }
  550. if (CanvasFront.DRAW_CALLS.has(name)) {
  551. lastDrawCallIndex = i;
  552. }
  553. }
  554. return {
  555. replayContext: replayContext,
  556. replayContextScaling: replayContextScaling,
  557. replayViewport: customViewport,
  558. lastDrawCallIndex: lastDrawCallIndex,
  559. doCleanup: doCleanup
  560. };
  561. },
  562. /**
  563. * Gets an object containing a buffer large enough to hold width * height
  564. * pixels, assuming 32bit/pixel and 4 color components.
  565. *
  566. * This method avoids allocating memory and tries to reuse a common buffer
  567. * as much as possible.
  568. *
  569. * @param number w
  570. * The desired pixel array storage width.
  571. * @param number h
  572. * The desired pixel array storage height.
  573. * @return object
  574. * The requested pixel array buffer.
  575. */
  576. getPixelStorage: function (w = 0, h = 0) {
  577. let storage = this._currentPixelStorage;
  578. if (storage && storage["32bit"].length >= w * h) {
  579. return storage;
  580. }
  581. return this.usePixelStorage(new ArrayBuffer(w * h * 4));
  582. },
  583. /**
  584. * Creates and saves the array buffer views used by `getPixelStorage`.
  585. *
  586. * @param ArrayBuffer buffer
  587. * The raw buffer used as storage for various array buffer views.
  588. */
  589. usePixelStorage: function (buffer) {
  590. let array8bit = new Uint8Array(buffer);
  591. let array32bit = new Uint32Array(buffer);
  592. return this._currentPixelStorage = {
  593. "8bit": array8bit,
  594. "32bit": array32bit
  595. };
  596. },
  597. /**
  598. * Creates a framebuffer of the specified dimensions for a WebGL context,
  599. * assuming a RGBA color buffer, a depth buffer and no stencil buffer.
  600. *
  601. * @param WebGLRenderingContext gl
  602. * The WebGL context to create and bind a framebuffer for.
  603. * @param number width
  604. * The desired width of the renderbuffers.
  605. * @param number height
  606. * The desired height of the renderbuffers.
  607. * @return WebGLFramebuffer
  608. * The generated framebuffer object.
  609. */
  610. createBoundFramebuffer: function (gl, width, height) {
  611. let oldFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING);
  612. let oldRenderbufferBinding = gl.getParameter(gl.RENDERBUFFER_BINDING);
  613. let oldTextureBinding = gl.getParameter(gl.TEXTURE_BINDING_2D);
  614. let newFramebuffer = gl.createFramebuffer();
  615. gl.bindFramebuffer(gl.FRAMEBUFFER, newFramebuffer);
  616. // Use a texture as the color renderbuffer attachment, since consumers of
  617. // this function will most likely want to read the rendered pixels back.
  618. let colorBuffer = gl.createTexture();
  619. gl.bindTexture(gl.TEXTURE_2D, colorBuffer);
  620. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
  621. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  622. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  623. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  624. gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
  625. let depthBuffer = gl.createRenderbuffer();
  626. gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);
  627. gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height);
  628. gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, colorBuffer, 0);
  629. gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer);
  630. gl.bindTexture(gl.TEXTURE_2D, oldTextureBinding);
  631. gl.bindRenderbuffer(gl.RENDERBUFFER, oldRenderbufferBinding);
  632. return { oldFramebuffer, newFramebuffer };
  633. },
  634. /**
  635. * Sets the viewport of the drawing buffer for a WebGL context.
  636. * @param WebGLRenderingContext gl
  637. * @param number width
  638. * @param number height
  639. */
  640. setCustomViewport: function (gl, width, height) {
  641. let oldViewport = XPCNativeWrapper.unwrap(gl.getParameter(gl.VIEWPORT));
  642. let newViewport = [0, 0, width, height];
  643. gl.viewport.apply(gl, newViewport);
  644. return { oldViewport, newViewport };
  645. }
  646. };
  647. /**
  648. * Goes through all the arguments and creates a one-level shallow copy
  649. * of all arrays and array buffers.
  650. */
  651. function inplaceShallowCloneArrays(functionArguments, contentWindow) {
  652. let { Object, Array, ArrayBuffer } = contentWindow;
  653. functionArguments.forEach((arg, index, store) => {
  654. if (arg instanceof Array) {
  655. store[index] = arg.slice();
  656. }
  657. if (arg instanceof Object && arg.buffer instanceof ArrayBuffer) {
  658. store[index] = new arg.constructor(arg);
  659. }
  660. });
  661. }