Bug 1604664 - WR force program rebinding after blend changes r=gw
authorDzmitry Malyshau <dmalyshau@mozilla.com>
Wed, 18 Dec 2019 23:26:39 +0000
changeset 507661 8b96d1241d111eb9d94446f89df812f81b6bd08d
parent 507660 ad7e08d4884143b7ec9d75552a5c1d479969d456
child 507662 f245cb8f26c3c058e2bae7cfad2c1e30b5680783
push id36931
push useropoprus@mozilla.com
push dateThu, 19 Dec 2019 09:50:06 +0000
treeherdermozilla-central@5e8b48c8cd93 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgw
bugs1604664
milestone73.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 1604664 - WR force program rebinding after blend changes r=gw Differential Revision: https://phabricator.services.mozilla.com/D57633
gfx/wr/webrender/src/device/gl.rs
gfx/wr/webrender/src/renderer.rs
gfx/wr/webrender/src/shade.rs
--- a/gfx/wr/webrender/src/device/gl.rs
+++ b/gfx/wr/webrender/src/device/gl.rs
@@ -1049,16 +1049,31 @@ pub struct Device {
 
     // GL extensions
     extensions: Vec<String>,
 
     /// Dumps the source of the shader with the given name
     dump_shader_source: Option<String>,
 
     surface_origin_is_top_left: bool,
+
+    /// A debug boolean for tracking if the shader program has been set after
+    /// a blend mode change.
+    ///
+    /// This is needed for compatibility with next-gen
+    /// GPU APIs that switch states using "pipeline object" that bundles
+    /// together the blending state with the shader.
+    ///
+    /// Having the constraint of always binding the shader last would allow
+    /// us to have the "pipeline object" bound at that time. Without this
+    /// constraint, we'd either have to eagerly bind the "pipeline object"
+    /// on changing either the shader or the blend more, or lazily bind it
+    /// at draw call time, neither of which is desirable.
+    #[cfg(debug_assertions)]
+    shader_is_ready: bool,
 }
 
 /// Contains the parameters necessary to bind a draw target.
 #[derive(Clone, Copy, Debug)]
 pub enum DrawTarget {
     /// Use the device's default draw target, with the provided dimensions,
     /// which are used to set the viewport.
     Default {
@@ -1492,16 +1507,19 @@ impl Device {
             cached_programs,
             frame_id: GpuFrameId(0),
             extensions,
             texture_storage_usage,
             requires_null_terminated_shader_source,
             optimal_pbo_stride,
             dump_shader_source,
             surface_origin_is_top_left,
+
+            #[cfg(debug_assertions)]
+            shader_is_ready: false,
         }
     }
 
     pub fn gl(&self) -> &dyn gl::Gl {
         &*self.gl
     }
 
     pub fn rc_gl(&self) -> &Rc<dyn gl::Gl> {
@@ -1614,16 +1632,20 @@ impl Device {
             }
             Ok(id)
         }
     }
 
     pub fn begin_frame(&mut self) -> GpuFrameId {
         debug_assert!(!self.inside_frame);
         self.inside_frame = true;
+        #[cfg(debug_assertions)]
+        {
+            self.shader_is_ready = false;
+        }
 
         // If our profiler state has changed, apply or remove the profiling
         // wrapper from our GL context.
         let being_profiled = profiler::thread_is_being_profiled();
         let using_wrapper = self.base_gl.is_some();
         if being_profiled && !using_wrapper {
             fn note(name: &str, duration: Duration) {
                 profiler::add_text_marker(cstr!("OpenGL Calls"), name, duration);
@@ -1991,16 +2013,20 @@ impl Device {
         program.u_mode = self.gl.get_uniform_location(program.id, "uMode");
 
         Ok(())
     }
 
     pub fn bind_program(&mut self, program: &Program) {
         debug_assert!(self.inside_frame);
         debug_assert!(program.is_initialized());
+        #[cfg(debug_assertions)]
+        {
+            self.shader_is_ready = true;
+        }
 
         if self.bound_program != program.id {
             self.gl.use_program(program.id);
             self.bound_program = program.id;
             self.program_mode_id = UniformLocation(program.u_mode);
         }
     }
 
@@ -2601,22 +2627,28 @@ impl Device {
     }
 
     pub fn set_uniforms(
         &self,
         program: &Program,
         transform: &Transform3D<f32>,
     ) {
         debug_assert!(self.inside_frame);
+        #[cfg(debug_assertions)]
+        debug_assert!(self.shader_is_ready);
+
         self.gl
             .uniform_matrix_4fv(program.u_transform, false, &transform.to_row_major_array());
     }
 
     pub fn switch_mode(&self, mode: i32) {
         debug_assert!(self.inside_frame);
+        #[cfg(debug_assertions)]
+        debug_assert!(self.shader_is_ready);
+
         self.gl.uniform_1i(self.program_mode_id.0, mode);
     }
 
     pub fn create_pbo(&mut self) -> PBO {
         let id = self.gl.gen_buffers(1)[0];
         PBO {
             id,
             reserved_size: 0,
@@ -3144,46 +3176,61 @@ impl Device {
             gl::ELEMENT_ARRAY_BUFFER,
             indices,
             usage_hint.to_gl(),
         );
     }
 
     pub fn draw_triangles_u16(&mut self, first_vertex: i32, index_count: i32) {
         debug_assert!(self.inside_frame);
+        #[cfg(debug_assertions)]
+        debug_assert!(self.shader_is_ready);
+
         self.gl.draw_elements(
             gl::TRIANGLES,
             index_count,
             gl::UNSIGNED_SHORT,
             first_vertex as u32 * 2,
         );
     }
 
     pub fn draw_triangles_u32(&mut self, first_vertex: i32, index_count: i32) {
         debug_assert!(self.inside_frame);
+        #[cfg(debug_assertions)]
+        debug_assert!(self.shader_is_ready);
+
         self.gl.draw_elements(
             gl::TRIANGLES,
             index_count,
             gl::UNSIGNED_INT,
             first_vertex as u32 * 4,
         );
     }
 
     pub fn draw_nonindexed_points(&mut self, first_vertex: i32, vertex_count: i32) {
         debug_assert!(self.inside_frame);
+        #[cfg(debug_assertions)]
+        debug_assert!(self.shader_is_ready);
+
         self.gl.draw_arrays(gl::POINTS, first_vertex, vertex_count);
     }
 
     pub fn draw_nonindexed_lines(&mut self, first_vertex: i32, vertex_count: i32) {
         debug_assert!(self.inside_frame);
+        #[cfg(debug_assertions)]
+        debug_assert!(self.shader_is_ready);
+
         self.gl.draw_arrays(gl::LINES, first_vertex, vertex_count);
     }
 
     pub fn draw_indexed_triangles_instanced_u16(&mut self, index_count: i32, instance_count: i32) {
         debug_assert!(self.inside_frame);
+        #[cfg(debug_assertions)]
+        debug_assert!(self.shader_is_ready);
+
         self.gl.draw_elements_instanced(
             gl::TRIANGLES,
             index_count,
             gl::UNSIGNED_SHORT,
             0,
             instance_count,
         );
     }
@@ -3300,92 +3347,142 @@ impl Device {
     pub fn enable_scissor(&self) {
         self.gl.enable(gl::SCISSOR_TEST);
     }
 
     pub fn disable_scissor(&self) {
         self.gl.disable(gl::SCISSOR_TEST);
     }
 
-    pub fn set_blend(&self, enable: bool) {
+    pub fn set_blend(&mut self, enable: bool) {
         if enable {
             self.gl.enable(gl::BLEND);
         } else {
             self.gl.disable(gl::BLEND);
         }
+        #[cfg(debug_assertions)]
+        {
+            self.shader_is_ready = false;
+        }
     }
 
-    pub fn set_blend_mode_alpha(&self) {
-        self.gl.blend_func_separate(gl::SRC_ALPHA, gl::ONE_MINUS_SRC_ALPHA,
-                                    gl::ONE, gl::ONE);
+    fn set_blend_factors(
+        &mut self,
+        color: (gl::GLenum, gl::GLenum),
+        alpha: (gl::GLenum, gl::GLenum),
+    ) {
         self.gl.blend_equation(gl::FUNC_ADD);
+        if color == alpha {
+            self.gl.blend_func(color.0, color.1);
+        } else {
+            self.gl.blend_func_separate(color.0, color.1, alpha.0, alpha.1);
+        }
+        #[cfg(debug_assertions)]
+        {
+            self.shader_is_ready = false;
+        }
     }
 
-    pub fn set_blend_mode_premultiplied_alpha(&self) {
-        self.gl.blend_func(gl::ONE, gl::ONE_MINUS_SRC_ALPHA);
-        self.gl.blend_equation(gl::FUNC_ADD);
+    pub fn set_blend_mode_alpha(&mut self) {
+        self.set_blend_factors(
+            (gl::SRC_ALPHA, gl::ONE_MINUS_SRC_ALPHA),
+            (gl::ONE, gl::ONE),
+        );
+    }
+
+    pub fn set_blend_mode_premultiplied_alpha(&mut self) {
+        self.set_blend_factors(
+            (gl::ONE, gl::ONE_MINUS_SRC_ALPHA),
+            (gl::ONE, gl::ONE_MINUS_SRC_ALPHA),
+        );
+    }
+
+    pub fn set_blend_mode_premultiplied_dest_out(&mut self) {
+        self.set_blend_factors(
+            (gl::ZERO, gl::ONE_MINUS_SRC_ALPHA),
+            (gl::ZERO, gl::ONE_MINUS_SRC_ALPHA),
+        );
+    }
+
+    pub fn set_blend_mode_multiply(&mut self) {
+        self.set_blend_factors(
+            (gl::ZERO, gl::SRC_COLOR),
+            (gl::ZERO, gl::SRC_ALPHA),
+        );
     }
-
-    pub fn set_blend_mode_premultiplied_dest_out(&self) {
-        self.gl.blend_func(gl::ZERO, gl::ONE_MINUS_SRC_ALPHA);
-        self.gl.blend_equation(gl::FUNC_ADD);
+    pub fn set_blend_mode_subpixel_pass0(&mut self) {
+        self.set_blend_factors(
+            (gl::ZERO, gl::ONE_MINUS_SRC_COLOR),
+            (gl::ZERO, gl::ONE_MINUS_SRC_ALPHA),
+        );
+    }
+    pub fn set_blend_mode_subpixel_pass1(&mut self) {
+        self.set_blend_factors(
+            (gl::ONE, gl::ONE),
+            (gl::ONE, gl::ONE),
+        );
+    }
+    pub fn set_blend_mode_subpixel_with_bg_color_pass0(&mut self) {
+        self.set_blend_factors(
+            (gl::ZERO, gl::ONE_MINUS_SRC_COLOR),
+            (gl::ZERO, gl::ONE),
+        );
+    }
+    pub fn set_blend_mode_subpixel_with_bg_color_pass1(&mut self) {
+        self.set_blend_factors(
+            (gl::ONE_MINUS_DST_ALPHA, gl::ONE),
+            (gl::ZERO, gl::ONE),
+        );
     }
-
-    pub fn set_blend_mode_multiply(&self) {
-        self.gl
-            .blend_func_separate(gl::ZERO, gl::SRC_COLOR, gl::ZERO, gl::SRC_ALPHA);
-        self.gl.blend_equation(gl::FUNC_ADD);
+    pub fn set_blend_mode_subpixel_with_bg_color_pass2(&mut self) {
+        self.set_blend_factors(
+            (gl::ONE, gl::ONE),
+            (gl::ONE, gl::ONE_MINUS_SRC_ALPHA),
+        );
+    }
+    pub fn set_blend_mode_subpixel_constant_text_color(&mut self, color: ColorF) {
+        // color is an unpremultiplied color.
+        self.gl.blend_color(color.r, color.g, color.b, 1.0);
+        self.set_blend_factors(
+            (gl::CONSTANT_COLOR, gl::ONE_MINUS_SRC_COLOR),
+            (gl::CONSTANT_ALPHA, gl::ONE_MINUS_SRC_ALPHA),
+        );
     }
-    pub fn set_blend_mode_max(&self) {
+    pub fn set_blend_mode_subpixel_dual_source(&mut self) {
+        self.set_blend_factors(
+            (gl::ONE, gl::ONE_MINUS_SRC1_COLOR),
+            (gl::ONE, gl::ONE_MINUS_SRC1_ALPHA),
+        );
+    }
+    pub fn set_blend_mode_show_overdraw(&mut self) {
+        self.set_blend_factors(
+            (gl::ONE, gl::ONE_MINUS_SRC_ALPHA),
+            (gl::ONE, gl::ONE_MINUS_SRC_ALPHA),
+        );
+    }
+
+    pub fn set_blend_mode_max(&mut self) {
         self.gl
             .blend_func_separate(gl::ONE, gl::ONE, gl::ONE, gl::ONE);
         self.gl.blend_equation_separate(gl::MAX, gl::FUNC_ADD);
+        #[cfg(debug_assertions)]
+        {
+            self.shader_is_ready = false;
+        }
     }
-    pub fn set_blend_mode_min(&self) {
+    pub fn set_blend_mode_min(&mut self) {
         self.gl
             .blend_func_separate(gl::ONE, gl::ONE, gl::ONE, gl::ONE);
         self.gl.blend_equation_separate(gl::MIN, gl::FUNC_ADD);
-    }
-    pub fn set_blend_mode_subpixel_pass0(&self) {
-        self.gl.blend_func(gl::ZERO, gl::ONE_MINUS_SRC_COLOR);
-        self.gl.blend_equation(gl::FUNC_ADD);
-    }
-    pub fn set_blend_mode_subpixel_pass1(&self) {
-        self.gl.blend_func(gl::ONE, gl::ONE);
-        self.gl.blend_equation(gl::FUNC_ADD);
-    }
-    pub fn set_blend_mode_subpixel_with_bg_color_pass0(&self) {
-        self.gl.blend_func_separate(gl::ZERO, gl::ONE_MINUS_SRC_COLOR, gl::ZERO, gl::ONE);
-        self.gl.blend_equation(gl::FUNC_ADD);
-    }
-    pub fn set_blend_mode_subpixel_with_bg_color_pass1(&self) {
-        self.gl.blend_func_separate(gl::ONE_MINUS_DST_ALPHA, gl::ONE, gl::ZERO, gl::ONE);
-        self.gl.blend_equation(gl::FUNC_ADD);
+        #[cfg(debug_assertions)]
+        {
+            self.shader_is_ready = false;
+        }
     }
-    pub fn set_blend_mode_subpixel_with_bg_color_pass2(&self) {
-        self.gl.blend_func_separate(gl::ONE, gl::ONE, gl::ONE, gl::ONE_MINUS_SRC_ALPHA);
-        self.gl.blend_equation(gl::FUNC_ADD);
-    }
-    pub fn set_blend_mode_subpixel_constant_text_color(&self, color: ColorF) {
-        // color is an unpremultiplied color.
-        self.gl.blend_color(color.r, color.g, color.b, 1.0);
-        self.gl
-            .blend_func(gl::CONSTANT_COLOR, gl::ONE_MINUS_SRC_COLOR);
-        self.gl.blend_equation(gl::FUNC_ADD);
-    }
-    pub fn set_blend_mode_subpixel_dual_source(&self) {
-        self.gl.blend_func(gl::ONE, gl::ONE_MINUS_SRC1_COLOR);
-        self.gl.blend_equation(gl::FUNC_ADD);
-    }
-    pub fn set_blend_mode_show_overdraw(&self) {
-        self.gl.blend_func(gl::ONE, gl::ONE_MINUS_SRC_ALPHA);
-        self.gl.blend_equation(gl::FUNC_ADD);
-    }
-
-    pub fn set_blend_mode_advanced(&self, mode: MixBlendMode) {
+    pub fn set_blend_mode_advanced(&mut self, mode: MixBlendMode) {
         self.gl.blend_equation(match mode {
             MixBlendMode::Normal => {
                 // blend factor only make sense for the normal mode
                 self.gl.blend_func_separate(gl::ZERO, gl::SRC_COLOR, gl::ZERO, gl::SRC_ALPHA);
                 gl::FUNC_ADD
             },
             MixBlendMode::Multiply => gl::MULTIPLY_KHR,
             MixBlendMode::Screen => gl::SCREEN_KHR,
@@ -3398,16 +3495,20 @@ impl Device {
             MixBlendMode::SoftLight => gl::SOFTLIGHT_KHR,
             MixBlendMode::Difference => gl::DIFFERENCE_KHR,
             MixBlendMode::Exclusion => gl::EXCLUSION_KHR,
             MixBlendMode::Hue => gl::HSL_HUE_KHR,
             MixBlendMode::Saturation => gl::HSL_SATURATION_KHR,
             MixBlendMode::Color => gl::HSL_COLOR_KHR,
             MixBlendMode::Luminosity => gl::HSL_LUMINOSITY_KHR,
         });
+        #[cfg(debug_assertions)]
+        {
+            self.shader_is_ready = false;
+        }
     }
 
     pub fn supports_extension(&self, extension: &str) -> bool {
         supports_extension(&self.extensions, extension)
     }
 
     /// Enable the pixel local storage functionality. Caller must
     /// have already confirmed the device supports this.
--- a/gfx/wr/webrender/src/renderer.rs
+++ b/gfx/wr/webrender/src/renderer.rs
@@ -3954,17 +3954,19 @@ impl Renderer {
             self.gpu_profile.finish_sampler(opaque_sampler);
         }
 
         if !alpha_batch_container.alpha_batches.is_empty()
             && !self.debug_flags.contains(DebugFlags::DISABLE_ALPHA_PASS) {
             let _gl = self.gpu_profile.start_marker("alpha batches");
             let transparent_sampler = self.gpu_profile.start_sampler(GPU_SAMPLER_TAG_TRANSPARENT);
             self.set_blend(true, framebuffer_kind);
+
             let mut prev_blend_mode = BlendMode::None;
+            let shaders_rc = self.shaders.clone();
 
             // If the device supports pixel local storage, initialize the PLS buffer for
             // the transparent pass. This involves reading the current framebuffer value
             // and storing that in PLS.
             // TODO(gw): This is quite expensive and relies on framebuffer fetch being
             //           available. We can probably switch the opaque pass over to use
             //           PLS too, and remove this pass completely.
             if self.device.get_capabilities().supports_pixel_local_storage {
@@ -3978,22 +3980,22 @@ impl Renderer {
                 );
             }
 
             for batch in &alpha_batch_container.alpha_batches {
                 if should_skip_batch(&batch.key.kind, &self.debug_flags) {
                     continue;
                 }
 
-                self.shaders.borrow_mut()
-                    .get(&batch.key, batch.features | BatchFeatures::ALPHA_PASS, self.debug_flags)
-                    .bind(
-                        &mut self.device, projection,
-                        &mut self.renderer_errors,
-                    );
+                let mut shaders = shaders_rc.borrow_mut();
+                let shader = shaders.get(
+                    &batch.key,
+                    batch.features | BatchFeatures::ALPHA_PASS,
+                    self.debug_flags,
+                );
 
                 if batch.key.blend_mode != prev_blend_mode {
                     match batch.key.blend_mode {
                         _ if self.debug_flags.contains(DebugFlags::SHOW_OVERDRAW) &&
                             framebuffer_kind == FramebufferKind::Main => {
                             self.device.set_blend_mode_show_overdraw();
                         }
                         BlendMode::None => {
@@ -4016,16 +4018,22 @@ impl Renderer {
                         }
                         BlendMode::SubpixelWithBgColor => {
                             // Using the three pass "component alpha with font smoothing
                             // background color" rendering technique:
                             //
                             // /webrender/doc/text-rendering.md
                             //
                             self.device.set_blend_mode_subpixel_with_bg_color_pass0();
+                            // need to make sure the shader is bound
+                            shader.bind(
+                                &mut self.device,
+                                projection,
+                                &mut self.renderer_errors,
+                            );
                             self.device.switch_mode(ShaderColorMode::SubpixelWithBgColorPass0 as _);
                         }
                         BlendMode::Advanced(mode) => {
                             if self.enable_advanced_blend_barriers {
                                 self.device.gl().blend_barrier_khr();
                             }
                             self.device.set_blend_mode_advanced(mode);
                         }
@@ -4043,36 +4051,53 @@ impl Renderer {
                         uses_scissor,
                         &render_tasks[source_id],
                         &render_tasks[task_id],
                         &render_tasks[backdrop_id],
                     );
                 }
 
                 let _timer = self.gpu_profile.start_timer(batch.key.kind.sampler_tag());
+                shader.bind(
+                    &mut self.device,
+                    projection,
+                    &mut self.renderer_errors,
+                );
 
                 self.draw_instanced_batch(
                     &batch.instances,
                     VertexArrayKind::Primitive,
                     &batch.key.textures,
                     stats
                 );
 
                 if batch.key.blend_mode == BlendMode::SubpixelWithBgColor {
                     self.set_blend_mode_subpixel_with_bg_color_pass1(framebuffer_kind);
+                    // re-binding the shader after the blend mode change
+                    shader.bind(
+                        &mut self.device,
+                        projection,
+                        &mut self.renderer_errors,
+                    );
                     self.device.switch_mode(ShaderColorMode::SubpixelWithBgColorPass1 as _);
 
                     // When drawing the 2nd and 3rd passes, we know that the VAO, textures etc
                     // are all set up from the previous draw_instanced_batch call,
                     // so just issue a draw call here to avoid re-uploading the
                     // instances and re-binding textures etc.
                     self.device
                         .draw_indexed_triangles_instanced_u16(6, batch.instances.len() as i32);
 
                     self.set_blend_mode_subpixel_with_bg_color_pass2(framebuffer_kind);
+                    // re-binding the shader after the blend mode change
+                    shader.bind(
+                        &mut self.device,
+                        projection,
+                        &mut self.renderer_errors,
+                    );
                     self.device.switch_mode(ShaderColorMode::SubpixelWithBgColorPass2 as _);
 
                     self.device
                         .draw_indexed_triangles_instanced_u16(6, batch.instances.len() as i32);
                 }
 
                 if batch.key.blend_mode == BlendMode::SubpixelWithBgColor {
                     prev_blend_mode = BlendMode::None;
@@ -4099,19 +4124,26 @@ impl Renderer {
             self.device.disable_scissor();
         }
     }
 
     /// Draw a list of tiles to the framebuffer
     fn draw_tile_list<'a, I: Iterator<Item = &'a CompositeTile>>(
         &mut self,
         tiles_iter: I,
+        projection: &default::Transform3D<f32>,
         partial_present_mode: Option<PartialPresentMode>,
         stats: &mut RendererStats,
     ) {
+        self.shaders.borrow_mut().composite.bind(
+            &mut self.device,
+            projection,
+            &mut self.renderer_errors
+        );
+
         let mut current_textures = BatchTextures::no_texture();
         let mut instances = Vec::new();
 
         for tile in tiles_iter {
             // Work out the draw params based on the tile surface
             let (texture, layer, color) = match tile.surface {
                 CompositeTileSurface::Color { color } => {
                     (TextureSource::Dummy, 0.0, color)
@@ -4249,63 +4281,60 @@ impl Renderer {
                     // Partial present is disabled, so clear the entire framebuffer
                     self.device.clear_target(clear_color,
                                              Some(1.0),
                                              None);
                 }
             }
         }
 
-        self.shaders.borrow_mut().composite.bind(
-            &mut self.device,
-            &projection,
-            &mut self.renderer_errors
-        );
-
         // We are only interested in tiles backed with actual cached pixels so we don't
         // count clear tiles here.
         let num_tiles = composite_state.opaque_tiles.len()
             + composite_state.alpha_tiles.len();
         self.profile_counters.total_picture_cache_tiles.set(num_tiles);
 
         // Draw opaque tiles first, front-to-back to get maxmum
         // z-reject efficiency.
         if !composite_state.opaque_tiles.is_empty() {
             let opaque_sampler = self.gpu_profile.start_sampler(GPU_SAMPLER_TAG_OPAQUE);
             self.device.enable_depth_write();
             self.set_blend(false, FramebufferKind::Main);
             self.draw_tile_list(
                 composite_state.opaque_tiles.iter().rev(),
+                projection,
                 partial_present_mode,
                 &mut results.stats,
             );
             self.gpu_profile.finish_sampler(opaque_sampler);
         }
 
         if !composite_state.clear_tiles.is_empty() {
             let transparent_sampler = self.gpu_profile.start_sampler(GPU_SAMPLER_TAG_TRANSPARENT);
             self.device.disable_depth_write();
             self.set_blend(true, FramebufferKind::Main);
             self.device.set_blend_mode_premultiplied_dest_out();
             self.draw_tile_list(
                 composite_state.clear_tiles.iter(),
+                projection,
                 partial_present_mode,
                 &mut results.stats,
             );
             self.gpu_profile.finish_sampler(transparent_sampler);
         }
 
         // Draw alpha tiles
         if !composite_state.alpha_tiles.is_empty() {
             let transparent_sampler = self.gpu_profile.start_sampler(GPU_SAMPLER_TAG_TRANSPARENT);
             self.device.disable_depth_write();
             self.set_blend(true, FramebufferKind::Main);
             self.set_blend_mode_premultiplied_alpha(FramebufferKind::Main);
             self.draw_tile_list(
                 composite_state.alpha_tiles.iter(),
+                projection,
                 partial_present_mode,
                 &mut results.stats,
             );
             self.gpu_profile.finish_sampler(transparent_sampler);
         }
     }
 
     fn draw_color_target(
@@ -5985,52 +6014,52 @@ impl Renderer {
         // Textures held internally within the device layer.
         report += self.device.report_memory();
 
         report
     }
 
     // Sets the blend mode. Blend is unconditionally set if the "show overdraw" debugging mode is
     // enabled.
-    fn set_blend(&self, mut blend: bool, framebuffer_kind: FramebufferKind) {
+    fn set_blend(&mut self, mut blend: bool, framebuffer_kind: FramebufferKind) {
         if framebuffer_kind == FramebufferKind::Main &&
                 self.debug_flags.contains(DebugFlags::SHOW_OVERDRAW) {
             blend = true
         }
         self.device.set_blend(blend)
     }
 
-    fn set_blend_mode_multiply(&self, framebuffer_kind: FramebufferKind) {
+    fn set_blend_mode_multiply(&mut self, framebuffer_kind: FramebufferKind) {
         if framebuffer_kind == FramebufferKind::Main &&
                 self.debug_flags.contains(DebugFlags::SHOW_OVERDRAW) {
             self.device.set_blend_mode_show_overdraw();
         } else {
             self.device.set_blend_mode_multiply();
         }
     }
 
-    fn set_blend_mode_premultiplied_alpha(&self, framebuffer_kind: FramebufferKind) {
+    fn set_blend_mode_premultiplied_alpha(&mut self, framebuffer_kind: FramebufferKind) {
         if framebuffer_kind == FramebufferKind::Main &&
                 self.debug_flags.contains(DebugFlags::SHOW_OVERDRAW) {
             self.device.set_blend_mode_show_overdraw();
         } else {
             self.device.set_blend_mode_premultiplied_alpha();
         }
     }
 
-    fn set_blend_mode_subpixel_with_bg_color_pass1(&self, framebuffer_kind: FramebufferKind) {
+    fn set_blend_mode_subpixel_with_bg_color_pass1(&mut self, framebuffer_kind: FramebufferKind) {
         if framebuffer_kind == FramebufferKind::Main &&
                 self.debug_flags.contains(DebugFlags::SHOW_OVERDRAW) {
             self.device.set_blend_mode_show_overdraw();
         } else {
             self.device.set_blend_mode_subpixel_with_bg_color_pass1();
         }
     }
 
-    fn set_blend_mode_subpixel_with_bg_color_pass2(&self, framebuffer_kind: FramebufferKind) {
+    fn set_blend_mode_subpixel_with_bg_color_pass2(&mut self, framebuffer_kind: FramebufferKind) {
         if framebuffer_kind == FramebufferKind::Main &&
                 self.debug_flags.contains(DebugFlags::SHOW_OVERDRAW) {
             self.device.set_blend_mode_show_overdraw();
         } else {
             self.device.set_blend_mode_subpixel_with_bg_color_pass2();
         }
     }
 
--- a/gfx/wr/webrender/src/shade.rs
+++ b/gfx/wr/webrender/src/shade.rs
@@ -68,31 +68,35 @@ pub(crate) enum ShaderKind {
     Resolve,
     Composite,
 }
 
 pub struct LazilyCompiledShader {
     program: Option<Program>,
     name: &'static str,
     kind: ShaderKind,
+    cached_projection: Transform3D<f32>,
     features: Vec<&'static str>,
 }
 
 impl LazilyCompiledShader {
     pub(crate) fn new(
         kind: ShaderKind,
         name: &'static str,
         features: &[&'static str],
         device: &mut Device,
         precache_flags: ShaderPrecacheFlags,
     ) -> Result<Self, ShaderError> {
         let mut shader = LazilyCompiledShader {
             program: None,
             name,
             kind,
+            //Note: this isn't really the default state, but there is no chance
+            // an actual projection passed here would accidentally match.
+            cached_projection: Transform3D::identity(),
             features: features.to_vec(),
         };
 
         if precache_flags.intersects(ShaderPrecacheFlags::ASYNC_COMPILE | ShaderPrecacheFlags::FULL_COMPILE) {
             let t0 = precise_time_ns();
             shader.get_internal(device, precache_flags)?;
             let t1 = precise_time_ns();
             debug!("[C: {:.1} ms ] Precache {} {:?}",
@@ -106,25 +110,30 @@ impl LazilyCompiledShader {
     }
 
     pub fn bind(
         &mut self,
         device: &mut Device,
         projection: &Transform3D<f32>,
         renderer_errors: &mut Vec<RendererError>,
     ) {
-        let program = match self.get(device) {
+        let update_projection = self.cached_projection != *projection;
+        let program = match self.get_internal(device, ShaderPrecacheFlags::FULL_COMPILE) {
             Ok(program) => program,
             Err(e) => {
                 renderer_errors.push(RendererError::from(e));
                 return;
             }
         };
         device.bind_program(program);
-        device.set_uniforms(program, projection);
+        if update_projection {
+            device.set_uniforms(program, projection);
+            // thanks NLL for this (`program` technically borrows `self`)
+            self.cached_projection = *projection;
+        }
     }
 
     fn get_internal(
         &mut self,
         device: &mut Device,
         precache_flags: ShaderPrecacheFlags,
     ) -> Result<&mut Program, ShaderError> {
         if self.program.is_none() {
@@ -240,20 +249,16 @@ impl LazilyCompiledShader {
                     );
                 }
             }
         }
 
         Ok(program)
     }
 
-    fn get(&mut self, device: &mut Device) -> Result<&mut Program, ShaderError> {
-        self.get_internal(device, ShaderPrecacheFlags::FULL_COMPILE)
-    }
-
     fn deinit(self, device: &mut Device) {
         if let Some(program) = self.program {
             device.delete_program(program);
         }
     }
 }
 
 // A brush shader supports two modes: