Bug 938549 - Recompiling shaders when highlighting is very heavy and loses cached state. r=rcampbell, a=bajaj
authorVictor Porof <vporof@mozilla.com>
Tue, 26 Nov 2013 10:51:20 +0200
changeset 167583 56f4f6a8d4d97ab81465bbb96aaf7b8f81e5e1d9
parent 167582 8dcdb738ddbc1e8fcd63015ebeb26c84d72d0b23
child 167584 af2d122f17cadea0ee6a917aad6bf5a8cd808ea2
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)
reviewersrcampbell, bajaj
bugs938549
milestone27.0a2
Bug 938549 - Recompiling shaders when highlighting is very heavy and loses cached state. r=rcampbell, a=bajaj
browser/devtools/shadereditor/shadereditor.js
browser/devtools/shadereditor/test/browser.ini
browser/devtools/shadereditor/test/browser_se_programs-blackbox-01.js
browser/devtools/shadereditor/test/browser_se_programs-blackbox-02.js
browser/devtools/shadereditor/test/browser_se_programs-blackbox.js
browser/devtools/shadereditor/test/browser_se_programs-highlight-01.js
browser/devtools/shadereditor/test/browser_se_programs-highlight-02.js
browser/devtools/shadereditor/test/browser_se_programs-highlight.js
browser/devtools/shadereditor/test/browser_webgl-actor-test-06.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-15.js
browser/devtools/shadereditor/test/browser_webgl-actor-test-16.js
browser/devtools/shadereditor/test/doc_blended-geometry.html
browser/devtools/shadereditor/test/head.js
toolkit/devtools/server/actors/webgl.js
--- a/browser/devtools/shadereditor/shadereditor.js
+++ b/browser/devtools/shadereditor/shadereditor.js
@@ -27,17 +27,17 @@ const EVENTS = {
   // When the vertex and fragment sources were shown in the editor.
   SOURCES_SHOWN: "ShaderEditor:SourcesShown",
 
   // When a shader's source was edited and compiled via the editor.
   SHADER_COMPILED: "ShaderEditor:ShaderCompiled"
 };
 
 const STRINGS_URI = "chrome://browser/locale/devtools/shadereditor.properties"
-const HIGHLIGHT_COLOR = [1, 0, 0, 1]; // rgba
+const HIGHLIGHT_TINT = [1, 0, 0.25, 1]; // rgba
 const TYPING_MAX_DELAY = 500; // ms
 const SHADERS_AUTOGROW_ITEMS = 4;
 const GUTTER_ERROR_PANEL_OFFSET_X = 7; // px
 const GUTTER_ERROR_PANEL_DELAY = 100; // ms
 const DEFAULT_EDITOR_CONFIG = {
   gutters: ["errors"],
   lineNumbers: true,
   showAnnotationRuler: true
@@ -292,17 +292,17 @@ let ShadersListView = Heritage.extend(Wi
   },
 
   /**
    * The mouseenter listener for the programs container.
    */
   _onProgramMouseEnter: function(e) {
     let sourceItem = this.getItemForElement(e.target, { noSiblings: true });
     if (sourceItem && !sourceItem.attachment.isBlackBoxed) {
-      sourceItem.attachment.programActor.highlight(HIGHLIGHT_COLOR);
+      sourceItem.attachment.programActor.highlight(HIGHLIGHT_TINT);
 
       if (e instanceof Event) {
         e.preventDefault();
         e.stopPropagation();
       }
     }
   },
 
--- a/browser/devtools/shadereditor/test/browser.ini
+++ b/browser/devtools/shadereditor/test/browser.ini
@@ -1,27 +1,30 @@
 [DEFAULT]
 support-files =
+  doc_blended-geometry.html
   doc_multiple-contexts.html
   doc_overlapping-geometry.html
   doc_shader-order.html
   doc_simple-canvas.html
   head.js
 
 [browser_se_aaa_run_first_leaktest.js]
 [browser_se_bfcache.js]
 [browser_se_editors-contents.js]
 [browser_se_editors-error-gutter.js]
 [browser_se_editors-error-tooltip.js]
 [browser_se_editors-lazy-init.js]
 [browser_se_first-run.js]
 [browser_se_navigation.js]
-[browser_se_programs-blackbox.js]
+[browser_se_programs-blackbox-01.js]
+[browser_se_programs-blackbox-02.js]
 [browser_se_programs-cache.js]
-[browser_se_programs-highlight.js]
+[browser_se_programs-highlight-01.js]
+[browser_se_programs-highlight-02.js]
 [browser_se_programs-list.js]
 [browser_se_shaders-edit-01.js]
 [browser_se_shaders-edit-02.js]
 [browser_se_shaders-edit-03.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]
rename from browser/devtools/shadereditor/test/browser_se_programs-blackbox.js
rename to browser/devtools/shadereditor/test/browser_se_programs-blackbox-01.js
--- a/browser/devtools/shadereditor/test/browser_se_programs-blackbox.js
+++ b/browser/devtools/shadereditor/test/browser_se_programs-blackbox-01.js
@@ -79,34 +79,34 @@ function ifWebGLSupported() {
   ok(true, "The second program was correctly blackboxed.");
 
   ShadersListView._onProgramMouseEnter({ target: getItemLabel(panel, 0) });
 
   yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas1");
   yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas2");
   yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas1");
   yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas2");
-  ok(true, "Highlighting didn't work while blackboxed (1).");
+  ok(true, "Highlighting shouldn't work while blackboxed (1).");
 
   ShadersListView._onProgramMouseLeave({ target: getItemLabel(panel, 0) });
   ShadersListView._onProgramMouseEnter({ target: getItemLabel(panel, 1) });
 
   yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas1");
   yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas2");
   yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas1");
   yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas2");
-  ok(true, "Highlighting didn't work while blackboxed (2).");
+  ok(true, "Highlighting shouldn't work while blackboxed (2).");
 
   ShadersListView._onProgramMouseLeave({ target: getItemLabel(panel, 1) });
 
   yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas1");
   yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas2");
   yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas1");
   yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 0, b: 0, a: 255 }, true, "#canvas2");
-  ok(true, "Highlighting didn't work while blackboxed (3).");
+  ok(true, "Highlighting shouldn't work while blackboxed (3).");
 
   getBlackBoxCheckbox(panel, 0).click();
   getBlackBoxCheckbox(panel, 1).click();
 
   ok(!ShadersListView.selectedAttachment.isBlackBoxed,
     "The first program should now be unblackboxed.");
   is(getBlackBoxCheckbox(panel, 0).checked, true,
     "The first blackbox checkbox should now be rechecked.");
@@ -128,19 +128,19 @@ function ifWebGLSupported() {
   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 program was correctly highlighted.");
 
   ShadersListView._onProgramMouseLeave({ target: getItemLabel(panel, 0) });
   ShadersListView._onProgramMouseEnter({ target: getItemLabel(panel, 1) });
 
   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: 255, g: 0, b: 0, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 64, 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: 255, g: 0, b: 0, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 0, b: 64, a: 255 }, true, "#canvas2");
   ok(true, "The second program was correctly highlighted.");
 
   ShadersListView._onProgramMouseLeave({ target: getItemLabel(panel, 1) });
 
   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");
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_se_programs-blackbox-02.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if blackboxing a program works properly in tandem with blended
+ * overlapping geometry.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, panel] = yield initShaderEditor(BLENDED_GEOMETRY_CANVAS_URL);
+  let { gFront, EVENTS, ShadersListView, ShadersEditorsView } = panel.panelWin;
+
+  reload(target);
+  let firstProgramActor = yield once(gFront, "program-linked");
+  let secondProgramActor = yield once(gFront, "program-linked");
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 127, g: 127, b: 127, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 64, y: 64 }, { r: 0, g: 127, b: 127, a: 127 }, true);
+  ok(true, "The canvas was correctly drawn.");
+
+  getBlackBoxCheckbox(panel, 0).click();
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 64, y: 64 }, { r: 0, g: 0, b: 0, a: 127 }, true);
+  ok(true, "The first program was correctly blackboxed.");
+
+  getBlackBoxCheckbox(panel, 0).click();
+  getBlackBoxCheckbox(panel, 1).click();
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 127, g: 127, b: 127, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 64, y: 64 }, { r: 127, g: 127, b: 127, a: 255 }, true);
+  ok(true, "The second program was correctly blackboxed.");
+
+  getBlackBoxCheckbox(panel, 1).click();
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 127, g: 127, b: 127, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 64, y: 64 }, { r: 0, g: 127, b: 127, a: 127 }, true);
+  ok(true, "The two programs were correctly unblackboxed.");
+
+  getBlackBoxCheckbox(panel, 0).click();
+  getBlackBoxCheckbox(panel, 1).click();
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 64, y: 64 }, { r: 0, g: 0, b: 0, a: 255 }, true);
+  ok(true, "The two programs were correctly blackboxed again.");
+
+  getBlackBoxCheckbox(panel, 0).click();
+  getBlackBoxCheckbox(panel, 1).click();
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 127, g: 127, b: 127, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 64, y: 64 }, { r: 0, g: 127, b: 127, a: 127 }, true);
+  ok(true, "The two programs were correctly unblackboxed again.");
+
+  yield teardown(panel);
+  finish();
+}
+
+function getBlackBoxCheckbox(aPanel, aIndex) {
+  return aPanel.panelWin.document.querySelectorAll(
+    ".side-menu-widget-item-checkbox")[aIndex];
+}
+
+function once(aTarget, aEvent) {
+  let deferred = promise.defer();
+  aTarget.once(aEvent, deferred.resolve);
+  return deferred.promise;
+}
rename from browser/devtools/shadereditor/test/browser_se_programs-highlight.js
rename to browser/devtools/shadereditor/test/browser_se_programs-highlight-01.js
--- a/browser/devtools/shadereditor/test/browser_se_programs-highlight.js
+++ b/browser/devtools/shadereditor/test/browser_se_programs-highlight-01.js
@@ -42,19 +42,19 @@ function ifWebGLSupported() {
   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 program was correctly highlighted.");
 
   ShadersListView._onProgramMouseLeave({ target: getItemLabel(panel, 0) });
   ShadersListView._onProgramMouseEnter({ target: getItemLabel(panel, 1) });
 
   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: 255, g: 0, b: 0, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, g: 0, b: 64, 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: 255, g: 0, b: 0, a: 255 }, true, "#canvas2");
