Bug 910953 - Implement the backend for a WebGL shader editor, r=dcamp,vlad
authorVictor Porof <vporof@mozilla.com>
Mon, 09 Sep 2013 23:33:25 +0300
changeset 166990 273efc178016ad3c5c57cce15cf386938ede035d
parent 166989 10f395671f719637a2bbe270612012e2d3fef041
child 166991 3097d7118a1863ae264a1edc02d4f69dad9ac226
push id428
push userbbajaj@mozilla.com
push dateTue, 28 Jan 2014 00:16:25 +0000
treeherdermozilla-release@cd72a7ff3a75 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdcamp, vlad
bugs910953
milestone27.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 910953 - Implement the backend for a WebGL shader editor, r=dcamp,vlad
browser/devtools/moz.build
browser/devtools/shadereditor/moz.build
browser/devtools/shadereditor/test/browser.ini
browser/devtools/shadereditor/test/browser_webgl-actor-test-01.js
browser/devtools/shadereditor/test/browser_webgl-actor-test-02.js
browser/devtools/shadereditor/test/browser_webgl-actor-test-03.js
browser/devtools/shadereditor/test/browser_webgl-actor-test-04.js
browser/devtools/shadereditor/test/browser_webgl-actor-test-05.js
browser/devtools/shadereditor/test/browser_webgl-actor-test-06.js
browser/devtools/shadereditor/test/browser_webgl-actor-test-07.js
browser/devtools/shadereditor/test/browser_webgl-actor-test-08.js
browser/devtools/shadereditor/test/browser_webgl-actor-test-09.js
browser/devtools/shadereditor/test/browser_webgl-actor-test-10.js
browser/devtools/shadereditor/test/browser_webgl-actor-test-11.js
browser/devtools/shadereditor/test/browser_webgl-actor-test-12.js
browser/devtools/shadereditor/test/browser_webgl-actor-test-13.js
browser/devtools/shadereditor/test/browser_webgl-actor-test-14.js
browser/devtools/shadereditor/test/doc_multiple-contexts.html
browser/devtools/shadereditor/test/doc_shader-order.html
browser/devtools/shadereditor/test/doc_simple-canvas.html
browser/devtools/shadereditor/test/head.js
browser/devtools/shadereditor/test/moz.build
toolkit/devtools/server/actors/webgl.js
toolkit/devtools/server/main.js
--- a/browser/devtools/moz.build
+++ b/browser/devtools/moz.build
@@ -1,31 +1,32 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 DIRS += [
+    'app-manager',
+    'commandline',
+    'debugger',
+    'fontinspector',
+    'framework',
     'inspector',
+    'layoutview',
     'markupview',
-    'webconsole',
-    'commandline',
+    'netmonitor',
+    'profiler',
+    'responsivedesign',
+    'scratchpad',
+    'shadereditor',
+    'shared',
     'sourceeditor',
     'styleeditor',
     'styleinspector',
     'tilt',
-    'scratchpad',
-    'debugger',
-    'netmonitor',
-    'layoutview',
-    'shared',
-    'responsivedesign',
-    'framework',
-    'profiler',
-    'fontinspector',
-    'app-manager',
+    'webconsole',
 ]
 
 EXTRA_COMPONENTS += [
     'devtools-clhandler.js',
     'devtools-clhandler.manifest',
 ]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/moz.build
