Bug 1041158 - Properly cleanup the framebuffer binding after generating a screenshot from a webgl context, r=rcampbell

This commit is contained in:
Victor Porof 2014-07-22 12:43:24 -04:00
parent 545b119ad1
commit 85551493a0
6 changed files with 184 additions and 36 deletions

View File

@ -5,6 +5,7 @@ support-files =
doc_simple-canvas-bitmasks.html
doc_simple-canvas-deep-stack.html
doc_simple-canvas-transparent.html
doc_webgl-bindings.html
doc_webgl-enum.html
head.js
@ -17,6 +18,7 @@ support-files =
[browser_canvas-actor-test-07.js]
[browser_canvas-actor-test-08.js]
[browser_canvas-actor-test-09.js]
[browser_canvas-actor-test-10.js]
[browser_canvas-frontend-call-highlight.js]
[browser_canvas-frontend-call-list.js]
[browser_canvas-frontend-call-search.js]

View File

@ -18,9 +18,7 @@ function ifTestingSupported() {
ok(true, "Target automatically navigated when the front was set up.");
let snapshotActor = yield front.recordAnimationFrame();
let animationOverview = yield snapshotActor.getOverview();
let functionCalls = animationOverview.calls;
is(functionCalls[0].name, "clearRect",

View File

@ -0,0 +1,74 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests that the correct framebuffer, renderbuffer and textures are re-bound
* after generating screenshots using the actor.
*/
function ifTestingSupported() {
let [target, debuggee, front] = yield initCanavsDebuggerBackend(WEBGL_BINDINGS_URL);
let navigated = once(target, "navigate");
yield front.setup({ reload: true });
ok(true, "The front was setup up successfully.");
yield navigated;
ok(true, "Target automatically navigated when the front was set up.");
let snapshotActor = yield front.recordAnimationFrame();
let animationOverview = yield snapshotActor.getOverview();
let functionCalls = animationOverview.calls;
let firstScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[0]);
is(firstScreenshot.index, -1,
"The first screenshot didn't encounter any draw call.");
is(firstScreenshot.width, 128,
"The first screenshot has the correct width.");
is(firstScreenshot.height, 128,
"The first screenshot has the correct height.");
is(firstScreenshot.flipped, true,
"The first screenshot has the correct 'flipped' flag.");
is(firstScreenshot.pixels.length, 0,
"The first screenshot should be empty.");
let gl = debuggee.gl;
is(gl.getParameter(gl.FRAMEBUFFER_BINDING), debuggee.customFramebuffer,
"The debuggee's gl context framebuffer wasn't changed.");
is(gl.getParameter(gl.RENDERBUFFER_BINDING), debuggee.customRenderbuffer,
"The debuggee's gl context renderbuffer wasn't changed.");
is(gl.getParameter(gl.TEXTURE_BINDING_2D), debuggee.customTexture,
"The debuggee's gl context texture binding wasn't changed.");
let secondScreenshot = yield snapshotActor.generateScreenshotFor(functionCalls[1]);
is(secondScreenshot.index, 1,
"The second screenshot has the correct index.");
is(secondScreenshot.width, 128,
"The second screenshot has the correct width.");
is(secondScreenshot.height, 128,
"The second screenshot has the correct height.");
is(secondScreenshot.flipped, true,
"The second screenshot has the correct 'flipped' flag.");
is(secondScreenshot.pixels.length, 128 * 128,
"The second screenshot should not be empty.");
is(new Uint8Array(secondScreenshot.pixels.buffer)[0], 0,
"The second screenshot has the correct red component.");
is(new Uint8Array(secondScreenshot.pixels.buffer)[1], 0,
"The second screenshot has the correct green component.");
is(new Uint8Array(secondScreenshot.pixels.buffer)[2], 255,
"The second screenshot has the correct blue component.");
is(new Uint8Array(secondScreenshot.pixels.buffer)[3], 255,
"The second screenshot has the correct alpha component.");
let gl = debuggee.gl;
is(gl.getParameter(gl.FRAMEBUFFER_BINDING), debuggee.customFramebuffer,
"The debuggee's gl context framebuffer still wasn't changed.");
is(gl.getParameter(gl.RENDERBUFFER_BINDING), debuggee.customRenderbuffer,
"The debuggee's gl context renderbuffer still wasn't changed.");
is(gl.getParameter(gl.TEXTURE_BINDING_2D), debuggee.customTexture,
"The debuggee's gl context texture binding still wasn't changed.");
yield removeTab(target.tab);
finish();
}

View File

@ -0,0 +1,60 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>WebGL editor test page</title>
</head>
<body>
<canvas id="canvas" width="128" height="128"></canvas>
<script type="text/javascript;version=1.8">
"use strict";
let canvas, gl;
let customFramebuffer;
let customRenderbuffer;
let customTexture;
window.onload = function() {
canvas = document.querySelector("canvas");
gl = canvas.getContext("webgl", { preserveDrawingBuffer: true });
gl.clearColor(1.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
customFramebuffer = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, customFramebuffer);
customRenderbuffer = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, customRenderbuffer);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, 128, 128);
customTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, customTexture);
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, 128, 128, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, customTexture, 0);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, customRenderbuffer);
gl.clearColor(0.0, 1.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
drawScene();
}
function drawScene() {
gl.clearColor(0.0, 0.0, 1.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
window.requestAnimationFrame(drawScene);
}
</script>
</body>
</html>

View File

@ -30,6 +30,7 @@ const SIMPLE_BITMASKS_URL = EXAMPLE_URL + "doc_simple-canvas-bitmasks.html";
const SIMPLE_CANVAS_TRANSPARENT_URL = EXAMPLE_URL + "doc_simple-canvas-transparent.html";
const SIMPLE_CANVAS_DEEP_STACK_URL = EXAMPLE_URL + "doc_simple-canvas-deep-stack.html";
const WEBGL_ENUM_URL = EXAMPLE_URL + "doc_webgl-enum.html";
const WEBGL_BINDINGS_URL = EXAMPLE_URL + "doc_webgl-bindings.html";
// All tests are asynchronous.
waitForExplicitFinish();

View File

@ -148,7 +148,7 @@ let FrameSnapshotActor = protocol.ActorClass({
// 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 { replayContext, lastDrawCallIndex } = ContextUtils.replayAnimationFrame({
let replayData = ContextUtils.replayAnimationFrame({
contextType: global,
canvas: canvas,
calls: calls,
@ -156,22 +156,23 @@ let FrameSnapshotActor = protocol.ActorClass({
last: index
});
let { replayContext, lastDrawCallIndex, doCleanup } = replayData;
let screenshot;
// Depending on the canvas' context, generating a screenshot is done
// in different ways. In case of the WebGL context, we also need to reset
// the framebuffer binding to the default value.
// in different ways.
if (global == CallWatcherFront.CANVAS_WEBGL_CONTEXT) {
screenshot = ContextUtils.getPixelsForWebGL(replayContext);
replayContext.bindFramebuffer(replayContext.FRAMEBUFFER, null);
screenshot.flipped = true;
}
// In case of 2D contexts, no additional special treatment is necessary.
else if (global == CallWatcherFront.CANVAS_2D_CONTEXT) {
} else if (global == CallWatcherFront.CANVAS_2D_CONTEXT) {
screenshot = ContextUtils.getPixelsFor2D(replayContext);
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.index = lastDrawCallIndex;
return screenshot;
}, {
@ -368,7 +369,7 @@ let CanvasActor = exports.CanvasActor = protocol.ActorClass({
let index = this._lastDrawCallIndex;
let width = this._lastContentCanvasWidth;
let height = this._lastContentCanvasHeight;
let flipped = this._lastThumbnailFlipped;
let flipped = !!this._lastThumbnailFlipped; // undefined -> false
let pixels = ContextUtils.getPixelStorage()["32bit"];
let lastDrawCallScreenshot = {
index: index,
@ -584,29 +585,35 @@ let ContextUtils = {
* @param number last
* The last (inclusive) function call to end at.
* @return object
* The context on which the specified calls were invoked and the
* last registered draw call's index.
* 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 replayCanvas;
let replayContext;
let customFramebuffer;
let lastDrawCallIndex = -1;
let doCleanup = () => {};
// In case of WebGL contexts, rendering will be done offscreen, in a
// custom framebuffer, but on the provided canvas context.
// 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) {
replayCanvas = canvas;
replayContext = this.getWebGLContext(replayCanvas);
customFramebuffer = this.createBoundFramebuffer(replayContext, w, h);
let gl = replayContext = this.getWebGLContext(canvas);
let { newFramebuffer, oldFramebuffer } = this.createBoundFramebuffer(gl, w, h);
customFramebuffer = newFramebuffer;
doCleanup = () => gl.bindFramebuffer(gl.FRAMEBUFFER, oldFramebuffer);
}
// In case of 2D contexts, draw everything on a separate canvas context.
else if (contextType == CallWatcherFront.CANVAS_2D_CONTEXT) {
let contentDocument = canvas.ownerDocument;
replayCanvas = contentDocument.createElement("canvas");
let replayCanvas = contentDocument.createElement("canvas");
replayCanvas.width = w;
replayCanvas.height = h;
replayContext = replayCanvas.getContext("2d");
@ -621,23 +628,24 @@ let ContextUtils = {
// to the default value, since we want to perform the rendering offscreen.
if (name == "bindFramebuffer" && args[1] == null) {
replayContext.bindFramebuffer(replayContext.FRAMEBUFFER, customFramebuffer);
continue;
}
if (type == CallWatcherFront.METHOD_FUNCTION) {
replayContext[name].apply(replayContext, args);
} else if (type == CallWatcherFront.SETTER_FUNCTION) {
replayContext[name] = args;
} else {
if (type == CallWatcherFront.METHOD_FUNCTION) {
replayContext[name].apply(replayContext, args);
} else if (type == CallWatcherFront.SETTER_FUNCTION) {
replayContext[name] = args;
} else {
// Ignore getter calls.
}
if (CanvasFront.DRAW_CALLS.has(name)) {
lastDrawCallIndex = i;
}
// Ignore getter calls.
}
if (CanvasFront.DRAW_CALLS.has(name)) {
lastDrawCallIndex = i;
}
}
return {
replayContext: replayContext,
lastDrawCallIndex: lastDrawCallIndex
lastDrawCallIndex: lastDrawCallIndex,
doCleanup: doCleanup
};
},
@ -692,16 +700,21 @@ let ContextUtils = {
* The generated framebuffer object.
*/
createBoundFramebuffer: function(gl, width, height) {
let framebuffer = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
let oldFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING);
let oldRenderbufferBinding = gl.getParameter(gl.RENDERBUFFER_BINDING);
let oldTextureBinding = gl.getParameter(gl.TEXTURE_BINDING_2D);
// Use a texture as the color rendebuffer attachment, since consumenrs of
let newFramebuffer = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, newFramebuffer);
// Use a texture as the color rendebuffer 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.generateMipmap(gl.TEXTURE_2D);
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();
@ -711,10 +724,10 @@ let ContextUtils = {
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, null);
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
gl.bindTexture(gl.TEXTURE_2D, oldTextureBinding);
gl.bindRenderbuffer(gl.RENDERBUFFER, oldRenderbufferBinding);
return framebuffer;
return { oldFramebuffer, newFramebuffer };
}
};