+  yield ensurePixelIs(debuggee, { x: 127, y: 127 }, { r: 0, g: 0, b: 64, a: 255 }, true, "#canvas2");
   ok(true, "The second program was correctly highlighted.");
 
   ShadersListView._onProgramMouseLeave({ target: getItemLabel(panel, 1) });
 
   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");
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/browser_se_programs-highlight-02.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if highlighting a program works properly in tandem with blended
+ * overlapping geometry.
+ */
+
+function ifWebGLSupported() {
+  let [target, debuggee, panel] = yield initShaderEditor(BLENDED_GEOMETRY_CANVAS_URL);
+  let { gFront, EVENTS, ShadersListView, ShadersEditorsView } = panel.panelWin;
+
+  reload(target);
+  let firstProgramActor = yield once(gFront, "program-linked");
+  let secondProgramActor = yield once(gFront, "program-linked");
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 127, g: 127, b: 127, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 64, y: 64 }, { r: 0, g: 127, b: 127, a: 127 }, true);
+  ok(true, "The canvas was correctly drawn.");
+
+  ShadersListView._onProgramMouseEnter({ target: getItemLabel(panel, 0) });
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 127, g: 0, b: 32, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 64, y: 64 }, { r: 0, g: 0, b: 32, a: 127 }, true);
+  ok(true, "The first program was correctly highlighted.");
+
+  ShadersListView._onProgramMouseLeave({ target: getItemLabel(panel, 0) });
+  ShadersListView._onProgramMouseEnter({ target: getItemLabel(panel, 1) });
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 127, g: 127, b: 127, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 64, y: 64 }, { r: 255, g: 0, b: 64, a: 255 }, true);
+  ok(true, "The second program was correctly highlighted.");
+
+  ShadersListView._onProgramMouseLeave({ target: getItemLabel(panel, 1) });
+
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 127, g: 127, b: 127, a: 255 }, true);
+  yield ensurePixelIs(debuggee, { x: 64, y: 64 }, { r: 0, g: 127, b: 127, a: 127 }, true);
+  ok(true, "The two programs were correctly unhighlighted.");
+
+  yield teardown(panel);
+  finish();
+}
+
+function getItemLabel(aPanel, aIndex) {
+  return aPanel.panelWin.document.querySelectorAll(
+    ".side-menu-widget-item-label")[aIndex];
+}
+
+function once(aTarget, aEvent) {
+  let deferred = promise.defer();
+  aTarget.once(aEvent, deferred.resolve);
+  return deferred.promise;
+}
--- a/browser/devtools/shadereditor/test/browser_webgl-actor-test-06.js
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-06.js
@@ -14,19 +14,19 @@ function ifWebGLSupported() {
   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 corner pixel colors are 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 programActor.highlight([0, 1, 0, 1]);
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, 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 preserved after highlighting.");
   ok(true, "The corner pixel colors are 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 corner pixel colors are correct after unhighlighting.");
--- a/browser/devtools/shadereditor/test/browser_webgl-actor-test-09.js
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-09.js
@@ -65,19 +65,19 @@ function ifWebGLSupported() {
   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);
+  yield programActor.highlight([0, 1, 0, 1]);
+  yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, 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, "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);
--- a/browser/devtools/shadereditor/test/browser_webgl-actor-test-10.js
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-10.js
@@ -22,20 +22,23 @@ function ifWebGLSupported() {
   ok(true, "Canvas was correctly instrumented on the third navigation.");
 
   yield removeTab(target.tab);
   finish();
 
   function testHighlighting(programActor) {
     return Task.spawn(function() {
       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 ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+      ok(true, "The corner pixel colors are 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.highlight([0, 1, 0, 1]);
+      yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, 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 corner pixel colors are 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 ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+      ok(true, "The corner pixel colors are correct after unhighlighting.");
     });
   }
 }
--- a/browser/devtools/shadereditor/test/browser_webgl-actor-test-15.js
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-15.js
@@ -76,25 +76,28 @@ function ifWebGLSupported() {
       isnot(newProgramActors[1], oldProgramActor,
         "The old program actor is not equal to the new second program actor.");
     });
   }
 
   function checkHighlightingInTheFirstPage(programActor) {
     return Task.spawn(function() {
       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 ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+      ok(true, "The corner pixel colors are 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.highlight([0, 1, 0, 1]);
+      yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, 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 corner pixel colors are 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 ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+      ok(true, "The corner pixel colors are correct after unhighlighting.");
     });
   }
 
   function checkHighlightingInTheSecondPage(firstProgramActor, secondProgramActor) {
     return Task.spawn(function() {
       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");
--- a/browser/devtools/shadereditor/test/browser_webgl-actor-test-16.js
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-16.js
@@ -76,25 +76,28 @@ function ifWebGLSupported() {
   ok(true, "The cached programs behave correctly after navigating forward and reloading.");
 
   yield removeTab(target.tab);
   finish();
 
   function checkHighlightingInTheFirstPage(programActor) {
     return Task.spawn(function() {
       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 ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+      ok(true, "The corner pixel colors are 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.highlight([0, 1, 0, 1]);
+      yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 0, 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 corner pixel colors are 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 ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
+      ok(true, "The corner pixel colors are correct after unhighlighting.");
     });
   }
 
   function checkHighlightingInTheSecondPage(firstProgramActor, secondProgramActor) {
     return Task.spawn(function() {
       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");
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shadereditor/test/doc_blended-geometry.html
@@ -0,0 +1,136 @@
+<!-- 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;
+      uniform float uDepth;
+
+      void main(void) {
+        gl_Position = vec4(aVertexPosition, uDepth);
+      }
+    </script>
+
+    <script id="shader-fs-0" type="x-shader/x-fragment">
+      precision lowp float;
+
+      void main(void) {
+        gl_FragColor = vec4(0.5, 0.5, 0.5, 1.0);
+      }
+    </script>
+
+    <script id="shader-fs-1" type="x-shader/x-fragment">
+      precision lowp float;
+
+      void main(void) {
+        gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
+      }
+    </script>
+  </head>
+
+  <body>
+    <canvas id="canvas" width="128" height="128"></canvas>
+
+    <script type="text/javascript;version=1.8">
+      "use strict";
+
+      let canvas, gl;
+      let program = [];
+      let squareVerticesPositionBuffer;
+      let vertexPositionAttribute = [];
+      let depthUniform = [];
+
+      window.onload = function() {
+        canvas = document.querySelector("canvas");
+        gl = canvas.getContext("webgl");
+        gl.clearColor(0.0, 0.0, 0.0, 1.0);
+
+        initProgram(0);
+        initProgram(1);
+        initBuffers();
+        drawScene();
+      }
+
+      function initProgram(i) {
+        let vertexShader = getShader("shader-vs");
+        let fragmentShader = getShader("shader-fs-" + i);
+
+        program[i] = gl.createProgram();
+        gl.attachShader(program[i], vertexShader);
+        gl.attachShader(program[i], fragmentShader);
+        gl.linkProgram(program[i]);
+
+        vertexPositionAttribute[i] = gl.getAttribLocation(program[i], "aVertexPosition");
+        gl.enableVertexAttribArray(vertexPositionAttribute[i]);
+
+        depthUniform[i] = gl.getUniformLocation(program[i], "uDepth");
+      }
+
+      function getShader(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);
+      }
+
+      function drawScene() {
+        gl.clear(gl.COLOR_BUFFER_BIT);
+
+        for (let i = 0; i < 2; i++) {
+          gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesPositionBuffer);
+          gl.vertexAttribPointer(vertexPositionAttribute[i], 3, gl.FLOAT, false, 0, 0);
+
+          gl.useProgram(program[i]);
+          gl.uniform1f(depthUniform[i], i + 1);
+          blend(i);
+          gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
+        }
+
+        window.requestAnimationFrame(drawScene);
+      }
+
+      function blend(i) {
+        if (i == 0) {
+          gl.disable(gl.BLEND);
+        }
+        else if (i == 1) {
+          gl.enable(gl.BLEND);
+          gl.blendColor(0.5, 0, 0, 0.25);
+          gl.blendEquationSeparate(
+            gl.FUNC_REVERSE_SUBTRACT, gl.FUNC_SUBTRACT);
+          gl.blendFuncSeparate(
+            gl.CONSTANT_COLOR, gl.ONE_MINUS_CONSTANT_COLOR,
+            gl.ONE_MINUS_CONSTANT_COLOR, gl.CONSTANT_COLOR);
+        }
+      }
+    </script>
+  </body>
+
+</html>
--- a/browser/devtools/shadereditor/test/head.js
+++ b/browser/devtools/shadereditor/test/head.js
@@ -23,16 +23,17 @@ let TiltGL = devtools.require("devtools/
 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";
 const OVERLAPPING_GEOMETRY_CANVAS_URL = EXAMPLE_URL + "doc_overlapping-geometry.html";
+const BLENDED_GEOMETRY_CANVAS_URL = EXAMPLE_URL + "doc_blended-geometry.html";
 
 // All tests are asynchronous.
 waitForExplicitFinish();
 
 let gToolEnabled = Services.prefs.getBoolPref("devtools.shadereditor.enabled");
 
 registerCleanupFunction(() => {
   info("finish() was called, cleaning up...");
--- a/toolkit/devtools/server/actors/webgl.js
+++ b/toolkit/devtools/server/actors/webgl.js
@@ -10,26 +10,20 @@ Cu.import("resource://gre/modules/Servic
 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");
-
 // These traits are bit masks. Make sure they're powers of 2.
 const PROGRAM_DEFAULT_TRAITS = 0;
 const PROGRAM_BLACKBOX_TRAIT = 1;
+const PROGRAM_HIGHLIGHT_TRAIT = 2;
 
 exports.register = function(handle) {
   handle.addTabActor(WebGLActor, "webglActor");
 }
 
 exports.unregister = function(handle) {
   handle.removeTabActor(WebGLActor);
 }
@@ -155,36 +149,31 @@ let ProgramActor = protocol.ActorClass({
    */
   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.
+   * Highlights any geometry rendered using this program.
    */
-  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;
+  highlight: method(function(tint) {
+    this.linkedProxy.highlightTint = tint;
+    this.linkedCache.setProgramTrait(this.program, PROGRAM_HIGHLIGHT_TRAIT);
   }, {
-    request: { color: Arg(0, "array:string") },
+    request: { tint: Arg(0, "array:number") },
     oneway: true
   }),
 
   /**
-   * Reverts this program's fragment shader to the latest user-defined source.
+   * Allows geometry to be rendered normally using this program.
    */
   unhighlight: method(function() {
-    let shaderActor = this._getShaderActor("fragment");
-    shaderActor.compile(shaderActor.text);
+    this.linkedCache.unsetProgramTrait(this.program, PROGRAM_HIGHLIGHT_TRAIT);
   }, {
     oneway: true
   }),
 
   /**
    * Prevents any geometry from being rendered using this program.
    */
   blackbox: method(function() {
@@ -472,80 +461,98 @@ let WebGLInstrumenter = {
    * Overrides a specific method in a HTMLCanvasElement context.
    *
    * @param WebGLObserver observer
    *        The observer watching function calls in the context.
    * @param WebGLRenderingContext context
    *        The targeted WebGL context instance.
    * @param string funcName
    *        The function to override.
-   * @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.
+   * @param array callbackName [optional]
+   *        The two callback function names in the observer, corresponding to
+   *        the "before" and "after" invocation times. If unspecified, they will
+   *        default to the name of the function to override.
    * @param number timing [optional]
    *        When to issue the callback in relation to the actual context
-   *        function call. Availalble values are 0 for "before" (default)
-   *        and 1 for "after".
+   *        function call. Availalble values are -1 for "before" (default)
+   *        1 for "after" and 0 for "before and after".
    */
-  _instrument: function(observer, context, funcName, callbackName, timing = 0) {
+  _instrument: function(observer, context, funcName, callbackName = [], timing = -1) {
     let { cache, proxy } = observer.for(context);
     let originalFunc = context[funcName];
-    let proxyFuncName = callbackName || funcName;
+    let beforeFuncName = callbackName[0] || funcName;
+    let afterFuncName = callbackName[1] || callbackName[0] || funcName;
 
     context[funcName] = function(...glArgs) {
-      if (timing == 0 && !observer.suppressHandlers) {
-        let glBreak = observer[proxyFuncName](glArgs, cache, proxy);
+      if (timing <= 0 && !observer.suppressHandlers) {
+        let glBreak = observer[beforeFuncName](glArgs, cache, proxy);
         if (glBreak) return undefined;
       }
 
       let glResult = originalFunc.apply(this, glArgs);
 
-      if (timing == 1 && !observer.suppressHandlers) {
-        let glBreak = observer[proxyFuncName](glArgs, glResult, cache, proxy);
+      if (timing >= 0 && !observer.suppressHandlers) {
+        let glBreak = observer[afterFuncName](glArgs, glResult, cache, proxy);
         if (glBreak) return undefined;
       }
 
       return glResult;
     };
   },
 
   /**
    * Override mappings for WebGL methods.
    */
   _methods: [{
     timing: 1, // after
     functions: [
       "linkProgram", "getAttribLocation", "getUniformLocation"
     ]
   }, {
-    callback: "toggleVertexAttribArray",
+    timing: -1, // before
+    callback: [
+      "toggleVertexAttribArray"
+    ],
     functions: [
       "enableVertexAttribArray", "disableVertexAttribArray"
     ]
   }, {
-    callback: "attribute_",
+    timing: -1, // before
+    callback: [
+      "attribute_"
+    ],
     functions: [
       "vertexAttrib1f", "vertexAttrib2f", "vertexAttrib3f", "vertexAttrib4f",
       "vertexAttrib1fv", "vertexAttrib2fv", "vertexAttrib3fv", "vertexAttrib4fv",
       "vertexAttribPointer"
     ]
   }, {
-    callback: "uniform_",
+    timing: -1, // before
+    callback: [
+      "uniform_"
+    ],
     functions: [
       "uniform1i", "uniform2i", "uniform3i", "uniform4i",
       "uniform1f", "uniform2f", "uniform3f", "uniform4f",
       "uniform1iv", "uniform2iv", "uniform3iv", "uniform4iv",
       "uniform1fv", "uniform2fv", "uniform3fv", "uniform4fv",
       "uniformMatrix2fv", "uniformMatrix3fv", "uniformMatrix4fv"
     ]
   }, {
-    timing: 1, // after
-    functions: ["useProgram"]
+    timing: -1, // before
+    functions: [
+      "useProgram", "enable", "disable", "blendColor",
+      "blendEquation", "blendEquationSeparate",
+      "blendFunc", "blendFuncSeparate"
+    ]
   }, {
-    callback: "draw_",
+    timing: 0, // before and after
+    callback: [
+      "beforeDraw_", "afterDraw_"
+    ],
     functions: [
       "drawArrays", "drawElements"
     ]
   }]
   // TODO: It'd be a good idea to handle other functions as well:
   //   - getActiveUniform
   //   - getUniform
   //   - getActiveAttrib
@@ -568,16 +575,17 @@ WebGLObserver.prototype = {
    * @param number id
    *        The id of the window containing the WebGL context.
    * @param WebGLRenderingContext context
    *        The WebGL context used in the cache and proxy instances.
    */
   registerContextForWindow: function(id, context) {
     let cache = new WebGLCache(id, context);
     let proxy = new WebGLProxy(id, context, cache, this);
+    cache.refreshState(proxy);
 
     this._contexts.set(context, {
       ownerWindow: id,
       cache: cache,
       proxy: proxy
     });
   },
 
@@ -701,73 +709,232 @@ WebGLObserver.prototype = {
    *        The state storage for the WebGL context initiating this call.
    */
   uniform_: function(glArgs, cache) {
     glArgs[0] = cache.getCurrentUniformLocation(glArgs[0]);
     return !glArgs[0]; // Return true to break original function call.
   },
 
   /**
-   * Called immediately *after* 'useProgram' is requested in the context.
+   * Called immediately *before* 'useProgram' is requested in the context.
+   *
+   * @param array glArgs
+   *        Overridable arguments with which the function is called.
+   * @param WebGLCache cache
+   *        The state storage for the WebGL context initiating this call.
+   */
+  useProgram: function(glArgs, cache) {
+    // Manually keeping a cache and not using gl.getParameter(CURRENT_PROGRAM)
+    // because gl.get* functions are slow as potatoes.
+    cache.currentProgram = glArgs[0];
+  },
+
+  /**
+   * Called immediately *before* 'enable' is requested in the context.
+   *
+   * @param array glArgs
+   *        Overridable arguments with which the function is called.
+   * @param WebGLCache cache
+   *        The state storage for the WebGL context initiating this call.
+   */
+  enable: function(glArgs, cache) {
+    cache.currentState[glArgs[0]] = true;
+  },
+
+  /**
+   * Called immediately *before* 'disable' is requested in the context.
+   *
+   * @param array glArgs
+   *        Overridable arguments with which the function is called.
+   * @param WebGLCache cache
+   *        The state storage for the WebGL context initiating this call.
+   */
+  disable: function(glArgs, cache) {
+    cache.currentState[glArgs[0]] = false;
+  },
+
+  /**
+   * Called immediately *before* 'blendColor' is requested in the context.
    *
    * @param array glArgs
    *        Overridable arguments with which the function is called.
-   * @param void glResult
-   *        The returned value of the original function call.
+   * @param WebGLCache cache
+   *        The state storage for the WebGL context initiating this call.
+   */
+  blendColor: function(glArgs, cache) {
+    let blendColor = cache.currentState.blendColor;
+    blendColor[0] = glArgs[0];
+    blendColor[1] = glArgs[1];
+    blendColor[2] = glArgs[2];
+    blendColor[3] = glArgs[3];
+  },
+
+  /**
+   * Called immediately *before* 'blendEquation' is requested in the context.
+   *
+   * @param array glArgs
+   *        Overridable arguments with which the function is called.
+   * @param WebGLCache cache
+   *        The state storage for the WebGL context initiating this call.
+   */
+  blendEquation: function(glArgs, cache) {
+    let state = cache.currentState;
+    state.blendEquationRgb = state.blendEquationAlpha = glArgs[0];
+  },
+
+  /**
+   * Called immediately *before* 'blendEquationSeparate' is requested in the context.
+   *
+   * @param array glArgs
+   *        Overridable arguments with which the function is called.
    * @param WebGLCache cache
    *        The state storage for the WebGL context initiating this call.
    */
-  useProgram: function(glArgs, glResult, cache) {
-    // Manually keeping a cache and not using gl.getParameter(CURRENT_PROGRAM)
-    // because gl.get* functions are slow as potatoes.
-    cache.currentProgram = glArgs[0];
+  blendEquationSeparate: function(glArgs, cache) {
+    let state = cache.currentState;
+    state.blendEquationRgb = glArgs[0];
+    state.blendEquationAlpha = glArgs[1];
+  },
+
+  /**
+   * Called immediately *before* 'blendFunc' is requested in the context.
+   *
+   * @param array glArgs
+   *        Overridable arguments with which the function is called.
+   * @param WebGLCache cache
+   *        The state storage for the WebGL context initiating this call.
+   */
+  blendFunc: function(glArgs, cache) {
+    let state = cache.currentState;
+    state.blendSrcRgb = state.blendSrcAlpha = glArgs[0];
+    state.blendDstRgb = state.blendDstAlpha = glArgs[1];
+  },
+
+  /**
+   * Called immediately *before* 'blendFuncSeparate' is requested in the context.
+   *
+   * @param array glArgs
+   *        Overridable arguments with which the function is called.
+   * @param WebGLCache cache
+   *        The state storage for the WebGL context initiating this call.
+   */
+  blendFuncSeparate: function(glArgs, cache) {
+    let state = cache.currentState;
+    state.blendSrcRgb = glArgs[0];
+    state.blendDstRgb = glArgs[1];
+    state.blendSrcAlpha = glArgs[2];
+    state.blendDstAlpha = glArgs[3];
   },
 
   /**
    * Called immediately *before* 'drawArrays' or 'drawElements' is requested
    * in the context.
    *
    * @param array glArgs
    *        Overridable arguments with which the function is called.
    * @param WebGLCache cache
    *        The state storage for the WebGL context initiating this call.
+   * @param WebGLProxy proxy
+   *        The proxy methods for the WebGL context initiating this call.
    */
-  draw_: function(glArgs, cache) {
-    // Return true to break original function call.
-    return cache.currentProgramTraits & PROGRAM_BLACKBOX_TRAIT;
+  beforeDraw_: function(glArgs, cache, proxy) {
+    let traits = cache.currentProgramTraits;
+
+    // Handle program blackboxing.
+    if (traits & PROGRAM_BLACKBOX_TRAIT) {
+      return true; // Return true to break original function call.
+    }
+    // Handle program highlighting.
+    if (traits & PROGRAM_HIGHLIGHT_TRAIT) {
+      proxy.enableHighlighting();
+    }
+
+    return false;
+  },
+
+  /**
+   * Called immediately *after* 'drawArrays' or 'drawElements' is requested
+   * in the context.
+   *
+   * @param array glArgs
+   *        Overridable arguments with which the function is called.
+   * @param void glResult
+   *        The returned value of the original function call.
+   * @param WebGLCache cache
+   *        The state storage for the WebGL context initiating this call.
+   * @param WebGLProxy proxy
+   *        The proxy methods for the WebGL context initiating this call.
+   */
+  afterDraw_: function(glArgs, glResult, cache, proxy) {
+    let traits = cache.currentProgramTraits;
+
+    // Handle program highlighting.
+    if (traits & PROGRAM_HIGHLIGHT_TRAIT) {
+      proxy.disableHighlighting();
+    }
   }
 };
 
 /**
  * A mechanism for storing a single WebGL context's state, programs, shaders,
  * attributes or uniforms.
  *
  * @param number id
  *        The id of the window containing the WebGL context.
  * @param WebGLRenderingContext context
  *        The WebGL context for which the state is stored.
  */
 function WebGLCache(id, context) {
   this._id = id;
   this._gl = context;
   this._programs = new Map();
+  this.currentState = {};
 }
 
 WebGLCache.prototype = {
   _id: 0,
   _gl: null,
   _programs: null,
   _currentProgramInfo: null,
   _currentAttributesMap: null,
   _currentUniformsMap: null,
 
   get ownerWindow() this._id,
   get ownerContext() this._gl,
 
   /**
+   * A collection of flags or properties representing the context's state.
+   * Implemented as an object hash and not a Map instance because keys are
+   * always either strings or numbers.
+   */
+  currentState: null,
+
+  /**
+   * Populates the current state with values retrieved from the context.
+   *
+   * @param WebGLProxy proxy
+   *        The proxy methods for the WebGL context owning the state.
+   */
+  refreshState: function(proxy) {
+    let gl = this._gl;
+    let s = this.currentState;
+
+    // Populate only with the necessary parameters. Not all default WebGL
+    // state values are required.
+    s[gl.BLEND] = proxy.isEnabled("BLEND");
+    s.blendColor = proxy.getParameter("BLEND_COLOR");
+    s.blendEquationRgb = proxy.getParameter("BLEND_EQUATION_RGB");
+    s.blendEquationAlpha = proxy.getParameter("BLEND_EQUATION_ALPHA");
+    s.blendSrcRgb = proxy.getParameter("BLEND_SRC_RGB");
+    s.blendSrcAlpha = proxy.getParameter("BLEND_SRC_ALPHA");
+    s.blendDstRgb = proxy.getParameter("BLEND_DST_RGB");
+    s.blendDstAlpha = proxy.getParameter("BLEND_DST_ALPHA");
+  },
+
+  /**
    * Adds a program to the cache.
    *
    * @param WebGLProgram program
    *        The shader for which the traits are to be cached.
    * @param number traits
    *        A default properties mask set for the program.
    */
   addProgram: function(program, traits) {
@@ -942,34 +1109,96 @@ WebGLCache.prototype = {
  */
 function WebGLProxy(id, context, cache, observer) {
   this._id = id;
   this._gl = context;
   this._cache = cache;
   this._observer = observer;
 
   let exports = [
+    "isEnabled",
+    "getParameter",
     "getAttachedShaders",
     "getShaderSource",
     "getShaderOfType",
-    "compileShader"
+    "compileShader",
+    "enableHighlighting",
+    "disableHighlighting"
   ];
   exports.forEach(e => this[e] = (...args) => this._call(e, args));
 }
 
 WebGLProxy.prototype = {
   _id: 0,
   _gl: null,
   _cache: null,
   _observer: null,
 
   get ownerWindow() this._id,
   get ownerContext() this._gl,
 
   /**
+   * Test whether a WebGL capability is enabled.
+   *
+   * @param string name
+   *        The WebGL capability name, for example "BLEND".
+   * @return boolean
+   *         True if enabled, false otherwise.
+   */
+  _isEnabled: function(name) {
+    return this._gl.isEnabled(this._gl[name]);
+  },
+
+  /**
+   * Returns the value for the specified WebGL parameter name.
+   *
+   * @param string name
+   *        The WebGL parameter name, for example "BLEND_COLOR".
+   * @return any
+   *         The corresponding parameter's value.
+   */
+  _getParameter: function(name) {
+    return this._gl.getParameter(this._gl[name]);
+  },
+
+  /**
+   * Returns the renderbuffer property value for the specified WebGL parameter.
+   * If no renderbuffer binding is available, null is returned.
+   *
+   * @param string name
+   *        The WebGL parameter name, for example "BLEND_COLOR".
+   * @return any
+   *         The corresponding parameter's value.
+   */
+  _getRenderbufferParameter: function(name) {
+    if (!this._getParameter("RENDERBUFFER_BINDING")) {
+      return null;
+    }
+    let gl = this._gl;
+    return gl.getRenderbufferParameter(gl.RENDERBUFFER, gl[name]);
+  },
+
+  /**
+   * Returns the framebuffer property value for the specified WebGL parameter.
+   * If no framebuffer binding is available, null is returned.
+   *
+   * @param string name
+   *        The WebGL parameter name, for example "BLEND_COLOR".
+   * @return any
+   *         The corresponding parameter's value.
+   */
+  _getFramebufferAttachmentParameter: function(type, name) {
+    if (!this._getParameter("FRAMEBUFFER_BINDING")) {
+      return null;
+    }
+    let gl = this._gl;
+    return gl.getFramebufferAttachmentParameter(gl.RENDERBUFFER, gl[type], gl[name]);
+  },
+
+  /**
    * Returns the shader objects attached to a program object.
    *
    * @param WebGLProgram program
    *        The program for which to retrieve the attached shaders.
    * @return array
    *         The attached vertex and fragment shaders.
    */
   _getAttachedShaders: function(program) {
@@ -1042,16 +1271,58 @@ WebGLProxy.prototype = {
 
     this._cache.updateAttributesForProgram(program);
     this._cache.updateUniformsForProgram(program);
 
     return error;
   },
 
   /**
+   * Enables color blending based on the geometry highlight tint.
+   */
+  _enableHighlighting: function() {
+    let gl = this._gl;
+
+    // Avoid changing the blending params when rendering to a depth texture.
+    let format = this._getRenderbufferParameter("RENDERBUFFER_INTERNAL_FORMAT");
+    if (format == gl.DEPTH_COMPONENT16) {
+      return;
+    }
+
+    // Non-premultiplied alpha blending based on a predefined constant color.
+    // Simply using gl.colorMask won't work, because we want non-tinted colors
+    // to be drawn as black, not ignored.
+    gl.enable(gl.BLEND);
+    gl.blendColor.apply(gl, this.highlightTint);
+    gl.blendEquation(gl.FUNC_ADD);
+    gl.blendFunc(gl.CONSTANT_COLOR, gl.ONE_MINUS_SRC_ALPHA, gl.CONSTANT_COLOR, gl.ZERO);
+    this.wasHighlighting = true;
+  },
+
+  /**
+   * Disables color blending based on the geometry highlight tint, by
+   * reverting the corresponding params back to their original values.
+   */
+  _disableHighlighting: function() {
+    let gl = this._gl;
+    let s = this._cache.currentState;
+
+    gl[s[gl.BLEND] ? "enable" : "disable"](gl.BLEND);
+    gl.blendColor.apply(gl, s.blendColor);
+    gl.blendEquationSeparate(s.blendEquationRgb, s.blendEquationAlpha);
+    gl.blendFuncSeparate(s.blendSrcRgb, s.blendDstRgb, s.blendSrcAlpha, s.blendDstAlpha);
+  },
+
+  /**
+   * The color tint used for highlighting geometry.
+   * @see _enableHighlighting and _disableHighlighting.
+   */
+  highlightTint: [0, 0, 0, 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.
    * @param array args