@@ -0,0 +1,6 @@
+# vim: set filetype=python:
+# 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/.
+
+TEST_DIRS += ['test']
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser.ini
@@ -0,0 +1,21 @@
+[DEFAULT]
+support-files =
+  doc_multiple-contexts.html
+  doc_shader-order.html
+  doc_simple-canvas.html
+  head.js
+
+[browser_webgl-actor-test-01.js]
+[browser_webgl-actor-test-02.js]
+[browser_webgl-actor-test-03.js]
+[browser_webgl-actor-test-04.js]
+[browser_webgl-actor-test-05.js]
+[browser_webgl-actor-test-06.js]
+[browser_webgl-actor-test-07.js]
+[browser_webgl-actor-test-08.js]
+[browser_webgl-actor-test-09.js]
+[browser_webgl-actor-test-10.js]
+[browser_webgl-actor-test-11.js]
+[browser_webgl-actor-test-12.js]
+[browser_webgl-actor-test-13.js]
+[browser_webgl-actor-test-14.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-01.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if a WebGL front can be created for a remote tab target.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, front] = yield initBackend(SIMPLE_CANVAS_URL);
+
+  ok(target, "Should have a target available.");
+  ok(debuggee, "Should have a debuggee available.");
+  ok(front, "Should have a protocol front available.");
+
+  yield removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-02.js
@@ -0,0 +1,19 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if notifications about WebGL programs being linked are not sent
+ * if the front wasn't set up first.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, front] = yield initBackend(SIMPLE_CANVAS_URL);
+
+  once(front, "program-linked").then(() => {
+    ok(false, "A 'program-linked' notification shouldn't have been sent!");
+  });
+
+  yield reload(target);
+  yield removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-03.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if notifications about WebGL programs being linked are sent
+ * after a target navigation.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, front] = yield initBackend(SIMPLE_CANVAS_URL);
+
+  let navigated = once(target, "navigate");
+  let linked = once(front, "program-linked");
+
+  yield front.setup();
+  ok(true, "The front was setup up successfully.");
+
+  yield navigated;
+  ok(true, "Target automatically navigated when the front was set up.");
+
+  yield linked;
+  ok(true, "A 'program-linked' notification was sent after reloading.");
+
+  yield removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-04.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if a program actor is sent when WebGL programs are linked,
+ * and that the corresponding vertex and fragment actors can be retrieved.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, front] = yield initBackend(SIMPLE_CANVAS_URL);
+  front.setup();
+
+  let programActor = yield once(front, "program-linked");
+  ok(programActor,
+    "A program actor was sent along with the 'program-linked' notification.")
+
+  let vertexShader = yield programActor.getVertexShader();
+  ok(programActor,
+    "A vertex shader actor was retrieved from the program actor.");
+
+  let fragmentShader = yield programActor.getFragmentShader();
+  ok(programActor,
+    "A fragment shader actor was retrieved from the program actor.");
+
+  yield removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-05.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the source contents can be retrieved from the vertex and fragment
+ * shader actors.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, front] = yield initBackend(SIMPLE_CANVAS_URL);
+  front.setup();
+
+  let programActor = yield once(front, "program-linked");
+  let vertexShader = yield programActor.getVertexShader();
+  let fragmentShader = yield programActor.getFragmentShader();
+
+  let vertSource = yield vertexShader.getText();
+  ok(vertSource.contains("gl_Position"),
+    "The correct vertex shader source was retrieved.");
+
+  let fragSource = yield fragmentShader.getText();
+  ok(fragSource.contains("gl_FragColor"),
+    "The correct fragment shader source was retrieved.");
+
+  yield removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-06.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the highlight/unhighlight operations on program actors
+ * work as expected.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, front] = yield initBackend(SIMPLE_CANVAS_URL);
+  front.setup();
+
+  let programActor = yield once(front, "program-linked");
+  let vertexShader = yield programActor.getVertexShader();
+  let fragmentShader = yield programActor.getFragmentShader();
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+  yield checkShaderSource("The shader sources are correct before highlighting.");
+  ok(true, "The top left pixel color was correct before highlighting.");
+
+  yield programActor.highlight([0, 0, 1, 1]);
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 255, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 0, b: 255, a: 255 }, true);
+  yield checkShaderSource("The shader sources are preserved after highlighting.");
+  ok(true, "The top left pixel color is correct after highlighting.");
+
+  yield programActor.unhighlight();
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+  yield checkShaderSource("The shader sources are correct after unhighlighting.");
+  ok(true, "The top left pixel color is correct after unhighlighting.");
+
+  function checkShaderSource(aMessage) {
+    return Task.spawn(function() {
+      let newVertexShader = yield programActor.getVertexShader();
+      let newFragmentShader = yield programActor.getFragmentShader();
+      is(vertexShader, newVertexShader,
+        "The same vertex shader actor was retrieved.");
+      is(fragmentShader, newFragmentShader,
+        "The same fragment shader actor was retrieved.");
+
+      let vertSource = yield newVertexShader.getText();
+      let fragSource = yield newFragmentShader.getText();
+      ok(vertSource.contains("I'm special!") &&
+         fragSource.contains("I'm also special!"), aMessage);
+    });
+  }
+
+  yield removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-07.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that vertex and fragment shader sources can be changed.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, front] = yield initBackend(SIMPLE_CANVAS_URL);
+  front.setup();
+
+  let programActor = yield once(front, "program-linked");
+  let vertexShader = yield programActor.getVertexShader();
+  let fragmentShader = yield programActor.getFragmentShader();
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 128, y: 128 }, { r: 191, g: 64, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+
+  let vertSource = yield vertexShader.getText();
+  let fragSource = yield fragmentShader.getText();
+  ok(!vertSource.contains("2.0"),
+    "The vertex shader source is correct before changing it.");
+  ok(!fragSource.contains("0.5"),
+    "The fragment shader source is correct before changing it.");
+
+  let newVertSource = vertSource.replace("1.0", "2.0");
+  let status = yield vertexShader.compile(newVertSource);
+  ok(!status,
+    "The new vertex shader source was compiled without errors.");
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 128, y: 128 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+
+  let vertSource = yield vertexShader.getText();
+  let fragSource = yield fragmentShader.getText();
+  ok(vertSource.contains("2.0"),
+    "The vertex shader source is correct after changing it.");
+  ok(!fragSource.contains("0.5"),
+    "The fragment shader source is correct after changing the vertex shader.");
+
+  let newFragSource = fragSource.replace("1.0", "0.5");
+  let status = yield fragmentShader.compile(newFragSource);
+  ok(!status,
+    "The new fragment shader source was compiled without errors.");
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 128, y: 128 }, { r: 255, g: 0, b: 0, a: 127 }, true);
+  yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+
+  let vertSource = yield vertexShader.getText();
+  let fragSource = yield fragmentShader.getText();
+  ok(vertSource.contains("2.0"),
+    "The vertex shader source is correct after changing the fragment shader.");
+  ok(fragSource.contains("0.5"),
+    "The fragment shader source is correct after changing it.");
+
+  yield removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-08.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the rendering is updated when a varying variable is
+ * changed in one shader.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, front] = yield initBackend(SIMPLE_CANVAS_URL);
+  front.setup();
+
+  let programActor = yield once(front, "program-linked");
+  let vertexShader = yield programActor.getVertexShader();
+  let fragmentShader = yield programActor.getFragmentShader();
+
+  let oldVertSource = yield vertexShader.getText();
+  let newVertSource = oldVertSource.replace("= aVertexColor", "= vec3(0, 0, 1)");
+  let status = yield vertexShader.compile(newVertSource);
+  ok(!status,
+    "The new vertex shader source was compiled without errors.");
+
+  yield waitForFrame(debuggee);
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 255, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 128, y: 128 }, { r: 0, g: 0, b: 255, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 0, b: 255, a: 255 }, true);
+
+  let vertSource = yield vertexShader.getText();
+  let fragSource = yield fragmentShader.getText();
+  ok(vertSource.contains("vFragmentColor = vec3(0, 0, 1);"),
+    "The vertex shader source is correct after changing it.");
+  ok(fragSource.contains("gl_FragColor = vec4(vFragmentColor, 1.0);"),
+    "The fragment shader source is correct after changing the vertex shader.");
+
+  yield removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-09.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that errors are properly handled when trying to compile a
+ * defective shader source.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, front] = yield initBackend(SIMPLE_CANVAS_URL);
+  front.setup();
+
+  let programActor = yield once(front, "program-linked");
+  let vertexShader = yield programActor.getVertexShader();
+  let fragmentShader = yield programActor.getFragmentShader();
+
+  let oldVertSource = yield vertexShader.getText();
+  let newVertSource = oldVertSource.replace("vec4", "vec3");
+
+  try {
+    yield vertexShader.compile(newVertSource);
+    ok(false, "Vertex shader was compiled with a defective source!");
+  } catch (error) {
+    ok(error,
+      "The new vertex shader source was compiled with errors.");
+    is(error.compile, "",
+      "The compilation status should be empty.");
+    isnot(error.link, "",
+      "The linkage status should not be empty.");
+    is(error.link.split("ERROR").length - 1, 2,
+      "The linkage status contains two errors.");
+    ok(error.link.contains("ERROR: 0:8: 'constructor'"),
+      "A constructor error is contained in the linkage status.");
+    ok(error.link.contains("ERROR: 0:8: 'assign'"),
+      "An assignment error is contained in the linkage status.");
+  }
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+  ok(true, "The shader was reverted to the old source.");
+
+  let vertSource = yield vertexShader.getText();
+  ok(vertSource.contains("vec4(aVertexPosition, 1.0);"),
+    "The previous correct vertex shader source was preserved.");
+
+  let oldFragSource = yield fragmentShader.getText();
+  let newFragSource = oldFragSource.replace("vec3", "vec4");
+
+  try {
+    yield fragmentShader.compile(newFragSource);
+    ok(false, "Fragment shader was compiled with a defective source!");
+  } catch (error) {
+    ok(error,
+      "The new fragment shader source was compiled with errors.");
+    is(error.compile, "",
+      "The compilation status should be empty.");
+    isnot(error.link, "",
+      "The linkage status should not be empty.");
+    is(error.link.split("ERROR").length - 1, 1,
+      "The linkage status contains one error.");
+    ok(error.link.contains("ERROR: 0:6: 'constructor'"),
+      "A constructor error is contained in the linkage status.");
+  }
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+  ok(true, "The shader was reverted to the old source.");
+
+  let fragSource = yield fragmentShader.getText();
+  ok(fragSource.contains("vec3 vFragmentColor;"),
+    "The previous correct fragment shader source was preserved.");
+
+  yield programActor.highlight([0, 0, 1, 1]);
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 255, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 0, b: 255, a: 255 }, true);
+  ok(true, "Highlighting worked after setting a defective fragment source.");
+
+  yield programActor.unhighlight();
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+  ok(true, "Unhighlighting worked after setting a defective vertex source.");
+
+  yield removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-10.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the WebGL context is correctly instrumented every time the
+ * target navigates.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, front] = yield initBackend(SIMPLE_CANVAS_URL);
+
+  let linked = once(front, "program-linked");
+  yield front.setup();
+  yield linked;
+  ok(true, "Canvas was correctly instrumented on the first navigation.");
+
+  let linked = once(front, "program-linked");
+  yield reload(target);
+  yield linked;
+  ok(true, "Canvas was correctly instrumented on the second navigation.");
+
+  let linked = once(front, "program-linked");
+  yield reload(target);
+  yield linked;
+  ok(true, "Canvas was correctly instrumented on the third navigation.");
+
+  let programActor = yield linked;
+  let vertexShader = yield programActor.getVertexShader();
+  let fragmentShader = yield programActor.getFragmentShader();
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+  ok(true, "The top left pixel color was correct before highlighting.");
+
+  yield programActor.highlight([0, 0, 1, 1]);
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 255, a: 255 }, true);
+  ok(true, "The top left pixel color is correct after highlighting.");
+
+  yield programActor.unhighlight();
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
+  ok(true, "The top left pixel color is correct after unhighlighting.");
+
+  yield removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-11.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the WebGL context is never instrumented anymore after the
+ * finalize method is called.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, front] = yield initBackend(SIMPLE_CANVAS_URL);
+
+  let linked = once(front, "program-linked");
+  yield front.setup();
+  yield linked;
+  ok(true, "Canvas was correctly instrumented on the first navigation.");
+
+  once(front, "program-linked").then(() => {
+    ok(false, "A 'program-linked' notification shouldn't have been sent!");
+  });
+
+  yield front.finalize();
+  yield reload(target);
+  yield removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-12.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the correct vertex and fragment shader sources are retrieved
+ * regardless of the order in which they were compiled and attached.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, front] = yield initBackend(SHADER_ORDER_URL);
+  front.setup();
+
+  let programActor = yield once(front, "program-linked");
+  let vertexShader = yield programActor.getVertexShader();
+  let fragmentShader = yield programActor.getFragmentShader();
+
+  let vertSource = yield vertexShader.getText();
+  let fragSource = yield fragmentShader.getText();
+
+  ok(vertSource.contains("I'm a vertex shader!"),
+    "The correct vertex shader text was retrieved.");
+  ok(fragSource.contains("I'm a fragment shader!"),
+    "The correct fragment shader text was retrieved.");
+
+  yield removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-13.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if multiple WebGL contexts are correctly handled.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, front] = yield initBackend(MULTIPLE_CONTEXTS_URL);
+  front.setup();
+
+  let firstProgramActor = yield once(front, "program-linked");
+  let secondProgramActor = yield once(front, "program-linked");
+
+  isnot(firstProgramActor, secondProgramActor,
+    "Two distinct program actors were recevide from two separate contexts.");
+
+  let firstVertexShader = yield firstProgramActor.getVertexShader();
+  let firstFragmentShader = yield firstProgramActor.getFragmentShader();
+  let secondVertexShader = yield secondProgramActor.getVertexShader();
+  let secondFragmentShader = yield secondProgramActor.getFragmentShader();
+
+  isnot(firstVertexShader, secondVertexShader,
+    "The two programs should have distinct vertex shaders.");
+  isnot(firstFragmentShader, secondFragmentShader,
+    "The two programs should have distinct fragment shaders.");
+
+  let firstVertSource = yield firstVertexShader.getText();
+  let firstFragSource = yield firstFragmentShader.getText();
+  let secondVertSource = yield secondVertexShader.getText();
+  let secondFragSource = yield secondFragmentShader.getText();
+
+  is(firstVertSource, secondVertSource,
+    "The vertex shaders should have identical sources.");
+  is(firstFragSource, secondFragSource,
+    "The vertex shaders should have identical sources.");
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  ok(true, "The two canvases are correctly drawn.");
+
+  yield firstProgramActor.highlight([1, 0, 0, 1]);
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  ok(true, "The first canvas was correctly filled after highlighting.");
+
+  yield secondProgramActor.highlight([0, 1, 0, 1]);
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 255, b: 0, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 0, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 255, b: 0, a: 255 }, true, "#canvas2");
+  ok(true, "The second canvas was correctly filled after highlighting.");
+
+  yield firstProgramActor.unhighlight();
+  yield secondProgramActor.unhighlight();
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 255, g: 255, b: 0, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  ok(true, "The two canvases were correctly filled after unhighlighting.");
+
+  yield removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-14.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the rendering is updated when a uniform variable is
+ * changed in one shader of a page with multiple WebGL contexts.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, front] = yield initBackend(MULTIPLE_CONTEXTS_URL);
+  front.setup();
+
+  let firstProgramActor = yield once(front, "program-linked");
+  let secondProgramActor = yield once(front, "program-linked");
+  let firstFragmentShader = yield firstProgramActor.getFragmentShader();
+  let secondFragmentShader = yield secondProgramActor.getFragmentShader();
+
+  let oldFragSource = yield firstFragmentShader.getText();
+  let newFragSource = oldFragSource.replace("vec4(uColor", "vec4(0.25, 0.25, 0.25");
+  let status = yield firstFragmentShader.compile(newFragSource);
+  ok(!status,
+    "The first new fragment shader source was compiled without errors.");
+
+  yield waitForFrame(debuggee);
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 64, g: 64, b: 64, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 64, g: 64, b: 64, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 255, b: 255, a: 255 }, true, "#canvas2");
+  ok(true, "The first fragment shader was changed.");
+
+  let oldFragSource = yield secondFragmentShader.getText();
+  let newFragSource = oldFragSource.replace("vec4(uColor", "vec4(0.75, 0.75, 0.75");
+  let status = yield secondFragmentShader.compile(newFragSource);
+  ok(!status,
+    "The second new fragment shader source was compiled without errors.");
+
+  yield waitForFrame(debuggee);
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 64, g: 64, b: 64, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 64, g: 64, b: 64, a: 255 }, true, "#canvas1");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 191, g: 191, b: 191, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 191, g: 191, b: 191, a: 255 }, true, "#canvas2");
+  ok(true, "The second fragment shader was changed.");
+
+  yield removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/doc_multiple-contexts.html
@@ -0,0 +1,112 @@
+<!-- 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>
+
+    <script id="shader-vs" type="x-shader/x-vertex">
+      precision lowp float;
+      attribute vec3 aVertexPosition;
+
+      void main(void) {
+        gl_Position = vec4(aVertexPosition, 1);
+      }
+    </script>
+
+    <script id="shader-fs" type="x-shader/x-fragment">
+      precision lowp float;
+      uniform vec3 uColor;
+
+      void main(void) {
+        gl_FragColor = vec4(uColor, 1);
+      }
+    </script>
+  </head>
+
+  <body>
+    <canvas id="canvas1" width="128" height="128"></canvas>
+    <canvas id="canvas2" width="128" height="128"></canvas>
+
+    <script type="text/javascript;version=1.8">
+      "use strict";
+
+      let canvas = [], gl = [];
+      let program = [];
+      let squareVerticesPositionBuffer = [];
+      let vertexPositionAttribute = [];
+      let colorUniform = [];
+
+      window.onload = function() {
+        for (let i = 0; i < 2; i++) {
+          canvas[i] = document.querySelector("#canvas" + (i + 1));
+          gl[i] = canvas[i].getContext("webgl");
+          gl[i].clearColor(0.0, 0.0, 0.0, 1.0);
+
+          initProgram(i);
+          initBuffers(i);
+          drawScene(i);
+        }
+      }
+
+      function initProgram(i) {
+        let vertexShader = getShader(gl[i], "shader-vs");
+        let fragmentShader = getShader(gl[i], "shader-fs");
+
+        program[i] = gl[i].createProgram();
+        gl[i].attachShader(program[i], vertexShader);
+        gl[i].attachShader(program[i], fragmentShader);
+        gl[i].linkProgram(program[i]);
+
+        vertexPositionAttribute[i] = gl[i].getAttribLocation(program[i], "aVertexPosition");
+        gl[i].enableVertexAttribArray(vertexPositionAttribute[i]);
+
+        colorUniform[i] = gl[i].getUniformLocation(program[i], "uColor");
+      }
+
+      function getShader(gl, id) {
+        let script = document.getElementById(id);
+        let source = script.textContent;
+        let shader;
+
+        if (script.type == "x-shader/x-fragment") {
+          shader = gl.createShader(gl.FRAGMENT_SHADER);
+        } else if (script.type == "x-shader/x-vertex") {
+          shader = gl.createShader(gl.VERTEX_SHADER);
+        }
+
+        gl.shaderSource(shader, source);
+        gl.compileShader(shader);
+
+        return shader;
+      }
+
+      function initBuffers(i) {
+        squareVerticesPositionBuffer[i] = gl[i].createBuffer();
+        gl[i].bindBuffer(gl[i].ARRAY_BUFFER, squareVerticesPositionBuffer[i]);
+        gl[i].bufferData(gl[i].ARRAY_BUFFER, new Float32Array([
+           1.0,  1.0, 0.0,
+          -1.0,  1.0, 0.0,
+           1.0, -1.0, 0.0,
+          -1.0, -1.0, 0.0
+        ]), gl[i].STATIC_DRAW);
+      }
+
+      function drawScene(i) {
+        gl[i].clear(gl[i].COLOR_BUFFER_BIT);
+
+        gl[i].bindBuffer(gl[i].ARRAY_BUFFER, squareVerticesPositionBuffer[i]);
+        gl[i].vertexAttribPointer(vertexPositionAttribute[i], 3, gl[i].FLOAT, false, 0, 0);
+
+        gl[i].useProgram(program[i]);
+        gl[i].uniform3fv(colorUniform[i], i == 0 ? [1, 1, 0] : [0, 1, 1]);
+        gl[i].drawArrays(gl[i].TRIANGLE_STRIP, 0, 4);
+
+        window.requestAnimationFrame(() => drawScene(i));
+      }
+    </script>
+  </body>
+
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/doc_shader-order.html
@@ -0,0 +1,83 @@
+<!-- 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>
+
+    <script id="shader-vs" type="x-shader/x-vertex">
+      precision lowp float;
+
+      void main(void) {
+        gl_Position = vec4(0, 0, 0, 1); // I'm a vertex shader!
+      }
+    </script>
+
+    <script id="shader-fs" type="x-shader/x-fragment">
+      precision lowp float;
+      varying vec3 vFragmentColor;
+
+      void main(void) {
+        gl_FragColor = vec4(1, 0, 0, 1); // I'm a fragment shader!
+      }
+    </script>
+  </head>
+
+  <body>
+    <canvas width="512" height="512"></canvas>
+
+    <script type="text/javascript;version=1.8">
+      "use strict";
+
+      let canvas, gl;
+
+      window.onload = function() {
+        canvas = document.querySelector("canvas");
+        gl = canvas.getContext("webgl");
+
+        let shaderProgram = gl.createProgram();
+        let vertexShader, fragmentShader;
+
+        // Compile and attach the shaders in a random order. The test will
+        // ensure that the correct vertex and fragment source is retrieved
+        // regardless of this crazyness.
+        if (Math.random() > 0.5) {
+          vertexShader = getShader(gl, "shader-vs");
+          fragmentShader = getShader(gl, "shader-fs");
+        } else {
+          fragmentShader = getShader(gl, "shader-fs");
+          vertexShader = getShader(gl, "shader-vs");
+        }
+        if (Math.random() > 0.5) {
+          gl.attachShader(shaderProgram, vertexShader);
+          gl.attachShader(shaderProgram, fragmentShader);
+        } else {
+          gl.attachShader(shaderProgram, fragmentShader);
+          gl.attachShader(shaderProgram, vertexShader);
+        }
+
+        gl.linkProgram(shaderProgram);
+      }
+
+      function getShader(gl, id) {
+        let script = document.getElementById(id);
+        let source = script.textContent;
+        let shader;
+
+        if (script.type == "x-shader/x-fragment") {
+          shader = gl.createShader(gl.FRAGMENT_SHADER);
+        } else if (script.type == "x-shader/x-vertex") {
+          shader = gl.createShader(gl.VERTEX_SHADER);
+        }
+
+        gl.shaderSource(shader, source);
+        gl.compileShader(shader);
+
+        return shader;
+      }
+    </script>
+  </body>
+
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/doc_simple-canvas.html
@@ -0,0 +1,125 @@
+<!-- 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>
+
+    <script id="shader-vs" type="x-shader/x-vertex">
+      precision lowp float;
+      attribute vec3 aVertexPosition;
+      attribute vec3 aVertexColor;
+      varying vec3 vFragmentColor;
+
+      void main(void) {
+        gl_Position = vec4(aVertexPosition, 1.0);
+        vFragmentColor = aVertexColor; // I'm special!
+      }
+    </script>
+
+    <script id="shader-fs" type="x-shader/x-fragment">
+      precision lowp float;
+      varying vec3 vFragmentColor;
+
+      void main(void) {
+        gl_FragColor = vec4(vFragmentColor, 1.0); // I'm also special!
+      }
+    </script>
+  </head>
+
+  <body>
+    <canvas width="512" height="512"></canvas>
+
+    <script type="text/javascript;version=1.8">
+      "use strict";
+
+      let canvas, gl;
+      let program;
+      let squareVerticesPositionBuffer;
+      let squareVerticesColorBuffer;
+      let vertexPositionAttribute;
+      let vertexColorAttribute;
+
+      window.onload = function() {
+        canvas = document.querySelector("canvas");
+        gl = canvas.getContext("webgl");
+        gl.clearColor(0.0, 0.0, 0.0, 1.0);
+
+        initProgram();
+        initBuffers();
+        drawScene();
+      }
+
+      function initProgram() {
+        let vertexShader = getShader(gl, "shader-vs");
+        let fragmentShader = getShader(gl, "shader-fs");
+
+        program = gl.createProgram();
+        gl.attachShader(program, vertexShader);
+        gl.attachShader(program, fragmentShader);
+        gl.linkProgram(program);
+
+        vertexPositionAttribute = gl.getAttribLocation(program, "aVertexPosition");
+        gl.enableVertexAttribArray(vertexPositionAttribute);
+
+        vertexColorAttribute = gl.getAttribLocation(program, "aVertexColor");
+        gl.enableVertexAttribArray(vertexColorAttribute);
+      }
+
+      function getShader(gl, id) {
+        let script = document.getElementById(id);
+        let source = script.textContent;
+        let shader;
+
+        if (script.type == "x-shader/x-fragment") {
+          shader = gl.createShader(gl.FRAGMENT_SHADER);
+        } else if (script.type == "x-shader/x-vertex") {
+          shader = gl.createShader(gl.VERTEX_SHADER);
+        }
+
+        gl.shaderSource(shader, source);
+        gl.compileShader(shader);
+
+        return shader;
+      }
+
+      function initBuffers() {
+        squareVerticesPositionBuffer = gl.createBuffer();
+        gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesPositionBuffer);
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
+           1.0,  1.0, 0.0,
+          -1.0,  1.0, 0.0,
+           1.0, -1.0, 0.0,
+          -1.0, -1.0, 0.0
+        ]), gl.STATIC_DRAW);
+
+        squareVerticesColorBuffer = gl.createBuffer();
+        gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesColorBuffer);
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
+          1.0, 1.0, 1.0, 1.0,
+          1.0, 0.0, 0.0, 1.0,
+          0.0, 1.0, 0.0, 1.0,
+          0.0, 0.0, 1.0, 1.0
+        ]), gl.STATIC_DRAW);
+      }
+
+      function drawScene() {
+        gl.clear(gl.COLOR_BUFFER_BIT);
+
+        gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesPositionBuffer);
+        gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);
+
+        gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesColorBuffer);
+        gl.vertexAttribPointer(vertexColorAttribute, 4, gl.FLOAT, false, 0, 0);
+
+        gl.useProgram(program);
+        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
+
+        window.requestAnimationFrame(drawScene);
+      }
+    </script>
+  </body>
+
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/head.js
@@ -0,0 +1,217 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+
+// Enable logging for all the tests. Both the debugger server and frontend will
+// be affected by this pref.
+let gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log");
+Services.prefs.setBoolPref("devtools.debugger.log", true);
+
+let { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
+let { Promise: promise } = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
+let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
+let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
+let { DebuggerClient } = Cu.import("resource://gre/modules/devtools/dbg-client.jsm", {});
+
+let { WebGLFront } = devtools.require("devtools/server/actors/webgl");
+let TiltGL = devtools.require("devtools/tilt/tilt-gl");
+let TargetFactory = devtools.TargetFactory;
+let Toolbox = devtools.Toolbox;
+
+const EXAMPLE_URL = "http://example.com/browser/browser/devtools/shadereditor/test/";
+const SIMPLE_CANVAS_URL = EXAMPLE_URL + "doc_simple-canvas.html";
+const SHADER_ORDER_URL = EXAMPLE_URL + "doc_shader-order.html";
+const MULTIPLE_CONTEXTS_URL = EXAMPLE_URL + "doc_multiple-contexts.html";
+
+// All tests are asynchronous.
+waitForExplicitFinish();
+
+registerCleanupFunction(() => {
+  info("finish() was called, cleaning up...");
+  Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging);
+});
+
+function addTab(aUrl, aWindow) {
+  info("Adding tab: " + aUrl);
+
+  let deferred = promise.defer();
+  let targetWindow = aWindow || window;
+  let targetBrowser = targetWindow.gBrowser;
+
+  targetWindow.focus();
+  let tab = targetBrowser.selectedTab = targetBrowser.addTab(aUrl);
+  let linkedBrowser = tab.linkedBrowser;
+
+  linkedBrowser.addEventListener("load", function onLoad() {
+    linkedBrowser.removeEventListener("load", onLoad, true);
+    info("Tab added and finished loading: " + aUrl);
+    deferred.resolve(tab);
+  }, true);
+
+  return deferred.promise;
+}
+
+function removeTab(aTab, aWindow) {
+  info("Removing tab.");
+
+  let deferred = promise.defer();
+  let targetWindow = aWindow || window;
+  let targetBrowser = targetWindow.gBrowser;
+  let tabContainer = targetBrowser.tabContainer;
+
+  tabContainer.addEventListener("TabClose", function onClose(aEvent) {
+    tabContainer.removeEventListener("TabClose", onClose, false);
+    info("Tab removed and finished closing.");
+    deferred.resolve();
+  }, false);
+
+  targetBrowser.removeTab(aTab);
+  return deferred.promise;
+}
+
+function handleError(aError) {
+  ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+  finish();
+}
+
+function ifWebGLSupported() {
+  ok(false, "You need to define a 'ifWebGLSupported' function.");
+  finish();
+}
+
+function ifWebGLUnsupported() {
+  todo(false, "Skipping test because WebGL isn't supported.");
+  finish();
+}
+
+function test() {
+  let generator = isWebGLSupported() ? ifWebGLSupported : ifWebGLUnsupported;
+  Task.spawn(generator).then(null, handleError);
+}
+
+function createCanvas() {
+  return document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+}
+
+function isWebGLSupported() {
+  let supported =
+    !TiltGL.isWebGLForceEnabled() &&
+     TiltGL.isWebGLSupported() &&
+     TiltGL.create3DContext(createCanvas());
+
+  info("Apparently, WebGL is" + (supported ? "" : " not") + " supported.");
+  return supported;
+}
+
+function once(aTarget, aEventName, aUseCapture = false) {
+  info("Waiting for event: '" + aEventName + "' on " + aTarget + ".");
+
+  let deferred = promise.defer();
+
+  for (let [add, remove] of [
+    ["addEventListener", "removeEventListener"],
+    ["addListener", "removeListener"],
+    ["on", "off"]
+  ]) {
+    if ((add in aTarget) && (remove in aTarget)) {
+      aTarget[add](aEventName, function onEvent(...aArgs) {
+        aTarget[remove](aEventName, onEvent, aUseCapture);
+        deferred.resolve.apply(deferred, aArgs);
+      }, aUseCapture);
+      break;
+    }
+  }
+
+  return deferred.promise;
+}
+
+function waitForFrame(aDebuggee) {
+  let deferred = promise.defer();
+  aDebuggee.mozRequestAnimationFrame(deferred.resolve);
+  return deferred.promise;
+}
+
+function isApprox(aFirst, aSecond, aMargin = 1) {
+  return Math.abs(aFirst - aSecond) <= aMargin;
+}
+
+function isApproxColor(aFirst, aSecond, aMargin) {
+  return isApprox(aFirst.r, aSecond.r, aMargin) &&
+    isApprox(aFirst.g, aSecond.g, aMargin) &&
+    isApprox(aFirst.b, aSecond.b, aMargin) &&
+    isApprox(aFirst.a, aSecond.a, aMargin);
+}
+
+function getPixels(aDebuggee, aSelector = "canvas") {
+  let canvas = aDebuggee.document.querySelector(aSelector);
+  let gl = canvas.getContext("webgl");
+
+  let { width, height } = canvas;
+  let buffer = new aDebuggee.Uint8Array(width * height * 4);
+  gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, buffer);
+
+  info("Retrieved pixels: " + width + "x" + height);
+  return [buffer, width, height];
+}
+
+function getPixel(aDebuggee, aPosition, aSelector = "canvas") {
+  let canvas = aDebuggee.document.querySelector(aSelector);
+  let gl = canvas.getContext("webgl");
+
+  let { width, height } = canvas;
+  let { x, y } = aPosition;
+  let buffer = new aDebuggee.Uint8Array(4);
+  gl.readPixels(x, height - y - 1, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, buffer);
+
+  let pixel = { r: buffer[0], g: buffer[1], b: buffer[2], a: buffer[3] };
+
+  info("Retrieved pixel: " + pixel.toSource() + " at " + aPosition.toSource());
+  return pixel;
+}
+
+function ensurePixelIs(aDebuggee, aPosition, aColor, aWaitFlag = false, aSelector = "canvas") {
+  let pixel = getPixel(aDebuggee, aPosition, aSelector);
+  if (isApproxColor(pixel, aColor)) {
+    ok(true, "Expected pixel is shown at: " + aPosition.toSource());
+    return promise.resolve(null);
+  }
+  if (aWaitFlag) {
+    return Task.spawn(function() {
+      yield waitForFrame(aDebuggee);
+      yield ensurePixelIs(aDebuggee, aPosition, aColor, aWaitFlag, aSelector);
+    });
+  }
+  ok(false, "Expected pixel was not already shown at: " + aPosition.toSource());
+  return promise.reject(null);
+}
+
+function reload(aTarget) {
+  let navigated = once(aTarget, "navigate");
+  aTarget.client.activeTab.reload();
+  return navigated;
+}
+
+function initBackend(aUrl) {
+  info("Initializing a shader editor front.");
+
+  if (!DebuggerServer.initialized) {
+    DebuggerServer.init(() => true);
+    DebuggerServer.addBrowserActors();
+  }
+
+  return Task.spawn(function*() {
+    let tab = yield addTab(aUrl);
+    let target = TargetFactory.forTab(tab);
+    let debuggee = target.window.wrappedJSObject;
+
+    yield target.makeRemote();
+
+    let front = new WebGLFront(target.client, target.form);
+    return [target, debuggee, front];
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/moz.build
@@ -0,0 +1,6 @@
+# vim: set filetype=python:
+# 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/.
+
+BROWSER_CHROME_MANIFESTS += ['browser.ini']
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/actors/webgl.js
@@ -0,0 +1,850 @@
+/* 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");
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+const events = require("sdk/event/core");
+const protocol = require("devtools/server/protocol");
+
+const { on, once, off, emit } = events;
+const { method, Arg, Option, RetVal } = protocol;
+
+const WEBGL_CONTEXT_NAMES = ["webgl", "experimental-webgl", "moz-webgl"];
+const HIGHLIGHT_FRAG_SHADER = [
+  "precision lowp float;",
+  "void main() {",
+    "gl_FragColor.rgba = vec4(%color);",
+  "}"
+].join("\n");
+
+exports.register = function(handle) {
+  handle.addTabActor(WebGLActor, "webglActor");
+}
+
+exports.unregister = function(handle) {
+  handle.removeTabActor(WebGLActor);
+}
+
+/**
+ * A WebGL Shader contributing to building a WebGL Program.
+ * You can either retrieve, or compile the source of a shader, which will
+ * automatically inflict the necessary changes to the WebGL state.
+ */
+let ShaderActor = protocol.ActorClass({
+  typeName: "gl-shader",
+  initialize: function(conn, id) {
+    protocol.Actor.prototype.initialize.call(this, conn);
+  },
+
+  /**
+   * Gets the source code for this shader.
+   */
+  getText: method(function() {
+    return this.text;
+  }, {
+    response: { text: RetVal("string") }
+  }),
+
+  /**
+   * Sets and compiles new source code for this shader.
+   */
+  compile: method(function(text) {
+    // Get the shader and corresponding program to change via the WebGL proxy.
+    let { context, shader, program, observer: { proxy } } = this;
+
+    // Get the new shader source to inject.
+    let oldText = this.text;
+    let newText = text;
+
+    // Overwrite the shader's source.
+    let error = proxy.call("compileShader", context, program, shader, this.text = newText);
+
+    // If something went wrong, revert to the previous shader.
+    if (error.compile || error.link) {
+      proxy.call("compileShader", context, program, shader, this.text = oldText);
+      return error;
+    }
+  }, {
+    request: { text: Arg(0, "string") },
+    response: { error: RetVal("nullable:json") }
+  })
+});
+
+/**
+ * The corresponding Front object for the ShaderActor.
+ */
+let ShaderFront = protocol.FrontClass(ShaderActor, {
+  initialize: function(client, form) {
+    protocol.Front.prototype.initialize.call(this, client, form);
+  }
+});
+
+/**
+ * A WebGL program is composed (at the moment, analogue to OpenGL ES 2.0)
+ * of two shaders: a vertex shader and a fragment shader.
+ */
+let ProgramActor = protocol.ActorClass({
+  typeName: "gl-program",
+  initialize: function(conn, id) {
+    protocol.Actor.prototype.initialize.call(this, conn);
+    this._shaderActorsCache = { vertex: null, fragment: null };
+  },
+
+  /**
+   * Gets the vertex shader linked to this program. This method guarantees
+   * a single actor instance per shader.
+   */
+  getVertexShader: method(function() {
+    return this._getShaderActor("vertex");
+  }, {
+    response: { shader: RetVal("gl-shader") }
+  }),
+
+  /**
+   * Gets the fragment shader linked to this program. This method guarantees
+   * a single actor instance per shader.
+   */
+  getFragmentShader: method(function() {
+    return this._getShaderActor("fragment");
+  }, {
+    response: { shader: RetVal("gl-shader") }
+  }),
+
+  /**
+   * Replaces this program's fragment shader with an temporary
+   * easy-to-distinguish alternative. See HIGHLIGHT_FRAG_SHADER.
+   */
+  highlight: method(function(color) {
+    let shaderActor = this._getShaderActor("fragment");
+    let oldText = shaderActor.text;
+    let newText = HIGHLIGHT_FRAG_SHADER.replace("%color", color)
+    shaderActor.compile(newText);
+    shaderActor.text = oldText;
+  }, {
+    request: { color: Arg(0, "array:string") },
+    oneway: true
+  }),
+
+  /**
+   * Reverts this program's fragment shader to the latest user-defined source.
+   */
+  unhighlight: method(function() {
+    let shaderActor = this._getShaderActor("fragment");
+    shaderActor.compile(shaderActor.text);
+  }, {
+    oneway: true
+  }),
+
+  /**
+   * Returns a cached ShaderActor instance based on the required shader type.
+   *
+   * @param string type
+   *        Either "vertex" or "fragment".
+   * @return ShaderActor
+   *         The respective shader actor instance.
+   */
+  _getShaderActor: function(type) {
+    if (this._shaderActorsCache[type]) {
+      return this._shaderActorsCache[type];
+    }
+
+    let shaderActor = new ShaderActor(this.conn);
+    shaderActor.context = this.context;
+    shaderActor.observer = this.observer;
+    shaderActor.program = this.program;
+    shaderActor.shader = this.shadersData[type].ref;
+    shaderActor.text = this.shadersData[type].text;
+
+    return this._shaderActorsCache[type] = shaderActor;
+  }
+});
+
+/**
+ * The corresponding Front object for the ProgramActor.
+ */
+let ProgramFront = protocol.FrontClass(ProgramActor, {
+  initialize: function(client, form) {
+    protocol.Front.prototype.initialize.call(this, client, form);
+  }
+});
+
+/**
+ * The WebGL Actor handles simple interaction with a WebGL context via a few
+ * high-level methods. After instantiating this actor, you'll need to set it
+ * up by calling setup().
+ */
+let WebGLActor = exports.WebGLActor = protocol.ActorClass({
+  typeName: "webgl",
+  initialize: function(conn, tabActor) {
+    protocol.Actor.prototype.initialize.call(this, conn);
+    this.tabActor = tabActor;
+    this._onGlobalCreated = this._onGlobalCreated.bind(this);
+    this._onProgramLinked = this._onProgramLinked.bind(this);
+  },
+  destroy: function(conn) {
+    protocol.Actor.prototype.destroy.call(this, conn);
+    this.finalize();
+  },
+
+  /**
+   * Starts waiting for the current tab actor's document global to be
+   * created, in order to instrument the Canvas context and become
+   * aware of everything the content does WebGL-wise.
+   *
+   * See ContentObserver and WebGLInstrumenter for more details.
+   */
+  setup: method(function() {
+    if (this._initialized) {
+      return;
+    }
+    this._initialized = true;
+    this._contentObserver = new ContentObserver(this.tabActor);
+    this._webglObserver = new WebGLObserver();
+    on(this._contentObserver, "global-created", this._onGlobalCreated);
+    on(this._webglObserver, "program-linked", this._onProgramLinked);
+
+    this.tabActor.window.location.reload();
+  }, {
+    oneway: true
+  }),
+
+  /**
+   * Stops listening for document global changes and puts this actor
+   * to hibernation. This method is called automatically just before the
+   * actor is destroyed.
+   */
+  finalize: method(function() {
+    if (!this._initialized) {
+      return;
+    }
+    this._initialized = false;
+    this._contentObserver.stopListening();
+    off(this._contentObserver, "global-created", this._onGlobalCreated);
+    off(this._webglObserver, "program-linked", this._onProgramLinked);
+  }, {
+   oneway: true
+  }),
+
+  /**
+   * Events emitted by this actor. The "program-linked" event is fired
+   * every time a WebGL program was linked with its respective two shaders.
+   */
+  events: {
+    "program-linked": {
+      type: "programLinked",
+      program: Arg(0, "gl-program")
+    }
+  },
+
+  /**
+   * Invoked whenever the current tab actor's document global is created.
+   */
+  _onGlobalCreated: function(window) {
+    WebGLInstrumenter.handle(window, this._webglObserver);
+  },
+
+  /**
+   * Invoked whenever the current WebGL context links a program.
+   */
+  _onProgramLinked: function(gl, program, shaders) {
+    let observer = this._webglObserver;
+    let shadersData = { vertex: null, fragment: null };
+
+    for (let shader of shaders) {
+      let text = observer.cache.call("getShaderInfo", shader);
+      let data = { ref: shader, text: text };
+
+      // Make sure the shader data object always contains the vertex shader
+      // first, and the fragment shader second. There are no guarantees that
+      // the compilation order of shaders in the debuggee is always the same.
+      if (gl.getShaderParameter(shader, gl.SHADER_TYPE) == gl.VERTEX_SHADER) {
+        shadersData.vertex = data;
+      } else {
+        shadersData.fragment = data;
+      }
+    }
+
+    let programActor = new ProgramActor(this.conn);
+    programActor.context = gl;
+    programActor.observer = observer;
+    programActor.program = program;
+    programActor.shadersData = shadersData;
+
+    events.emit(this, "program-linked", programActor);
+  }
+});
+
+/**
+ * The corresponding Front object for the WebGLActor.
+ */
+let WebGLFront = exports.WebGLFront = protocol.FrontClass(WebGLActor, {
+  initialize: function(client, { webglActor }) {
+    protocol.Front.prototype.initialize.call(this, client, { actor: webglActor });
+    client.addActorPool(this);
+    this.manage(this);
+  }
+});
+
+/**
+ * Handles adding an observer for the creation of content document globals,
+ * event sent immediately after a web content document window has been set up,
+ * but before any script code has been executed. This will allow us to
+ * instrument the HTMLCanvasElement with the appropriate inspection methods.
+ */
+function ContentObserver(tabActor) {
+  this._contentWindow = tabActor.browser.contentWindow;
+  this._onContentGlobalCreated = this._onContentGlobalCreated.bind(this);
+  this.startListening();
+}
+
+ContentObserver.prototype = {
+  /**
+   * Starts listening for the required observer messages.
+   */
+  startListening: function() {
+    Services.obs.addObserver(
+      this._onContentGlobalCreated, "content-document-global-created", false);
+  },
+
+  /**
+   * Stops listening for the required observer messages.
+   */
+  stopListening: function() {
+    Services.obs.removeObserver(
+      this._onContentGlobalCreated, "content-document-global-created", false);
+  },
+
+  /**
+   * Fired immediately after a web content document window has been set up.
+   */
+  _onContentGlobalCreated: function(subject, topic, data) {
+    if (subject == this._contentWindow) {
+      emit(this, "global-created", subject);
+    }
+  }
+};
+
+/**
+ * Instruments a HTMLCanvasElement with the appropriate inspection methods.
+ */
+let WebGLInstrumenter = {
+  /**
+   * Overrides the getContext method in the HTMLCanvasElement prototype.
+   *
+   * @param nsIDOMWindow window
+   *        The window to perform the instrumentation in.
+   * @param WebGLObserver observer
+   *        The observer watching function calls in the context.
+   */
+  handle: function(window, observer) {
+    let self = this;
+
+    let canvasElem = XPCNativeWrapper.unwrap(window.HTMLCanvasElement);
+    let canvasPrototype = canvasElem.prototype;
+    let originalGetContext = canvasPrototype.getContext;
+
+    /**
+     * Returns a drawing context on the canvas, or null if the context ID is
+     * not supported. This override creates an observer for the targeted context
+     * type and instruments specific functions in the targeted context instance.
+     */
+    canvasPrototype.getContext = function(name, options) {
+      // Make sure a context was able to be created.
+      let context = originalGetContext.call(this, name, options);
+      if (!context) {
+        return context;
+      }
+      // Make sure a WebGL (not a 2D) context will be instrumented.
+      if (WEBGL_CONTEXT_NAMES.indexOf(name) == -1) {
+        return context;
+      }
+
+      // Link our observer to the new WebGL context methods.
+      for (let { timing, callback, functions } of self._methods) {
+        for (let func of functions) {
+          self._instrument(observer, context, func, timing, callback);
+        }
+      }
+
+      // Return the decorated context back to the content consumer, which
+      // will continue using it normally.
+      return context;
+    };
+  },
+
+  /**
+   * Overrides a specific method in a HTMLCanvasElement context.
+   *
+   * @param WebGLObserver observer
+   *        The observer watching function calls in the context.
+   * @param WebGLRenderingContext context
+   *        The targeted context instance.
+   * @param string funcName
+   *        The function to override.
+   * @param string timing [optional]
+   *        When to issue the callback in relation to the actual context
+   *        function call. Availalble values are "before" and "after" (default).
+   * @param string callbackName [optional]
+   *        A custom callback function name in the observer. If unspecified,
+   *        it will default to the name of the function to override.
+   */
+  _instrument: function(observer, context, funcName, timing, callbackName) {
+    let originalFunc = context[funcName];
+
+    context[funcName] = function() {
+      let glArgs = Array.slice(arguments);
+      let glResult, glBreak;
+
+      if (timing == "before" && !observer.suppressHandlers) {
+        glBreak = observer.call(callbackName || funcName, context, glArgs);
+        if (glBreak) return;
+      }
+
+      glResult = originalFunc.apply(this, glArgs);
+
+      if (timing == "after" && !observer.suppressHandlers) {
+        glBreak = observer.call(callbackName || funcName, context, glArgs, glResult);
+        if (glBreak) return;
+      }
+
+      return glResult;
+    };
+  },
+
+  /**
+   * Override mappings for WebGL methods.
+   */
+  _methods: [{
+    timing: "after",
+    functions: [
+      "linkProgram", "getAttribLocation", "getUniformLocation"
+    ]
+  }, {
+    timing: "before",
+    callback: "toggleVertexAttribArray",
+    functions: [
+      "enableVertexAttribArray", "disableVertexAttribArray"
+    ]
+  }, {
+    timing: "before",
+    callback: "attribute_",
+    functions: [
+      "vertexAttrib1f", "vertexAttrib2f", "vertexAttrib3f", "vertexAttrib4f",
+      "vertexAttrib1fv", "vertexAttrib2fv", "vertexAttrib3fv", "vertexAttrib4fv",
+      "vertexAttribPointer"
+    ]
+  }, {
+    timing: "before",
+    callback: "uniform_",
+    functions: [
+      "uniform1i", "uniform2i", "uniform3i", "uniform4i",
+      "uniform1f", "uniform2f", "uniform3f", "uniform4f",
+      "uniform1iv", "uniform2iv", "uniform3iv", "uniform4iv",
+      "uniform1fv", "uniform2fv", "uniform3fv", "uniform4fv",
+      "uniformMatrix2fv", "uniformMatrix3fv", "uniformMatrix4fv"
+    ]
+  }]
+  // TODO: It'd be a good idea to handle other functions as well:
+  //   - getActiveUniform
+  //   - getUniform
+  //   - getActiveAttrib
+  //   - getVertexAttrib
+};
+
+/**
+ * An observer that captures a WebGL context's method calls.
+ */
+function WebGLObserver() {
+  this.cache = new WebGLCache(this);
+  this.proxy = new WebGLProxy(this);
+}
+
+WebGLObserver.prototype = {
+  /**
+   * Set this flag to true to stop observing any context function calls.
+   */
+  suppressHandlers: false,
+
+  /**
+   * Called immediately *after* 'linkProgram' is requested in the context.
+   *
+   * @param WebGLRenderingContext gl
+   *        The WebGL context initiating this call.
+   * @param array glArgs
+   *        Overridable arguments with which the function is called.
+   * @param void glResult
+   *        The returned value of the original function call.
+   */
+  linkProgram: function(gl, glArgs, glResult) {
+    let program = glArgs[0];
+    let shaders = gl.getAttachedShaders(program);
+
+    for (let shader of shaders) {
+      let source = gl.getShaderSource(shader);
+      this.cache.call("addShaderInfo", shader, source);
+    }
+
+    emit(this, "program-linked", gl, program, shaders);
+  },
+
+  /**
+   * Called immediately *after* 'getAttribLocation' is requested in the context.
+   *
+   * @param WebGLRenderingContext gl
+   *        The WebGL context initiating this call.
+   * @param array glArgs
+   *        Overridable arguments with which the function is called.
+   * @param GLint glResult
+   *        The returned value of the original function call.
+   */
+  getAttribLocation: function(gl, glArgs, glResult) {
+    let [program, name] = glArgs;
+    this.cache.call("addAttribute", program, name, glResult);
+  },
+
+  /**
+   * Called immediately *after* 'getUniformLocation' is requested in the context.
+   *
+   * @param WebGLRenderingContext gl
+   *        The WebGL context initiating this call.
+   * @param array glArgs
+   *        Overridable arguments with which the function is called.
+   * @param WebGLUniformLocation glResult
+   *        The returned value of the original function call.
+   */
+  getUniformLocation: function(gl, glArgs, glResult) {
+    let [program, name] = glArgs;
+    this.cache.call("addUniform", program, name, glResult);
+  },
+
+  /**
+   * Called immediately *before* 'enableVertexAttribArray' or
+   * 'disableVertexAttribArray'is requested in the context.
+   *
+   * @param WebGLRenderingContext gl
+   *        The WebGL context initiating this call.
+   * @param array glArgs
+   *        Overridable arguments with which the function is called.
+   */
+  toggleVertexAttribArray: function(gl, glArgs) {
+    glArgs[0] = this.cache.call("getCurrentAttributeLocation", glArgs[0]);
+    return glArgs[0] < 0;
+  },
+
+  /**
+   * Called immediately *before* 'attribute_' is requested in the context.
+   *
+   * @param WebGLRenderingContext gl
+   *        The WebGL context initiating this call.
+   * @param array glArgs
+   *        Overridable arguments with which the function is called.
+   */
+  attribute_: function(gl, glArgs) {
+    glArgs[0] = this.cache.call("getCurrentAttributeLocation", glArgs[0]);
+    return glArgs[0] < 0;
+  },
+
+  /**
+   * Called immediately *before* 'uniform_' is requested in the context.
+   *
+   * @param WebGLRenderingContext gl
+   *        The WebGL context initiating this call.
+   * @param array glArgs
+   *        Overridable arguments with which the function is called.
+   */
+  uniform_: function(gl, glArgs) {
+    glArgs[0] = this.cache.call("getCurrentUniformLocation", glArgs[0]);
+    return !glArgs[0];
+  },
+
+  /**
+   * Executes a function in this object.
+   * This method makes sure that any handlers in the context observer are
+   * suppressed, hence stopping observing any context function calls.
+   *
+   * @param string funcName
+   *        The function to call.
+   */
+  call: function(funcName, ...args) {
+    let prevState = this.suppressHandlers;
+
+    this.suppressHandlers = true;
+    let result = this[funcName].apply(this, args);
+    this.suppressHandlers = prevState;
+
+    return result;
+  }
+};
+
+/**
+ * A cache storing WebGL state, like shaders, attributes or uniforms.
+ *
+ * @param WebGLObserver observer
+ *        The observer for the target context.
+ */
+function WebGLCache(observer) {
+  this._observer = observer;
+
+  this._shaders = new Map();
+  this._attributes = [];
+  this._uniforms = [];
+  this._attributesBridge = new Map();
+  this._uniformsBridge = new Map();
+}
+
+WebGLCache.prototype = {
+  /**
+   * Adds shader information to the cache.
+   *
+   * @param WebGLShader shader
+   *        The shader for which the source is to be cached. If the shader
+   *        was already cached, nothing happens.
+   * @param string text
+   *        The current shader text.
+   */
+  _addShaderInfo: function(shader, text) {
+    if (!this._shaders.has(shader)) {
+      this._shaders.set(shader, text);
+    }
+  },
+
+  /**
+   * Gets shader information from the cache.
+   *
+   * @param WebGLShader shader
+   *        The shader for which the source was cached.
+   * @return object | null
+   *         The original shader source, or null if there's a cache miss.
+   */
+  _getShaderInfo: function(shader) {
+    return this._shaders.get(shader);
+  },
+
+  /**
+   * Adds an attribute to the cache.
+   *
+   * @param WebGLProgram program
+   *        The program for which the attribute is bound. If the attribute
+   *        was already cached, nothing happens.
+   * @param string name
+   *        The attribute name.
+   * @param GLint value
+   *        The attribute value.
+   */
+  _addAttribute: function(program, name, value) {
+    let isCached = this._attributes.some(e => e.program == program && e.name == name);
+    if (isCached || value < 0) {
+      return;
+    }
+    let attributeInfo = {
+      program: program,
+      name: name,
+      value: value
+    };
+    this._attributes.push(attributeInfo);
+    this._attributesBridge.set(value, attributeInfo);
+  },
+
+  /**
+   * Adds a uniform to the cache.
+   *
+   * @param WebGLProgram program
+   *        The program for which the uniform is bound. If the uniform
+   *        was already cached, nothing happens.
+   * @param string name
+   *        The uniform name.
+   * @param WebGLUniformLocation value
+   *        The uniform value.
+   */
+  _addUniform: function(program, name, value) {
+    let isCached = this._uniforms.some(e => e.program == program && e.name == name);
+    if (isCached || !value) {
+      return;
+    }
+    let uniformInfo = {
+      program: program,
+      name: name,
+      value: value
+    };
+    this._uniforms.push(uniformInfo);
+    this._uniformsBridge.set(new XPCNativeWrapper(value), uniformInfo);
+  },
+
+  /**
+   * Gets all the cached attributes for a specific program.
+   *
+   * @param WebGLProgram program
+   *        The program for which the attributes are bound.
+   * @return array
+   *         A list containing information about all the attributes.
+   */
+  _getAttributesForProgram: function(program) {
+    return this._attributes.filter(e => e.program == program);
+  },
+
+  /**
+   * Gets all the cached uniforms for a specific program.
+   *
+   * @param WebGLProgram program
+   *        The program for which the uniforms are bound.
+   * @return array
+   *         A list containing information about all the uniforms.
+   */
+  _getUniformsForProgram: function(program) {
+    return this._uniforms.filter(e => e.program == program);
+  },
+
+  /**
+   * Updates the attribute locations for a specific program.
+   * This is necessary, for example, when the shader is relinked and all the
+   * attribute locations become obsolete.
+   *
+   * @param WebGLRenderingContext gl
+   *        The WebGL context owning the program.
+   * @param WebGLProgram program
+   *        The program for which the attributes need updating.
+   */
+  _updateAttributesForProgram: function(gl, program) {
+    let dirty = this._attributes.filter(e => e.program == program);
+    dirty.forEach(e => e.value = gl.getAttribLocation(program, e.name));
+  },
+
+  /**
+   * Updates the uniform locations for a specific program.
+   * This is necessary, for example, when the shader is relinked and all the
+   * uniform locations become obsolete.
+   *
+   * @param WebGLRenderingContext gl
+   *        The WebGL context owning the program.
+   * @param WebGLProgram program
+   *        The program for which the uniforms need updating.
+   */
+  _updateUniformsForProgram: function(gl, program) {
+    let dirty = this._uniforms.filter(e => e.program == program);
+    dirty.forEach(e => e.value = gl.getUniformLocation(program, e.name));
+  },
+
+  /**
+   * Gets the actual attribute location in a specific program.
+   * When relinked, all the attribute locations become obsolete and are updated
+   * in the cache. This method returns the (current) real attribute location.
+   *
+   * @param GLint initialValue
+   *        The initial attribute value.
+   * @return GLint
+   *         The current attribute value, or the initial value if it's already
+   *         up to date with its corresponding program.
+   */
+  _getCurrentAttributeLocation: function(initialValue) {
+    let currentInfo = this._attributesBridge.get(initialValue);
+    return currentInfo ? currentInfo.value : initialValue;
+  },
+
+  /**
+   * Gets the actual uniform location in a specific program.
+   * When relinked, all the uniform locations become obsolete and are updated
+   * in the cache. This method returns the (current) real uniform location.
+   *
+   * @param WebGLUniformLocation initialValue
+   *        The initial uniform value.
+   * @return WebGLUniformLocation
+   *         The current uniform value, or the initial value if it's already
+   *         up to date with its corresponding program.
+   */
+  _getCurrentUniformLocation: function(initialValue) {
+    let currentInfo = this._uniformsBridge.get(initialValue);
+    return currentInfo ? currentInfo.value : initialValue;
+  },
+
+  /**
+   * Executes a function in this object.
+   * This method makes sure that any handlers in the context observer are
+   * suppressed, hence stopping observing any context function calls.
+   *
+   * @param string funcName
+   *        The function to call.
+   * @return any
+   *         The called function result.
+   */
+  call: function(funcName, ...aArgs) {
+    let prevState = this._observer.suppressHandlers;
+
+    this._observer.suppressHandlers = true;
+    let result = this["_" + funcName].apply(this, aArgs);
+    this._observer.suppressHandlers = prevState;
+
+    return result;
+  }
+};
+
+/**
+ * A mechanism for injecting or qureying state into/from a WebGL context.
+ *
+ * @param WebGLObserver observer
+ *        The observer for the target context.
+ */
+function WebGLProxy(observer) {
+  this._observer = observer;
+}
+
+WebGLProxy.prototype = {
+  get cache() this._observer.cache,
+
+  /**
+   * Changes a shader's source code and relinks the respective program.
+   *
+   * @param WebGLRenderingContext gl
+   *        The WebGL context owning the program.
+   * @param WebGLProgram program
+   *        The program who's linked shader is to be modified.
+   * @param WebGLShader shader
+   *        The shader to be modified.
+   * @param string text
+   *        The new shader source code.
+   * @return string
+   *         The shader's compilation and linking status.
+   */
+  _compileShader: function(gl, program, shader, text) {
+    gl.shaderSource(shader, text);
+    gl.compileShader(shader);
+    gl.linkProgram(program);
+
+    let error = { compile: "", link: "" };
+
+    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
+      error.compile = gl.getShaderInfoLog(shader);
+    }
+    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
+      error.link = gl.getShaderInfoLog(shader);
+    }
+
+    this.cache.call("updateAttributesForProgram", gl, program);
+    this.cache.call("updateUniformsForProgram", gl, program);
+
+    return error;
+  },
+
+  /**
+   * Executes a function in this object.
+   * This method makes sure that any handlers in the context observer are
+   * suppressed, hence stopping observing any context function calls.
+   *
+   * @param string funcName
+   *        The function to call.
+   * @return any
+   *         The called function result.
+   */
+  call: function(funcName, ...aArgs) {
+    let prevState = this._observer.suppressHandlers;
+
+    this._observer.suppressHandlers = true;
+    let result = this["_" + funcName].apply(this, aArgs);
+    this._observer.suppressHandlers = prevState;
+
+    return result;
+  }
+};
--- a/toolkit/devtools/server/main.js
+++ b/toolkit/devtools/server/main.js
@@ -361,16 +361,17 @@ var DebuggerServer = {
     this.addActors("resource://gre/modules/devtools/server/actors/webconsole.js");
     this.addActors("resource://gre/modules/devtools/server/actors/gcli.js");
     if ("nsIProfiler" in Ci)
       this.addActors("resource://gre/modules/devtools/server/actors/profiler.js");
 
     this.addActors("resource://gre/modules/devtools/server/actors/styleeditor.js");
     this.addActors("resource://gre/modules/devtools/server/actors/webapps.js");
     this.registerModule("devtools/server/actors/inspector");
+    this.registerModule("devtools/server/actors/webgl");
     this.registerModule("devtools/server/actors/tracer");
     this.registerModule("devtools/server/actors/device");
   },
 
   /**
    * Install tab actors in documents loaded in content childs
    */
   addChildActors: function () {
@@ -379,16 +380,17 @@ var DebuggerServer = {
     // but childtab.js hasn't been loaded yet.
     if (!("BrowserTabActor" in this)) {
       this.addActors("resource://gre/modules/devtools/server/actors/webbrowser.js");
       this.addActors("resource://gre/modules/devtools/server/actors/script.js");
       this.addActors("resource://gre/modules/devtools/server/actors/webconsole.js");
       this.addActors("resource://gre/modules/devtools/server/actors/gcli.js");
       this.addActors("resource://gre/modules/devtools/server/actors/styleeditor.js");
       this.registerModule("devtools/server/actors/inspector");
+      this.registerModule("devtools/server/actors/webgl");
     }
     if (!("ContentAppActor" in DebuggerServer)) {
       this.addActors("resource://gre/modules/devtools/server/actors/childtab.js");
     }
   },
 
   /**
    * Listens on the given port or socket file for remote debugger connections.