gecko/toolkit/devtools/server/actors/canvas.js

868 lines
30 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {Cc, Ci, Cu, Cr} = require("chrome");
const events = require("sdk/event/core");
const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
const protocol = require("devtools/server/protocol");
const {CallWatcherActor, CallWatcherFront} = require("devtools/server/actors/call-watcher");
const DevToolsUtils = require("devtools/toolkit/DevToolsUtils.js");
const {on, once, off, emit} = events;
const {method, custom, Arg, Option, RetVal} = protocol;
const CANVAS_CONTEXTS = [
"CanvasRenderingContext2D",
"WebGLRenderingContext"
];
const ANIMATION_GENERATORS = [
"requestAnimationFrame",
"mozRequestAnimationFrame"
];
const LOOP_GENERATORS = [
"setTimeout"
];
const DRAW_CALLS = [
// 2D canvas
"fill",
"stroke",
"clearRect",
"fillRect",
"strokeRect",
"fillText",
"strokeText",
"drawImage",
// WebGL
"clear",
"drawArrays",
"drawElements",
"finish",
"flush"
];
const INTERESTING_CALLS = [
// 2D canvas
"save",
"restore",
// WebGL
"useProgram"
];
/**
* Type representing an ArrayBufferView, serialized fast(er).
*
* Don't create a new array buffer view from the parsed array on the frontend.
* Consumers may copy the data into an existing buffer, or create a new one if
* necesasry. For example, this avoids the need for a redundant copy when
* populating ImageData objects, at the expense of transferring char views
* of a pixel buffer over the protocol instead of a packed int view.
*
* XXX: It would be nice if on local connections (only), we could just *give*
* the buffer directly to the front, instead of going through all this
* serialization redundancy.
*/
protocol.types.addType("array-buffer-view", {
write: (v) => "[" + Array.join(v, ",") + "]",
read: (v) => JSON.parse(v)
});
/**
* Type describing a thumbnail or screenshot in a recorded animation frame.
*/
protocol.types.addDictType("snapshot-image", {
index: "number",
width: "number",
height: "number",
scaling: "number",
flipped: "boolean",
pixels: "array-buffer-view"
});
/**
* Type describing an overview of a recorded animation frame.
*/
protocol.types.addDictType("snapshot-overview", {
calls: "array:function-call",
thumbnails: "array:snapshot-image",
screenshot: "snapshot-image"
});
/**
* This actor represents a recorded animation frame snapshot, along with
* all the corresponding canvas' context methods invoked in that frame,
* thumbnails for each draw call and a screenshot of the end result.
*/
let FrameSnapshotActor = protocol.ActorClass({
typeName: "frame-snapshot",
/**
* Creates the frame snapshot call actor.
*
* @param DebuggerServerConnection conn
* The server connection.
* @param HTMLCanvasElement canvas
* A reference to the content canvas.
* @param array calls
* An array of "function-call" actor instances.
* @param object screenshot
* A single "snapshot-image" type instance.
*/
initialize: function(conn, { canvas, calls, screenshot }) {
protocol.Actor.prototype.initialize.call(this, conn);
this._contentCanvas = canvas;
this._functionCalls = calls;
this._animationFrameEndScreenshot = screenshot;
},
/**
* Gets as much data about this snapshot without computing anything costly.
*/
getOverview: method(function() {
return {
calls: this._functionCalls,
thumbnails: this._functionCalls.map(e => e._thumbnail).filter(e => !!e),
screenshot: this._animationFrameEndScreenshot
};
}, {
response: { overview: RetVal("snapshot-overview") }
}),
/**
* Gets a screenshot of the canvas's contents after the specified
* function was called.
*/
generateScreenshotFor: method(function(functionCall) {
let caller = functionCall.details.caller;
let global = functionCall.meta.global;
let canvas = this._contentCanvas;
let calls = this._functionCalls;
let index = calls.indexOf(functionCall);
// To get a screenshot, replay all the steps necessary to render the frame,
// by invoking the context calls up to and including the specified one.
// This will be done in a custom framebuffer in case of a WebGL context.
let replayData = ContextUtils.replayAnimationFrame({
contextType: global,
canvas: canvas,
calls: calls,
first: 0,
last: index
});
let { replayContext, replayContextScaling, lastDrawCallIndex, doCleanup } = replayData;
let [left, top, width, height] = replayData.replayViewport;
let screenshot;
// Depending on the canvas' context, generating a screenshot is done
// in different ways.
if (global == CallWatcherFront.CANVAS_WEBGL_CONTEXT) {
screenshot = ContextUtils.getPixelsForWebGL(replayContext, left, top, width, height);
screenshot.flipped = true;
} else if (global == CallWatcherFront.CANVAS_2D_CONTEXT) {
screenshot = ContextUtils.getPixelsFor2D(replayContext, left, top, width, height);
screenshot.flipped = false;
}
// In case of the WebGL context, we also need to reset the framebuffer
// binding to the original value, after generating the screenshot.
doCleanup();
screenshot.scaling = replayContextScaling;
screenshot.index = lastDrawCallIndex;
return screenshot;
}, {
request: { call: Arg(0, "function-call") },
response: { screenshot: RetVal("snapshot-image") }
})
});
/**
* The corresponding Front object for the FrameSnapshotActor.
*/
let FrameSnapshotFront = protocol.FrontClass(FrameSnapshotActor, {
initialize: function(client, form) {
protocol.Front.prototype.initialize.call(this, client, form);
this._animationFrameEndScreenshot = null;
this._cachedScreenshots = new WeakMap();
},
/**
* This implementation caches the animation frame end screenshot to optimize
* frontend requests to `generateScreenshotFor`.
*/
getOverview: custom(function() {
return this._getOverview().then(data => {
this._animationFrameEndScreenshot = data.screenshot;
return data;
});
}, {
impl: "_getOverview"
}),
/**
* This implementation saves a roundtrip to the backend if the screenshot
* was already generated and retrieved once.
*/
generateScreenshotFor: custom(function(functionCall) {
if (CanvasFront.ANIMATION_GENERATORS.has(functionCall.name) ||
CanvasFront.LOOP_GENERATORS.has(functionCall.name)) {
return promise.resolve(this._animationFrameEndScreenshot);
}
let cachedScreenshot = this._cachedScreenshots.get(functionCall);
if (cachedScreenshot) {
return cachedScreenshot;
}
let screenshot = this._generateScreenshotFor(functionCall);
this._cachedScreenshots.set(functionCall, screenshot);
return screenshot;
}, {
impl: "_generateScreenshotFor"
})
});
/**
* This Canvas Actor handles simple instrumentation of all the methods
* of a 2D or WebGL context, to provide information regarding all the calls
* made when drawing frame inside an animation loop.
*/
let CanvasActor = exports.CanvasActor = protocol.ActorClass({
// Reset for each recording, boolean indicating whether or not
// any draw calls were called for a recording.
_animationContainsDrawCall: false,
typeName: "canvas",
initialize: function(conn, tabActor) {
protocol.Actor.prototype.initialize.call(this, conn);
this.tabActor = tabActor;
this._onContentFunctionCall = this._onContentFunctionCall.bind(this);
},
destroy: function(conn) {
protocol.Actor.prototype.destroy.call(this, conn);
this.finalize();
},
/**
* Starts listening for function calls.
*/
setup: method(function({ reload }) {
if (this._initialized) {
return;
}
this._initialized = true;
this._callWatcher = new CallWatcherActor(this.conn, this.tabActor);
this._callWatcher.onCall = this._onContentFunctionCall;
this._callWatcher.setup({
tracedGlobals: CANVAS_CONTEXTS,
tracedFunctions: [...ANIMATION_GENERATORS, ...LOOP_GENERATORS],
performReload: reload,
storeCalls: true
});
}, {
request: { reload: Option(0, "boolean") },
oneway: true
}),
/**
* Stops listening for function calls.
*/
finalize: method(function() {
if (!this._initialized) {
return;
}
this._initialized = false;
this._callWatcher.finalize();
this._callWatcher = null;
}, {
oneway: true
}),
/**
* Returns whether this actor has been set up.
*/
isInitialized: method(function() {
return !!this._initialized;
}, {
response: { initialized: RetVal("boolean") }
}),
/**
* Returns whether or not the CanvasActor is recording an animation.
* Used in tests.
*/
isRecording: method(function() {
return !!this._callWatcher.isRecording();
}, {
response: { recording: RetVal("boolean") }
}),
/**
* Records a snapshot of all the calls made during the next animation frame.
* The animation should be implemented via the de-facto requestAnimationFrame
* utility, or inside recursive `setTimeout`s. `setInterval` at this time are not supported.
*/
recordAnimationFrame: method(function() {
if (this._callWatcher.isRecording()) {
return this._currentAnimationFrameSnapshot.promise;
}
this._recordingContainsDrawCall = false;
this._callWatcher.eraseRecording();
this._callWatcher.resumeRecording();
let deferred = this._currentAnimationFrameSnapshot = promise.defer();
return deferred.promise;
}, {
response: { snapshot: RetVal("nullable:frame-snapshot") }
}),
/**
* Cease attempts to record an animation frame.
*/
stopRecordingAnimationFrame: method(function() {
if (!this._callWatcher.isRecording()) {
return;
}
this._animationStarted = false;
this._callWatcher.pauseRecording();
this._callWatcher.eraseRecording();
this._currentAnimationFrameSnapshot.resolve(null);
this._currentAnimationFrameSnapshot = null;
}, {
oneway: true
}),
/**
* Invoked whenever an instrumented function is called, be it on a
* 2d or WebGL context, or an animation generator like requestAnimationFrame.
*/
_onContentFunctionCall: function(functionCall) {
let { window, name, args } = functionCall.details;
// The function call arguments are required to replay animation frames,
// in order to generate screenshots. However, simply storing references to
// every kind of object is a bad idea, since their properties may change.
// Consider transformation matrices for example, which are typically
// Float32Arrays whose values can easily change across context calls.
// They need to be cloned.
inplaceShallowCloneArrays(args, window);
// Handle animations generated using requestAnimationFrame
if (CanvasFront.ANIMATION_GENERATORS.has(name)) {
this._handleAnimationFrame(functionCall);
return;
}
// Handle animations generated using setTimeout. While using
// those timers is considered extremely poor practice, they're still widely
// used on the web, especially for old demos; it's nice to support them as well.
if (CanvasFront.LOOP_GENERATORS.has(name)) {
this._handleAnimationFrame(functionCall);
return;
}
if (CanvasFront.DRAW_CALLS.has(name) && this._animationStarted) {
this._handleDrawCall(functionCall);
return;
}
},
/**
* Handle animations generated using requestAnimationFrame.
*/
_handleAnimationFrame: function(functionCall) {
if (!this._animationStarted) {
this._handleAnimationFrameBegin();
}
// Check to see if draw calls occurred yet, as it could be future frames,
// like in the scenario where requestAnimationFrame is called to trigger an animation,
// and rAF is at the beginning of the animate loop.
else if (this._animationContainsDrawCall) {
this._handleAnimationFrameEnd(functionCall);
}
},
/**
* Called whenever an animation frame rendering begins.
*/
_handleAnimationFrameBegin: function() {
this._callWatcher.eraseRecording();
this._animationStarted = true;
},
/**
* Called whenever an animation frame rendering ends.
*/
_handleAnimationFrameEnd: function() {
// Get a hold of all the function calls made during this animation frame.
// Since only one snapshot can be recorded at a time, erase all the
// previously recorded calls.
let functionCalls = this._callWatcher.pauseRecording();
this._callWatcher.eraseRecording();
this._animationContainsDrawCall = false;
// Since the animation frame finished, get a hold of the (already retrieved)
// canvas pixels to conveniently create a screenshot of the final rendering.
let index = this._lastDrawCallIndex;
let width = this._lastContentCanvasWidth;
let height = this._lastContentCanvasHeight;
let flipped = !!this._lastThumbnailFlipped; // undefined -> false
let pixels = ContextUtils.getPixelStorage()["8bit"];
let animationFrameEndScreenshot = {
index: index,
width: width,
height: height,
scaling: 1,
flipped: flipped,
pixels: pixels.subarray(0, width * height * 4)
};
// Wrap the function calls and screenshot in a FrameSnapshotActor instance,
// which will resolve the promise returned by `recordAnimationFrame`.
let frameSnapshot = new FrameSnapshotActor(this.conn, {
canvas: this._lastDrawCallCanvas,
calls: functionCalls,
screenshot: animationFrameEndScreenshot
});
this._currentAnimationFrameSnapshot.resolve(frameSnapshot);
this._currentAnimationFrameSnapshot = null;
this._animationStarted = false;
},
/**
* Invoked whenever a draw call is detected in the animation frame which is
* currently being recorded.
*/
_handleDrawCall: function(functionCall) {
let functionCalls = this._callWatcher.pauseRecording();
let caller = functionCall.details.caller;
let global = functionCall.meta.global;
let contentCanvas = this._lastDrawCallCanvas = caller.canvas;
let index = this._lastDrawCallIndex = functionCalls.indexOf(functionCall);
let w = this._lastContentCanvasWidth = contentCanvas.width;
let h = this._lastContentCanvasHeight = contentCanvas.height;
// To keep things fast, generate images of small and fixed dimensions.
let dimensions = CanvasFront.THUMBNAIL_SIZE;
let thumbnail;
this._animationContainsDrawCall = true;
// Create a thumbnail on every draw call on the canvas context, to augment
// the respective function call actor with this additional data.
if (global == CallWatcherFront.CANVAS_WEBGL_CONTEXT) {
// Check if drawing to a custom framebuffer (when rendering to texture).
// Don't create a thumbnail in this particular case.
let framebufferBinding = caller.getParameter(caller.FRAMEBUFFER_BINDING);
if (framebufferBinding == null) {
thumbnail = ContextUtils.getPixelsForWebGL(caller, 0, 0, w, h, dimensions);
thumbnail.flipped = this._lastThumbnailFlipped = true;
thumbnail.index = index;
}
} else if (global == CallWatcherFront.CANVAS_2D_CONTEXT) {
thumbnail = ContextUtils.getPixelsFor2D(caller, 0, 0, w, h, dimensions);
thumbnail.flipped = this._lastThumbnailFlipped = false;
thumbnail.index = index;
}
functionCall._thumbnail = thumbnail;
this._callWatcher.resumeRecording();
}
});
/**
* A collection of methods for manipulating canvas contexts.
*/
let ContextUtils = {
/**
* WebGL contexts are sensitive to how they're queried. Use this function
* to make sure the right context is always retrieved, if available.
*
* @param HTMLCanvasElement canvas
* The canvas element for which to get a WebGL context.
* @param WebGLRenderingContext gl
* The queried WebGL context, or null if unavailable.
*/
getWebGLContext: function(canvas) {
return canvas.getContext("webgl") ||
canvas.getContext("experimental-webgl");
},
/**
* Gets a hold of the rendered pixels in the most efficient way possible for
* a canvas with a WebGL context.
*
* @param WebGLRenderingContext gl
* The WebGL context to get a screenshot from.
* @param number srcX [optional]
* The first left pixel that is read from the framebuffer.
* @param number srcY [optional]
* The first top pixel that is read from the framebuffer.
* @param number srcWidth [optional]
* The number of pixels to read on the X axis.
* @param number srcHeight [optional]
* The number of pixels to read on the Y axis.
* @param number dstHeight [optional]
* The desired generated screenshot height.
* @return object
* An objet containing the screenshot's width, height and pixel data,
* represented as an 8-bit array buffer of r, g, b, a values.
*/
getPixelsForWebGL: function(gl,
srcX = 0, srcY = 0,
srcWidth = gl.canvas.width,
srcHeight = gl.canvas.height,
dstHeight = srcHeight)
{
let contentPixels = ContextUtils.getPixelStorage(srcWidth, srcHeight);
let { "8bit": charView, "32bit": intView } = contentPixels;
gl.readPixels(srcX, srcY, srcWidth, srcHeight, gl.RGBA, gl.UNSIGNED_BYTE, charView);
return this.resizePixels(intView, srcWidth, srcHeight, dstHeight);
},
/**
* Gets a hold of the rendered pixels in the most efficient way possible for
* a canvas with a 2D context.
*
* @param CanvasRenderingContext2D ctx
* The 2D context to get a screenshot from.
* @param number srcX [optional]
* The first left pixel that is read from the canvas.
* @param number srcY [optional]
* The first top pixel that is read from the canvas.
* @param number srcWidth [optional]
* The number of pixels to read on the X axis.
* @param number srcHeight [optional]
* The number of pixels to read on the Y axis.
* @param number dstHeight [optional]
* The desired generated screenshot height.
* @return object
* An objet containing the screenshot's width, height and pixel data,
* represented as an 8-bit array buffer of r, g, b, a values.
*/
getPixelsFor2D: function(ctx,
srcX = 0, srcY = 0,
srcWidth = ctx.canvas.width,
srcHeight = ctx.canvas.height,
dstHeight = srcHeight)
{
let { data } = ctx.getImageData(srcX, srcY, srcWidth, srcHeight);
let { "32bit": intView } = ContextUtils.usePixelStorage(data.buffer);
return this.resizePixels(intView, srcWidth, srcHeight, dstHeight);
},
/**
* Resizes the provided pixels to fit inside a rectangle with the specified
* height and the same aspect ratio as the source.
*
* @param Uint32Array srcPixels
* The source pixel data, assuming 32bit/pixel and 4 color components.
* @param number srcWidth
* The source pixel data width.
* @param number srcHeight
* The source pixel data height.
* @param number dstHeight [optional]
* The desired resized pixel data height.
* @return object
* An objet containing the resized pixels width, height and data,
* represented as an 8-bit array buffer of r, g, b, a values.
*/
resizePixels: function(srcPixels, srcWidth, srcHeight, dstHeight) {
let screenshotRatio = dstHeight / srcHeight;
let dstWidth = (srcWidth * screenshotRatio) | 0;
let dstPixels = new Uint32Array(dstWidth * dstHeight);
// If the resized image ends up being completely transparent, returning
// an empty array will skip some redundant serialization cycles.
let isTransparent = true;
for (let dstX = 0; dstX < dstWidth; dstX++) {
for (let dstY = 0; dstY < dstHeight; dstY++) {
let srcX = (dstX / screenshotRatio) | 0;
let srcY = (dstY / screenshotRatio) | 0;
let cPos = srcX + srcWidth * srcY;
let dPos = dstX + dstWidth * dstY;
let color = dstPixels[dPos] = srcPixels[cPos];
if (color) {
isTransparent = false;
}
}
}
return {
width: dstWidth,
height: dstHeight,
pixels: isTransparent ? [] : new Uint8Array(dstPixels.buffer)
};
},
/**
* Invokes a series of canvas context calls, to "replay" an animation frame
* and generate a screenshot.
*
* In case of a WebGL context, an offscreen framebuffer is created for
* the respective canvas, and the rendering will be performed into it.
* This is necessary because some state (like shaders, textures etc.) can't
* be shared between two different WebGL contexts.
* - Hopefully, once SharedResources are a thing this won't be necessary:
* http://www.khronos.org/webgl/wiki/SharedResouces
* - Alternatively, we could pursue the idea of using the same context
* for multiple canvases, instead of trying to share resources:
* https://www.khronos.org/webgl/public-mailing-list/archives/1210/msg00058.html
*
* In case of a 2D context, a new canvas is created, since there's no
* intrinsic state that can't be easily duplicated.
*
* @param number contexType
* The type of context to use. See the CallWatcherFront scope types.
* @param HTMLCanvasElement canvas
* The canvas element which is the source of all context calls.
* @param array calls
* An array of function call actors.
* @param number first
* The first function call to start from.
* @param number last
* The last (inclusive) function call to end at.
* @return object
* The context on which the specified calls were invoked, the
* last registered draw call's index and a cleanup function, which
* needs to be called whenever any potential followup work is finished.
*/
replayAnimationFrame: function({ contextType, canvas, calls, first, last }) {
let w = canvas.width;
let h = canvas.height;
let replayContext;
let replayContextScaling;
let customViewport;
let customFramebuffer;
let lastDrawCallIndex = -1;
let doCleanup = () => {};
// In case of WebGL contexts, rendering will be done offscreen, in a
// custom framebuffer, but using the same provided context. This is
// necessary because it's very memory-unfriendly to rebuild all the
// required GL state (like recompiling shaders, setting global flags, etc.)
// in an entirely new canvas. However, special care is needed to not
// permanently affect the existing GL state in the process.
if (contextType == CallWatcherFront.CANVAS_WEBGL_CONTEXT) {
// To keep things fast, replay the context calls on a framebuffer
// of smaller dimensions than the actual canvas (maximum 256x256 pixels).
let scaling = Math.min(CanvasFront.WEBGL_SCREENSHOT_MAX_HEIGHT, h) / h;
replayContextScaling = scaling;
w = (w * scaling) | 0;
h = (h * scaling) | 0;
// Fetch the same WebGL context and bind a new framebuffer.
let gl = replayContext = this.getWebGLContext(canvas);
let { newFramebuffer, oldFramebuffer } = this.createBoundFramebuffer(gl, w, h);
customFramebuffer = newFramebuffer;
// Set the viewport to match the new framebuffer's dimensions.
let { newViewport, oldViewport } = this.setCustomViewport(gl, w, h);
customViewport = newViewport;
// Revert the framebuffer and viewport to the original values.
doCleanup = () => {
gl.bindFramebuffer(gl.FRAMEBUFFER, oldFramebuffer);
gl.viewport.apply(gl, oldViewport);
};
}
// In case of 2D contexts, draw everything on a separate canvas context.
else if (contextType == CallWatcherFront.CANVAS_2D_CONTEXT) {
let contentDocument = canvas.ownerDocument;
let replayCanvas = contentDocument.createElement("canvas");
replayCanvas.width = w;
replayCanvas.height = h;
replayContext = replayCanvas.getContext("2d");
replayContextScaling = 1;
customViewport = [0, 0, w, h];
}
// Replay all the context calls up to and including the specified one.
for (let i = first; i <= last; i++) {
let { type, name, args } = calls[i].details;
// Prevent WebGL context calls that try to reset the framebuffer binding
// to the default value, since we want to perform the rendering offscreen.
if (name == "bindFramebuffer" && args[1] == null) {
replayContext.bindFramebuffer(replayContext.FRAMEBUFFER, customFramebuffer);
continue;
}
// Also prevent WebGL context calls that try to change the viewport
// while our custom framebuffer is bound.
if (name == "viewport") {
let framebufferBinding = replayContext.getParameter(replayContext.FRAMEBUFFER_BINDING);
if (framebufferBinding == customFramebuffer) {
replayContext.viewport.apply(replayContext, customViewport);
continue;
}
}
if (type == CallWatcherFront.METHOD_FUNCTION) {
replayContext[name].apply(replayContext, args);
} else if (type == CallWatcherFront.SETTER_FUNCTION) {
replayContext[name] = args;
}
if (CanvasFront.DRAW_CALLS.has(name)) {
lastDrawCallIndex = i;
}
}
return {
replayContext: replayContext,
replayContextScaling: replayContextScaling,
replayViewport: customViewport,
lastDrawCallIndex: lastDrawCallIndex,
doCleanup: doCleanup
};
},
/**
* Gets an object containing a buffer large enough to hold width * height
* pixels, assuming 32bit/pixel and 4 color components.
*
* This method avoids allocating memory and tries to reuse a common buffer
* as much as possible.
*
* @param number w
* The desired pixel array storage width.
* @param number h
* The desired pixel array storage height.
* @return object
* The requested pixel array buffer.
*/
getPixelStorage: function(w = 0, h = 0) {
let storage = this._currentPixelStorage;
if (storage && storage["32bit"].length >= w * h) {
return storage;
}
return this.usePixelStorage(new ArrayBuffer(w * h * 4));
},
/**
* Creates and saves the array buffer views used by `getPixelStorage`.
*
* @param ArrayBuffer buffer
* The raw buffer used as storage for various array buffer views.
*/
usePixelStorage: function(buffer) {
let array8bit = new Uint8Array(buffer);
let array32bit = new Uint32Array(buffer);
return this._currentPixelStorage = {
"8bit": array8bit,
"32bit": array32bit
};
},
/**
* Creates a framebuffer of the specified dimensions for a WebGL context,
* assuming a RGBA color buffer, a depth buffer and no stencil buffer.
*
* @param WebGLRenderingContext gl
* The WebGL context to create and bind a framebuffer for.
* @param number width
* The desired width of the renderbuffers.
* @param number height
* The desired height of the renderbuffers.
* @return WebGLFramebuffer
* The generated framebuffer object.
*/
createBoundFramebuffer: function(gl, width, height) {
let oldFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING);
let oldRenderbufferBinding = gl.getParameter(gl.RENDERBUFFER_BINDING);
let oldTextureBinding = gl.getParameter(gl.TEXTURE_BINDING_2D);
let newFramebuffer = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, newFramebuffer);
// Use a texture as the color renderbuffer attachment, since consumers of
// this function will most likely want to read the rendered pixels back.
let colorBuffer = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, colorBuffer);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
let depthBuffer = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, colorBuffer, 0);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer);
gl.bindTexture(gl.TEXTURE_2D, oldTextureBinding);
gl.bindRenderbuffer(gl.RENDERBUFFER, oldRenderbufferBinding);
return { oldFramebuffer, newFramebuffer };
},
/**
* Sets the viewport of the drawing buffer for a WebGL context.
* @param WebGLRenderingContext gl
* @param number width
* @param number height
*/
setCustomViewport: function(gl, width, height) {
let oldViewport = XPCNativeWrapper.unwrap(gl.getParameter(gl.VIEWPORT));
let newViewport = [0, 0, width, height];
gl.viewport.apply(gl, newViewport);
return { oldViewport, newViewport };
}
};
/**
* The corresponding Front object for the CanvasActor.
*/
let CanvasFront = exports.CanvasFront = protocol.FrontClass(CanvasActor, {
initialize: function(client, { canvasActor }) {
protocol.Front.prototype.initialize.call(this, client, { actor: canvasActor });
this.manage(this);
}
});
/**
* Constants.
*/
CanvasFront.CANVAS_CONTEXTS = new Set(CANVAS_CONTEXTS);
CanvasFront.ANIMATION_GENERATORS = new Set(ANIMATION_GENERATORS);
CanvasFront.LOOP_GENERATORS = new Set(LOOP_GENERATORS);
CanvasFront.DRAW_CALLS = new Set(DRAW_CALLS);
CanvasFront.INTERESTING_CALLS = new Set(INTERESTING_CALLS);
CanvasFront.THUMBNAIL_SIZE = 50; // px
CanvasFront.WEBGL_SCREENSHOT_MAX_HEIGHT = 256; // px
CanvasFront.INVALID_SNAPSHOT_IMAGE = {
index: -1,
width: 0,
height: 0,
pixels: []
};
/**
* Goes through all the arguments and creates a one-level shallow copy
* of all arrays and array buffers.
*/
function inplaceShallowCloneArrays(functionArguments, contentWindow) {
let { Object, Array, ArrayBuffer } = contentWindow;
functionArguments.forEach((arg, index, store) => {
if (arg instanceof Array) {
store[index] = arg.slice();
}
if (arg instanceof Object && arg.buffer instanceof ArrayBuffer) {
store[index] = new arg.constructor(arg);
}
});
}