Bug 1510030 - Implement WebRender backend to run background color animations on the compositor. r=gw,boris
authorHiroyuki Ikezoe <hikezoe.birchill@mozilla.com>
Thu, 27 Feb 2020 08:43:06 +0000
changeset 515862 9092c93a4ac6621e8fb255ae5b386046a0524c0a
parent 515861 42d17390bcca6e518624bddb755433277dd64ba4
child 515863 f6ff71a9517a9331e0b0253833f7ed94eddfaab2
push id108472
push userhikezoe.birchill@mozilla.com
push dateThu, 27 Feb 2020 08:44:56 +0000
treeherderautoland@0d3fcf6f3aaa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgw, boris
bugs1510030
milestone75.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 1510030 - Implement WebRender backend to run background color animations on the compositor. r=gw,boris Differential Revision: https://phabricator.services.mozilla.com/D63604
gfx/layers/AnimationHelper.cpp
gfx/layers/wr/WebRenderBridgeParent.cpp
gfx/layers/wr/WebRenderBridgeParent.h
gfx/webrender_bindings/WebRenderAPI.cpp
gfx/webrender_bindings/WebRenderAPI.h
gfx/webrender_bindings/WebRenderTypes.h
gfx/webrender_bindings/src/bindings.rs
gfx/wr/examples/animation.rs
gfx/wr/webrender/src/box_shadow.rs
gfx/wr/webrender/src/picture.rs
gfx/wr/webrender/src/prim_store/mod.rs
gfx/wr/webrender/src/scene.rs
gfx/wr/webrender/src/scene_building.rs
gfx/wr/webrender_api/src/api.rs
gfx/wr/webrender_api/src/display_item.rs
gfx/wr/webrender_api/src/display_list.rs
gfx/wr/wrench/src/scene.rs
gfx/wr/wrench/src/yaml_frame_writer.rs
layout/painting/nsDisplayList.cpp
--- a/gfx/layers/AnimationHelper.cpp
+++ b/gfx/layers/AnimationHelper.cpp
@@ -610,16 +610,22 @@ bool AnimationHelper::SampleAnimations(C
       continue;
     }
 
     const PropertyAnimationGroup& lastPropertyAnimationGroup =
         animationStorageData.mAnimation.LastElement();
 
     // Store the AnimatedValue
     switch (lastPropertyAnimationGroup.mProperty) {
+      case eCSSProperty_background_color: {
+        aStorage->SetAnimatedValue(
+            iter.Key(), Servo_AnimationValue_GetColor(animationValues[0],
+                                                      NS_RGBA(0, 0, 0, 0)));
+        break;
+      }
       case eCSSProperty_opacity: {
         MOZ_ASSERT(animationValues.Length() == 1);
         aStorage->SetAnimatedValue(
             iter.Key(), Servo_AnimationValue_GetOpacity(animationValues[0]));
         break;
       }
       case eCSSProperty_rotate:
       case eCSSProperty_scale:
--- a/gfx/layers/wr/WebRenderBridgeParent.cpp
+++ b/gfx/layers/wr/WebRenderBridgeParent.cpp
@@ -2274,33 +2274,38 @@ bool WebRenderBridgeParent::AdvanceAnima
   // started animations.
   mPreviousFrameTimeStamp = isAnimating ? lastComposeTime : TimeStamp();
 
   return isAnimating;
 }
 
 bool WebRenderBridgeParent::SampleAnimations(
     wr::RenderRootArray<nsTArray<wr::WrOpacityProperty>>& aOpacityArrays,
-    wr::RenderRootArray<nsTArray<wr::WrTransformProperty>>& aTransformArrays) {
+    wr::RenderRootArray<nsTArray<wr::WrTransformProperty>>& aTransformArrays,
+    wr::RenderRootArray<nsTArray<wr::WrColorProperty>>& aColorArrays) {
   const bool isAnimating = AdvanceAnimations();
 
   // return the animated data if has
   if (mAnimStorage->AnimatedValueCount()) {
     for (auto iter = mAnimStorage->ConstAnimatedValueTableIter(); !iter.Done();
          iter.Next()) {
       AnimatedValue* value = iter.UserData();
       wr::RenderRoot renderRoot = mAnimStorage->AnimationRenderRoot(iter.Key());
       auto& transformArray = aTransformArrays[renderRoot];
       auto& opacityArray = aOpacityArrays[renderRoot];
+      auto& colorArray = aColorArrays[renderRoot];
       if (value->Is<AnimationTransform>()) {
         transformArray.AppendElement(wr::ToWrTransformProperty(
             iter.Key(), value->Transform().mTransformInDevSpace));
       } else if (value->Is<float>()) {
         opacityArray.AppendElement(
             wr::ToWrOpacityProperty(iter.Key(), value->Opacity()));
+      } else if (value->Is<nscolor>()) {
+        colorArray.AppendElement(wr::ToWrColorProperty(
+            iter.Key(), gfx::Color::FromABGR(value->Color())));
       }
     }
   }
 
   return isAnimating;
 }
 
 void WebRenderBridgeParent::CompositeIfNeeded() {
@@ -2420,31 +2425,33 @@ void WebRenderBridgeParent::MaybeGenerat
   if (framesGenerated == 0) {
     // Could skip generating frame now.
     mPreviousFrameTimeStamp = TimeStamp();
     return;
   }
 
   wr::RenderRootArray<nsTArray<wr::WrOpacityProperty>> opacityArrays;
   wr::RenderRootArray<nsTArray<wr::WrTransformProperty>> transformArrays;
-
-  if (SampleAnimations(opacityArrays, transformArrays)) {
+  wr::RenderRootArray<nsTArray<wr::WrColorProperty>> colorArrays;
+
+  if (SampleAnimations(opacityArrays, transformArrays, colorArrays)) {
     // TODO we should have a better way of assessing whether we need a content
     // or a chrome frame generation.
     ScheduleGenerateFrameAllRenderRoots();
   }
   // We do this even if the arrays are empty, because it will clear out any
   // previous properties store on the WR side, which is desirable.
   for (auto& api : mApis) {
     if (!api) {
       continue;
     }
     auto renderRoot = api->GetRenderRoot();
     fastTxns[renderRoot]->UpdateDynamicProperties(opacityArrays[renderRoot],
-                                                  transformArrays[renderRoot]);
+                                                  transformArrays[renderRoot],
+                                                  colorArrays[renderRoot]);
   }
 
   SetAPZSampleTime();
 
   wr::RenderThread::Get()->IncPendingFrameCount(
       mApis[wr::RenderRoot::Default]->GetId(), aId, start, framesGenerated);
 
 #if defined(ENABLE_FRAME_LATENCY_LOG)
--- a/gfx/layers/wr/WebRenderBridgeParent.h
+++ b/gfx/layers/wr/WebRenderBridgeParent.h
@@ -422,17 +422,18 @@ class WebRenderBridgeParent final
   bool ShouldParentObserveEpoch();
   mozilla::ipc::IPCResult HandleShutdown();
 
   // Returns true if there is any animation (including animations in delay
   // phase).
   bool AdvanceAnimations();
   bool SampleAnimations(
       wr::RenderRootArray<nsTArray<wr::WrOpacityProperty>>& aOpacityArrays,
-      wr::RenderRootArray<nsTArray<wr::WrTransformProperty>>& aTransformArrays);
+      wr::RenderRootArray<nsTArray<wr::WrTransformProperty>>& aTransformArrays,
+      wr::RenderRootArray<nsTArray<wr::WrColorProperty>>& aColorArrays);
 
   CompositorBridgeParent* GetRootCompositorBridgeParent() const;
 
   RefPtr<WebRenderBridgeParent> GetRootWebRenderBridgeParent() const;
 
   // Tell APZ what the subsequent sampling's timestamp should be.
   void SetAPZSampleTime();
 
--- a/gfx/webrender_bindings/WebRenderAPI.cpp
+++ b/gfx/webrender_bindings/WebRenderAPI.cpp
@@ -211,22 +211,25 @@ void TransactionBuilder::GenerateFrame()
 }
 
 void TransactionBuilder::InvalidateRenderedFrame() {
   wr_transaction_invalidate_rendered_frame(mTxn);
 }
 
 void TransactionBuilder::UpdateDynamicProperties(
     const nsTArray<wr::WrOpacityProperty>& aOpacityArray,
-    const nsTArray<wr::WrTransformProperty>& aTransformArray) {
+    const nsTArray<wr::WrTransformProperty>& aTransformArray,
+    const nsTArray<wr::WrColorProperty>& aColorArray) {
   wr_transaction_update_dynamic_properties(
       mTxn, aOpacityArray.IsEmpty() ? nullptr : aOpacityArray.Elements(),
       aOpacityArray.Length(),
       aTransformArray.IsEmpty() ? nullptr : aTransformArray.Elements(),
-      aTransformArray.Length());
+      aTransformArray.Length(),
+      aColorArray.IsEmpty() ? nullptr : aColorArray.Elements(),
+      aColorArray.Length());
 }
 
 bool TransactionBuilder::IsEmpty() const {
   return wr_transaction_is_empty(mTxn);
 }
 
 bool TransactionBuilder::IsResourceUpdatesEmpty() const {
   return wr_transaction_resource_updates_is_empty(mTxn);
@@ -1134,16 +1137,30 @@ void DisplayListBuilder::PushHitTest(con
                                      bool aIsBackfaceVisible) {
   wr::LayoutRect clip = MergeClipLeaf(aClip);
   WRDL_LOG("PushHitTest b=%s cl=%s\n", mWrState, Stringify(aBounds).c_str(),
            Stringify(clip).c_str());
   wr_dp_push_hit_test(mWrState, aBounds, clip, aIsBackfaceVisible,
                       &mCurrentSpaceAndClipChain);
 }
 
+void DisplayListBuilder::PushRectWithAnimation(
+    const wr::LayoutRect& aBounds, const wr::LayoutRect& aClip,
+    bool aIsBackfaceVisible, const wr::ColorF& aColor,
+    const WrAnimationProperty* aAnimation) {
+  wr::LayoutRect clip = MergeClipLeaf(aClip);
+  WRDL_LOG("PushRectWithAnimation b=%s cl=%s c=%s\n", mWrState,
+           Stringify(aBounds).c_str(), Stringify(clip).c_str(),
+           Stringify(aColor).c_str());
+
+  wr_dp_push_rect_with_animation(mWrState, aBounds, clip, aIsBackfaceVisible,
+                                 &mCurrentSpaceAndClipChain, aColor,
+                                 aAnimation);
+}
+
 void DisplayListBuilder::PushClearRect(const wr::LayoutRect& aBounds) {
   wr::LayoutRect clip = MergeClipLeaf(aBounds);
   WRDL_LOG("PushClearRect b=%s c=%s\n", mWrState, Stringify(aBounds).c_str(),
            Stringify(clip).c_str());
   wr_dp_push_clear_rect(mWrState, aBounds, clip, &mCurrentSpaceAndClipChain);
 }
 
 void DisplayListBuilder::PushClearRectWithComplexRegion(
--- a/gfx/webrender_bindings/WebRenderAPI.h
+++ b/gfx/webrender_bindings/WebRenderAPI.h
@@ -101,17 +101,18 @@ class TransactionBuilder final {
   void ClearDisplayList(Epoch aEpoch, wr::WrPipelineId aPipeline);
 
   void GenerateFrame();
 
   void InvalidateRenderedFrame();
 
   void UpdateDynamicProperties(
       const nsTArray<wr::WrOpacityProperty>& aOpacityArray,
-      const nsTArray<wr::WrTransformProperty>& aTransformArray);
+      const nsTArray<wr::WrTransformProperty>& aTransformArray,
+      const nsTArray<wr::WrColorProperty>& aColorArray);
 
   void SetDocumentView(const LayoutDeviceIntRect& aDocRect);
 
   void UpdateScrollPosition(
       const wr::WrPipelineId& aPipelineId,
       const layers::ScrollableLayerGuid::ViewID& aScrollId,
       const wr::LayoutPoint& aScrollPosition);
 
@@ -457,16 +458,20 @@ class DisplayListBuilder final {
   wr::WrSpaceAndClip DefineScrollLayer(
       const layers::ScrollableLayerGuid::ViewID& aViewId,
       const Maybe<wr::WrSpaceAndClip>& aParent,
       const wr::LayoutRect& aContentRect, const wr::LayoutRect& aClipRect,
       const wr::LayoutPoint& aScrollOffset);
 
   void PushRect(const wr::LayoutRect& aBounds, const wr::LayoutRect& aClip,
                 bool aIsBackfaceVisible, const wr::ColorF& aColor);
+  void PushRectWithAnimation(const wr::LayoutRect& aBounds,
+                             const wr::LayoutRect& aClip,
+                             bool aIsBackfaceVisible, const wr::ColorF& aColor,
+                             const WrAnimationProperty* aAnimation);
   void PushRoundedRect(const wr::LayoutRect& aBounds,
                        const wr::LayoutRect& aClip, bool aIsBackfaceVisible,
                        const wr::ColorF& aColor);
   void PushHitTest(const wr::LayoutRect& aBounds, const wr::LayoutRect& aClip,
                    bool aIsBackfaceVisible);
   void PushClearRect(const wr::LayoutRect& aBounds);
   void PushClearRectWithComplexRegion(const wr::LayoutRect& aBounds,
                                       const wr::ComplexClipRegion& aRegion);
--- a/gfx/webrender_bindings/WebRenderTypes.h
+++ b/gfx/webrender_bindings/WebRenderTypes.h
@@ -639,16 +639,24 @@ static inline wr::WrTransformProperty To
 static inline wr::WrOpacityProperty ToWrOpacityProperty(uint64_t id,
                                                         const float opacity) {
   wr::WrOpacityProperty prop;
   prop.id = id;
   prop.opacity = opacity;
   return prop;
 }
 
+static inline wr::WrColorProperty ToWrColorProperty(uint64_t id,
+                                                    const gfx::Color& color) {
+  wr::WrColorProperty prop;
+  prop.id = id;
+  prop.color = ToColorF(color);
+  return prop;
+}
+
 // Whenever possible, use wr::ExternalImageId instead of manipulating uint64_t.
 inline uint64_t AsUint64(const ExternalImageId& aId) {
   return static_cast<uint64_t>(aId._0);
 }
 
 static inline ExternalImageId ToExternalImageId(uint64_t aID) {
   ExternalImageId Id;
   Id._0 = aID;
--- a/gfx/webrender_bindings/src/bindings.rs
+++ b/gfx/webrender_bindings/src/bindings.rs
@@ -428,19 +428,21 @@ pub struct WrFilterData {
     B_values: *mut c_float,
     B_values_count: usize,
     funcA_type: ComponentTransferFuncType,
     A_values: *mut c_float,
     A_values_count: usize,
 }
 
 #[repr(u32)]
+#[derive(Debug)]
 pub enum WrAnimationType {
     Transform = 0,
     Opacity = 1,
+    BackgroundColor = 2,
 }
 
 #[repr(C)]
 pub struct WrAnimationProperty {
     effect_type: WrAnimationType,
     id: u64,
 }
 
@@ -454,16 +456,23 @@ pub struct WrTransformProperty {
 
 #[repr(C)]
 #[derive(Copy, Clone, Debug)]
 pub struct WrOpacityProperty {
     pub id: u64,
     pub opacity: f32,
 }
 
+#[repr(C)]
+#[derive(Copy, Clone, Debug)]
+pub struct WrColorProperty {
+    pub id: u64,
+    pub color: ColorF,
+}
+
 /// cbindgen:field-names=[mHandle]
 /// cbindgen:derive-lt=true
 /// cbindgen:derive-lte=true
 #[repr(C)]
 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
 pub struct WrWindowId(u64);
 
 fn get_proc_address(glcontext_ptr: *mut c_void,
@@ -1700,20 +1709,23 @@ pub extern "C" fn wr_transaction_invalid
 
 #[no_mangle]
 pub extern "C" fn wr_transaction_update_dynamic_properties(
     txn: &mut Transaction,
     opacity_array: *const WrOpacityProperty,
     opacity_count: usize,
     transform_array: *const WrTransformProperty,
     transform_count: usize,
+    color_array: *const WrColorProperty,
+    color_count: usize,
 ) {
     let mut properties = DynamicProperties {
         transforms: Vec::with_capacity(transform_count),
         floats: Vec::with_capacity(opacity_count),
+        colors: Vec::with_capacity(color_count),
     };
 
     if transform_count > 0 {
         let transform_slice = unsafe { make_slice(transform_array, transform_count) };
 
         properties.transforms.reserve(transform_slice.len());
         for element in transform_slice.iter() {
             let prop = PropertyValue {
@@ -1733,16 +1745,29 @@ pub extern "C" fn wr_transaction_update_
             let prop = PropertyValue {
                 key: PropertyBindingKey::new(element.id),
                 value: element.opacity,
             };
             properties.floats.push(prop);
         }
     }
 
+    if color_count > 0 {
+        let color_slice = unsafe { make_slice(color_array, color_count) };
+
+        properties.colors.reserve(color_slice.len());
+        for element in color_slice.iter() {
+            let prop = PropertyValue {
+                key: PropertyBindingKey::new(element.id),
+                value: element.color,
+            };
+            properties.colors.push(prop);
+        }
+    }
+
     txn.update_dynamic_properties(properties);
 }
 
 #[no_mangle]
 pub extern "C" fn wr_transaction_append_transform_properties(
     txn: &mut Transaction,
     transform_array: *const WrTransformProperty,
     transform_count: usize,
@@ -2376,16 +2401,17 @@ pub extern "C" fn wr_dp_push_stacking_co
                 has_opacity_animation = true;
             },
             WrAnimationType::Transform => {
                 transform_binding =
                     Some(PropertyBinding::Binding(PropertyBindingKey::new(anim.id),
                                                   // Same as above opacity case.
                                                   transform_ref.cloned().unwrap_or(LayoutTransform::identity())));
             },
+            _ => unreachable!("{:?} should not create a stacking context", anim.effect_type),
         }
     }
 
     if let Some(opacity) = opacity_ref {
         if !has_opacity_animation && *opacity < 1.0 {
             filters.push(FilterOp::Opacity(PropertyBinding::Value(*opacity), *opacity));
         }
     }
@@ -2608,47 +2634,97 @@ fn prim_flags(
 ) -> PrimitiveFlags {
     if is_backface_visible {
         PrimitiveFlags::IS_BACKFACE_VISIBLE
     } else {
         PrimitiveFlags::empty()
     }
 }
 
-#[no_mangle]
-pub extern "C" fn wr_dp_push_rect(state: &mut WrState,
-                                  rect: LayoutRect,
-                                  clip: LayoutRect,
-                                  is_backface_visible: bool,
-                                  parent: &WrSpaceAndClipChain,
-                                  color: ColorF) {
-    debug_assert!(unsafe { !is_in_render_thread() });
-
+fn common_item_properties_for_rect(
+    state: &mut WrState,
+    rect: LayoutRect,
+    clip: LayoutRect,
+    is_backface_visible: bool,
+    parent: &WrSpaceAndClipChain
+) -> CommonItemProperties {
     let space_and_clip = parent.to_webrender(state.pipeline_id);
     let clip_rect = clip.intersection(&rect);
 
-    let prim_info = CommonItemProperties {
+    CommonItemProperties {
         // NB: the damp-e10s talos-test will frequently crash on startup if we
         // early-return here for empty rects. I couldn't figure out why, but
         // it's pretty harmless to feed these through, so, uh, we do?
         clip_rect: clip_rect.unwrap_or(LayoutRect::zero()),
         clip_id: space_and_clip.clip_id,
         spatial_id: space_and_clip.spatial_id,
         flags: prim_flags(is_backface_visible),
         hit_info: state.current_tag,
         item_key: state.current_item_key,
-    };
+    }
+}
+
+#[no_mangle]
+pub extern "C" fn wr_dp_push_rect(state: &mut WrState,
+                                  rect: LayoutRect,
+                                  clip: LayoutRect,
+                                  is_backface_visible: bool,
+                                  parent: &WrSpaceAndClipChain,
+                                  color: ColorF) {
+    debug_assert!(unsafe { !is_in_render_thread() });
+
+    let prim_info = common_item_properties_for_rect(
+        state,
+        rect,
+        clip,
+        is_backface_visible,
+        parent,
+    );
 
     state.frame_builder.dl_builder.push_rect(
         &prim_info,
         color,
     );
 }
 
 #[no_mangle]
+pub extern "C" fn wr_dp_push_rect_with_animation(
+    state: &mut WrState,
+    rect: LayoutRect,
+    clip: LayoutRect,
+    is_backface_visible: bool,
+    parent: &WrSpaceAndClipChain,
+    color: ColorF,
+    animation: *const WrAnimationProperty) {
+    debug_assert!(unsafe { !is_in_render_thread() });
+
+    let prim_info = common_item_properties_for_rect(
+        state,
+        rect,
+        clip,
+        is_backface_visible,
+        parent,
+    );
+
+    let anim = unsafe { animation.as_ref() };
+    if let Some(anim) = anim {
+        debug_assert!(anim.id > 0);
+        match anim.effect_type {
+            WrAnimationType::BackgroundColor => {
+                state.frame_builder.dl_builder.push_rect_with_animation(
+                    &prim_info,
+                    PropertyBinding::Binding(PropertyBindingKey::new(anim.id), color),
+                )
+            },
+            _ => unreachable!("Didn't expect {:?} animation", anim.effect_type),
+        }
+    }
+}
+
+#[no_mangle]
 pub extern "C" fn wr_dp_push_rect_with_parent_clip(
     state: &mut WrState,
     rect: LayoutRect,
     clip: LayoutRect,
     is_backface_visible: bool,
     parent: &WrSpaceAndClip,
     color: ColorF,
 ) {
--- a/gfx/wr/examples/animation.rs
+++ b/gfx/wr/examples/animation.rs
@@ -182,16 +182,17 @@ impl Example for App {
                             },
                         ],
                         floats: vec![
                             PropertyValue {
                                 key: self.opacity_key,
                                 value: self.opacity,
                             }
                         ],
+                        colors: vec![],
                     },
                 );
                 txn.generate_frame();
                 api.send_transaction(document_id, txn);
             }
             _ => (),
         }
 
--- a/gfx/wr/webrender/src/box_shadow.rs
+++ b/gfx/wr/webrender/src/box_shadow.rs
@@ -1,13 +1,14 @@
 /* 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 api::{BorderRadius, BoxShadowClipMode, ClipMode, ColorF, PrimitiveKeyKind};
+use api::PropertyBinding;
 use api::MAX_BLUR_RADIUS;
 use api::units::*;
 use crate::clip::{ClipItemKey, ClipItemKeyKind};
 use crate::scene_building::SceneBuilder;
 use crate::gpu_cache::GpuCacheHandle;
 use crate::gpu_types::BoxShadowStretchMode;
 use crate::prim_store::ScrollNodeAndClipChain;
 use crate::render_task_cache::RenderTaskCacheEntryHandle;
@@ -158,17 +159,17 @@ impl<'a> SceneBuilder<'a> {
                 spatial_node_index: clip_and_scroll.spatial_node_index,
             });
 
             self.add_primitive(
                 clip_and_scroll,
                 &LayoutPrimitiveInfo::with_clip_rect(final_prim_rect, prim_info.clip_rect),
                 clips,
                 PrimitiveKeyKind::Rectangle {
-                    color: color.into(),
+                    color: PropertyBinding::Value(color.into()),
                 },
             );
         } else {
             // Normal path for box-shadows with a valid blur radius.
             let blur_offset = (BLUR_SAMPLE_SCALE * blur_radius).ceil();
             let mut extra_clips = vec![];
 
             // Add a normal clip mask to clip out the contents
@@ -184,17 +185,17 @@ impl<'a> SceneBuilder<'a> {
 
             // Get the local rect of where the shadow will be drawn,
             // expanded to include room for the blurred region.
             let dest_rect = shadow_rect.inflate(blur_offset, blur_offset);
 
             // Draw the box-shadow as a solid rect, using a box-shadow
             // clip mask item.
             let prim = PrimitiveKeyKind::Rectangle {
-                color: color.into(),
+                color: PropertyBinding::Value(color.into()),
             };
 
             // Create the box-shadow clip item.
             let shadow_clip_source = ClipItemKey {
                 kind: ClipItemKeyKind::box_shadow(
                     shadow_rect,
                     shadow_radius,
                     dest_rect,
--- a/gfx/wr/webrender/src/picture.rs
+++ b/gfx/wr/webrender/src/picture.rs
@@ -91,17 +91,17 @@
 //! of the compositor surface. Otherwise, we can draw the tiles in the normal fast
 //! path before the compositor surface is drawn. Use of the per-tile valid and
 //! dirty rects ensure that we do a minimal amount of per-pixel work here to
 //! blend the overlay tile (this is not always optimal right now, but will be
 //! improved as a follow up).
 
 use api::{MixBlendMode, PipelineId, PremultipliedColorF, FilterPrimitiveKind};
 use api::{PropertyBinding, PropertyBindingId, FilterPrimitive, FontRenderMode};
-use api::{DebugFlags, RasterSpace, ImageKey, ColorF, PrimitiveFlags};
+use api::{DebugFlags, RasterSpace, ImageKey, ColorF, ColorU, PrimitiveFlags};
 use api::units::*;
 use crate::box_shadow::{BLUR_SAMPLE_SCALE};
 use crate::clip::{ClipStore, ClipChainInstance, ClipDataHandle, ClipChainId};
 use crate::spatial_tree::{ROOT_SPATIAL_NODE_INDEX,
     SpatialTree, CoordinateSpaceMapping, SpatialNodeIndex, VisibleFace
 };
 use crate::composite::{CompositorKind, CompositeState, NativeSurfaceId, NativeTileId};
 use crate::composite::{ExternalSurfaceDescriptor};
@@ -115,16 +115,17 @@ use crate::internal_types::{FastHashMap,
 use crate::frame_builder::{FrameBuildingContext, FrameBuildingState, PictureState, PictureContext};
 use crate::gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle};
 use crate::gpu_types::UvRectKind;
 use plane_split::{Clipper, Polygon, Splitter};
 use crate::prim_store::{SpaceMapper, PrimitiveVisibilityMask, PointKey, PrimitiveTemplateKind};
 use crate::prim_store::{SpaceSnapper, PictureIndex, PrimitiveInstance, PrimitiveInstanceKind};
 use crate::prim_store::{get_raster_rects, PrimitiveScratchBuffer, RectangleKey};
 use crate::prim_store::{OpacityBindingStorage, ImageInstanceStorage, OpacityBindingIndex};
+use crate::prim_store::{ColorBindingStorage, ColorBindingIndex};
 use crate::print_tree::{PrintTree, PrintTreePrinter};
 use crate::render_backend::DataStores;
 use crate::render_task_graph::RenderTaskId;
 use crate::render_target::RenderTargetKind;
 use crate::render_task::{RenderTask, RenderTaskLocation, BlurTaskCache, ClearMode};
 use crate::resource_cache::{ResourceCache, ImageGeneration};
 use crate::scene::SceneProperties;
 use crate::spatial_tree::CoordinateSystemId;
@@ -259,30 +260,33 @@ struct PictureInfo {
 /// Picture-caching state to keep between scenes.
 pub struct PictureCacheState {
     /// The tiles retained by this picture cache.
     pub tiles: FastHashMap<TileOffset, Box<Tile>>,
     /// State of the spatial nodes from previous frame
     spatial_nodes: FastHashMap<SpatialNodeIndex, SpatialNodeDependency>,
     /// State of opacity bindings from previous frame
     opacity_bindings: FastHashMap<PropertyBindingId, OpacityBindingInfo>,
+    /// State of color bindings from previous frame
+    color_bindings: FastHashMap<PropertyBindingId, ColorBindingInfo>,
     /// The current transform of the picture cache root spatial node
     root_transform: TransformKey,
     /// The current tile size in device pixels
     current_tile_size: DeviceIntSize,
     /// Various allocations we want to avoid re-doing.
     allocations: PictureCacheRecycledAllocations,
     /// Currently allocated native compositor surface for this picture cache.
     pub native_surface_id: Option<NativeSurfaceId>,
     /// True if the entire picture cache is opaque.
     is_opaque: bool,
 }
 
 pub struct PictureCacheRecycledAllocations {
     old_opacity_bindings: FastHashMap<PropertyBindingId, OpacityBindingInfo>,
+    old_color_bindings: FastHashMap<PropertyBindingId, ColorBindingInfo>,
     compare_cache: FastHashMap<PrimitiveComparisonKey, PrimitiveCompareResult>,
 }
 
 /// Stores a list of cached picture tiles that are retained
 /// between new scenes.
 #[cfg_attr(feature = "capture", derive(Serialize))]
 pub struct RetainedTiles {
     /// The tiles retained between display lists.
@@ -396,43 +400,49 @@ fn clampf(value: f32, low: f32, high: f3
 }
 
 /// An index into the prims array in a TileDescriptor.
 #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct PrimitiveDependencyIndex(pub u32);
 
-/// Information about the state of an opacity binding.
+/// Information about the state of a binding.
 #[derive(Debug)]
-pub struct OpacityBindingInfo {
+pub struct BindingInfo<T> {
     /// The current value retrieved from dynamic scene properties.
-    value: f32,
+    value: T,
     /// True if it was changed (or is new) since the last frame build.
     changed: bool,
 }
 
-/// Information stored in a tile descriptor for an opacity binding.
+/// Information stored in a tile descriptor for a binding.
 #[derive(Debug, PartialEq, Clone, Copy)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
-pub enum OpacityBinding {
-    Value(f32),
+pub enum Binding<T> {
+    Value(T),
     Binding(PropertyBindingId),
 }
 
-impl From<PropertyBinding<f32>> for OpacityBinding {
-    fn from(binding: PropertyBinding<f32>) -> OpacityBinding {
+impl<T> From<PropertyBinding<T>> for Binding<T> {
+    fn from(binding: PropertyBinding<T>) -> Binding<T> {
         match binding {
-            PropertyBinding::Binding(key, _) => OpacityBinding::Binding(key.id),
-            PropertyBinding::Value(value) => OpacityBinding::Value(value),
+            PropertyBinding::Binding(key, _) => Binding::Binding(key.id),
+            PropertyBinding::Value(value) => Binding::Value(value),
         }
     }
 }
 
+pub type OpacityBinding = Binding<f32>;
+pub type OpacityBindingInfo = BindingInfo<f32>;
+
+pub type ColorBinding = Binding<ColorU>;
+pub type ColorBindingInfo = BindingInfo<ColorU>;
+
 /// Information about the state of a spatial node value
 #[derive(Debug)]
 pub struct SpatialNodeDependency {
     /// The current value retrieved from the spatial tree.
     value: TransformKey,
     /// True if it was changed (or is new) since the last frame build.
     changed: bool,
 }
@@ -471,16 +481,19 @@ struct TilePostUpdateContext<'a> {
     backdrop: BackdropInfo,
 
     /// Information about transform node differences from last frame.
     spatial_nodes: &'a FastHashMap<SpatialNodeIndex, SpatialNodeDependency>,
 
     /// Information about opacity bindings from the picture cache.
     opacity_bindings: &'a FastHashMap<PropertyBindingId, OpacityBindingInfo>,
 
+    /// Information about color bindings from the picture cache.
+    color_bindings: &'a FastHashMap<PropertyBindingId, ColorBindingInfo>,
+
     /// Current size in device pixels of tiles for this cache
     current_tile_size: DeviceIntSize,
 
     /// The local rect of the overall picture cache
     local_rect: PictureRect,
 }
 
 // Mutable state passed to picture cache tiles during post_update
@@ -510,16 +523,19 @@ struct PrimitiveDependencyInfo {
     prim_clip_rect: PictureRect,
 
     /// Image keys this primitive depends on.
     images: SmallVec<[ImageDependency; 8]>,
 
     /// Opacity bindings this primitive depends on.
     opacity_bindings: SmallVec<[OpacityBinding; 4]>,
 
+    /// Color binding this primitive depends on.
+    color_binding: Option<ColorBinding>,
+
     /// Clips that this primitive depends on.
     clips: SmallVec<[ItemUid; 8]>,
 
     /// Spatial nodes references by the clip dependencies of this primitive.
     spatial_nodes: SmallVec<[SpatialNodeIndex; 4]>,
 
     /// If true, this primitive has been promoted to be a compositor surface.
     is_compositor_surface: bool,
@@ -532,16 +548,17 @@ impl PrimitiveDependencyInfo {
         prim_origin: PicturePoint,
         prim_clip_rect: PictureRect,
     ) -> Self {
         PrimitiveDependencyInfo {
             prim_uid,
             prim_origin,
             images: SmallVec::new(),
             opacity_bindings: SmallVec::new(),
+            color_binding: None,
             clip_by_tile: false,
             prim_clip_rect,
             clips: SmallVec::new(),
             spatial_nodes: SmallVec::new(),
             is_compositor_surface: false,
         }
     }
 }
@@ -683,16 +700,18 @@ pub enum PrimitiveCompareResult {
     /// The clip node content or spatial node changed
     Clip,
     /// The value of the transform changed
     Transform,
     /// An image dependency was dirty
     Image,
     /// The value of an opacity binding changed
     OpacityBinding,
+    /// The value of a color binding changed
+    ColorBinding,
 }
 
 /// A more detailed version of PrimitiveCompareResult used when
 /// debug logging is enabled.
 #[derive(Debug, Copy, Clone, PartialEq)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub enum PrimitiveCompareResultDetail {
@@ -713,17 +732,21 @@ pub enum PrimitiveCompareResultDetail {
     },
     /// An image dependency was dirty
     Image {
         detail: CompareHelperResult<ImageDependency>,
     },
     /// The value of an opacity binding changed
     OpacityBinding {
         detail: CompareHelperResult<OpacityBinding>,
-    }
+    },
+    /// The value of a color binding changed
+    ColorBinding {
+        detail: CompareHelperResult<ColorBinding>,
+    },
 }
 
 /// Debugging information about why a tile was invalidated
 #[derive(Debug,Clone)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub enum InvalidationReason {
     /// The fractional offset changed
@@ -881,16 +904,17 @@ impl Tile {
         frame_context: &FrameVisibilityContext,
     ) -> PictureRect {
         let mut prim_comparer = PrimitiveComparer::new(
             &self.prev_descriptor,
             &self.current_descriptor,
             state.resource_cache,
             ctx.spatial_nodes,
             ctx.opacity_bindings,
+            ctx.color_bindings,
         );
 
         let mut dirty_rect = PictureRect::zero();
         self.root.update_dirty_rects(
             &self.prev_descriptor.prims,
             &self.current_descriptor.prims,
             &mut prim_comparer,
             &mut dirty_rect,
@@ -1049,16 +1073,22 @@ impl Tile {
         self.current_descriptor.opacity_bindings.extend_from_slice(&info.opacity_bindings);
 
         // Include any clip nodes that this primitive depends on.
         self.current_descriptor.clips.extend_from_slice(&info.clips);
 
         // Include any transforms that this primitive depends on.
         self.current_descriptor.transforms.extend_from_slice(&info.spatial_nodes);
 
+        // Include any color bindings this primitive depends on.
+        if info.color_binding.is_some() {
+            self.current_descriptor.color_bindings.insert(
+                self.current_descriptor.color_bindings.len(), info.color_binding.unwrap());
+        }
+
         // TODO(gw): The origin of background rects produced by APZ changes
         //           in Gecko during scrolling. Consider investigating this so the
         //           hack / workaround below is not required.
         let (prim_origin, prim_clip_rect) = if info.clip_by_tile {
             let tile_p0 = self.local_tile_rect.origin;
             let tile_p1 = self.local_tile_rect.bottom_right();
 
             let clip_p0 = PicturePoint::new(
@@ -1101,16 +1131,17 @@ impl Tile {
         self.current_descriptor.prims.push(PrimitiveDescriptor {
             prim_uid: info.prim_uid,
             origin: prim_origin.into(),
             prim_clip_rect: prim_clip_rect.into(),
             transform_dep_count: info.spatial_nodes.len()  as u8,
             clip_dep_count: info.clips.len() as u8,
             image_dep_count: info.images.len() as u8,
             opacity_binding_dep_count: info.opacity_bindings.len() as u8,
+            color_binding_dep_count: if info.color_binding.is_some() { 1 } else { 0 } as u8,
         });
 
         // Add this primitive to the dirty rect quadtree.
         self.root.add_prim(prim_index, &info.prim_clip_rect);
     }
 
     /// Called during tile cache instance post_update. Allows invalidation and dirty
     /// rect calculation after primitive dependencies have been updated.
@@ -1312,16 +1343,17 @@ pub struct PrimitiveDescriptor {
     /// dependencies since there is no entry in the clip chain
     /// dependencies for the local clip rect.
     pub prim_clip_rect: RectangleKey,
     /// The number of extra dependencies that this primitive has.
     transform_dep_count: u8,
     image_dep_count: u8,
     opacity_binding_dep_count: u8,
     clip_dep_count: u8,
+    color_binding_dep_count: u8,
 }
 
 impl PartialEq for PrimitiveDescriptor {
     fn eq(&self, other: &Self) -> bool {
         const EPSILON: f32 = 0.001;
 
         if self.prim_uid != other.prim_uid {
             return false;
@@ -1462,27 +1494,32 @@ pub struct TileDescriptor {
     opacity_bindings: Vec<OpacityBinding>,
 
     /// List of the effects of transforms that we care about
     /// tracking for this tile.
     transforms: Vec<SpatialNodeIndex>,
 
     /// Picture space rect that contains valid pixels region of this tile.
     local_valid_rect: PictureRect,
+
+    /// List of the effects of color that we care about
+    /// tracking for this tile.
+    color_bindings: Vec<ColorBinding>,
 }
 
 impl TileDescriptor {
     fn new() -> Self {
         TileDescriptor {
             prims: Vec::new(),
             clips: Vec::new(),
             opacity_bindings: Vec::new(),
             images: Vec::new(),
             transforms: Vec::new(),
             local_valid_rect: PictureRect::zero(),
+            color_bindings: Vec::new(),
         }
     }
 
     /// Print debug information about this tile descriptor to a tree printer.
     fn print(&self, pt: &mut dyn PrintTreePrinter) {
         pt.new_level("current_descriptor".to_string());
 
         pt.new_level("prims".to_string());
@@ -1490,21 +1527,22 @@ impl TileDescriptor {
             pt.new_level(format!("prim uid={}", prim.prim_uid.get_uid()));
             pt.add_item(format!("origin: {},{}", prim.origin.x, prim.origin.y));
             pt.add_item(format!("clip: origin={},{} size={}x{}",
                 prim.prim_clip_rect.x,
                 prim.prim_clip_rect.y,
                 prim.prim_clip_rect.w,
                 prim.prim_clip_rect.h,
             ));
-            pt.add_item(format!("deps: t={} i={} o={} c={}",
+            pt.add_item(format!("deps: t={} i={} o={} c={} color={}",
                 prim.transform_dep_count,
                 prim.image_dep_count,
                 prim.opacity_binding_dep_count,
                 prim.clip_dep_count,
+                prim.color_binding_dep_count,
             ));
             pt.end_level();
         }
         pt.end_level();
 
         if !self.clips.is_empty() {
             pt.new_level("clips".to_string());
             for clip in &self.clips {
@@ -1537,28 +1575,38 @@ impl TileDescriptor {
             pt.new_level("transforms".to_string());
             for transform in &self.transforms {
                 pt.new_level(format!("spatial_node={:?}", transform));
                 pt.end_level();
             }
             pt.end_level();
         }
 
+        if !self.color_bindings.is_empty() {
+            pt.new_level("color_bindings".to_string());
+            for color_binding in &self.color_bindings {
+                pt.new_level(format!("binding={:?}", color_binding));
+                pt.end_level();
+            }
+            pt.end_level();
+        }
+
         pt.end_level();
     }
 
     /// Clear the dependency information for a tile, when the dependencies
     /// are being rebuilt.
     fn clear(&mut self) {
         self.prims.clear();
         self.clips.clear();
         self.opacity_bindings.clear();
         self.images.clear();
         self.transforms.clear();
         self.local_valid_rect = PictureRect::zero();
+        self.color_bindings.clear();
     }
 }
 
 /// Stores both the world and devices rects for a single dirty rect.
 #[derive(Debug, Clone)]
 pub struct DirtyRegionRect {
     /// World rect of this dirty region
     pub world_rect: WorldRect,
@@ -2021,16 +2069,21 @@ pub struct TileCacheInstance {
     spatial_nodes: FastHashMap<SpatialNodeIndex, SpatialNodeDependency>,
     /// Switch back and forth between old and new spatial nodes hashmaps to avoid re-allocating.
     old_spatial_nodes: FastHashMap<SpatialNodeIndex, SpatialNodeDependency>,
     /// A set of spatial nodes that primitives / clips depend on found
     /// during dependency creation. This is used to avoid trying to
     /// calculate invalid relative transforms when building the spatial
     /// nodes hash above.
     used_spatial_nodes: FastHashSet<SpatialNodeIndex>,
+    /// List of color bindings, with some extra information
+    /// about whether they changed since last frame.
+    color_bindings: FastHashMap<PropertyBindingId, ColorBindingInfo>,
+    /// Switch back and forth between old and new bindings hashmaps to avoid re-allocating.
+    old_color_bindings: FastHashMap<PropertyBindingId, ColorBindingInfo>,
     /// The current dirty region tracker for this picture.
     pub dirty_region: DirtyRegion,
     /// Current size of tiles in picture units.
     tile_size: PictureSize,
     /// Tile coords of the currently allocated grid.
     tile_rect: TileRect,
     /// Pre-calculated versions of the tile_rect above, used to speed up the
     /// calculations in get_tile_coords_for_rect.
@@ -2109,16 +2162,18 @@ impl TileCacheInstance {
                 ROOT_SPATIAL_NODE_INDEX,
                 PictureRect::zero(),
             ),
             opacity_bindings: FastHashMap::default(),
             old_opacity_bindings: FastHashMap::default(),
             spatial_nodes: FastHashMap::default(),
             old_spatial_nodes: FastHashMap::default(),
             used_spatial_nodes: FastHashSet::default(),
+            color_bindings: FastHashMap::default(),
+            old_color_bindings: FastHashMap::default(),
             dirty_region: DirtyRegion::new(),
             tile_size: PictureSize::zero(),
             tile_rect: TileRect::zero(),
             tile_bounds_p0: TileOffset::zero(),
             tile_bounds_p1: TileOffset::zero(),
             local_rect: PictureRect::zero(),
             local_clip_rect: PictureRect::zero(),
             surface_index: SurfaceIndex(0),
@@ -2170,17 +2225,17 @@ impl TileCacheInstance {
         p0.x = clamp(p0.x, self.tile_bounds_p0.x, self.tile_bounds_p1.x);
         p0.y = clamp(p0.y, self.tile_bounds_p0.y, self.tile_bounds_p1.y);
         p1.x = clamp(p1.x, self.tile_bounds_p0.x, self.tile_bounds_p1.x);
         p1.y = clamp(p1.y, self.tile_bounds_p0.y, self.tile_bounds_p1.y);
 
         (p0, p1)
     }
 
-    /// Update transforms, opacity bindings and tile rects.
+    /// Update transforms, opacity, color bindings and tile rects.
     pub fn pre_update(
         &mut self,
         pic_rect: PictureRect,
         surface_index: SurfaceIndex,
         frame_context: &FrameVisibilityContext,
         frame_state: &mut FrameVisibilityState,
     ) -> WorldRect {
         self.external_surfaces.clear();
@@ -2252,16 +2307,17 @@ impl TileCacheInstance {
         }
 
         // If there are pending retained state, retrieve it.
         if let Some(prev_state) = frame_state.retained_tiles.caches.remove(&self.slice) {
             self.tiles.extend(prev_state.tiles);
             self.root_transform = prev_state.root_transform;
             self.spatial_nodes = prev_state.spatial_nodes;
             self.opacity_bindings = prev_state.opacity_bindings;
+            self.color_bindings = prev_state.color_bindings;
             self.current_tile_size = prev_state.current_tile_size;
             self.native_surface_id = prev_state.native_surface_id;
             self.is_opaque = prev_state.is_opaque;
 
             fn recycle_map<K: std::cmp::Eq + std::hash::Hash, V>(
                 ideal_len: usize,
                 dest: &mut FastHashMap<K, V>,
                 src: FastHashMap<K, V>,
@@ -2276,16 +2332,21 @@ impl TileCacheInstance {
                 }
             }
             recycle_map(
                 self.opacity_bindings.len(),
                 &mut self.old_opacity_bindings,
                 prev_state.allocations.old_opacity_bindings,
             );
             recycle_map(
+                self.color_bindings.len(),
+                &mut self.old_color_bindings,
+                prev_state.allocations.old_color_bindings,
+            );
+            recycle_map(
                 prev_state.allocations.compare_cache.len(),
                 &mut self.compare_cache,
                 prev_state.allocations.compare_cache,
             );
         }
 
         // Only evaluate what tile size to use fairly infrequently, so that we don't end
         // up constantly invalidating and reallocating tiles if the picture rect size is
@@ -2375,16 +2436,33 @@ impl TileCacheInstance {
                 None => true,
             };
             self.opacity_bindings.insert(*id, OpacityBindingInfo {
                 value: *value,
                 changed,
             });
         }
 
+        // Do a hacky diff of color binding values from the last frame. This is
+        // used later on during tile invalidation tests.
+        let current_properties = frame_context.scene_properties.color_properties();
+        mem::swap(&mut self.color_bindings, &mut self.old_color_bindings);
+
+        self.color_bindings.clear();
+        for (id, value) in current_properties {
+            let changed = match self.old_color_bindings.get(id) {
+                Some(old_property) => old_property.value != (*value).into(),
+                None => true,
+            };
+            self.color_bindings.insert(*id, ColorBindingInfo {
+                value: (*value).into(),
+                changed,
+            });
+        }
+
         let world_tile_size = WorldSize::new(
             self.current_tile_size.width as f32 / frame_context.global_device_pixel_scale.0,
             self.current_tile_size.height as f32 / frame_context.global_device_pixel_scale.0,
         );
 
         // We know that this is an exact rectangle, since we (for now) only support tile
         // caches where the scroll root is in the root coordinate system.
         let local_tile_rect = pic_to_world_mapper
@@ -2534,16 +2612,17 @@ impl TileCacheInstance {
         prim_clip_chain: Option<&ClipChainInstance>,
         local_prim_rect: LayoutRect,
         frame_context: &FrameVisibilityContext,
         data_stores: &DataStores,
         clip_store: &ClipStore,
         pictures: &[PicturePrimitive],
         resource_cache: &ResourceCache,
         opacity_binding_store: &OpacityBindingStorage,
+        color_bindings: &ColorBindingStorage,
         image_instances: &ImageInstanceStorage,
         surface_stack: &[SurfaceIndex],
         composite_state: &CompositeState,
     ) -> bool {
         // This primitive exists on the last element on the current surface stack.
         let prim_surface_index = *surface_stack.last().unwrap();
 
         // If the primitive is completely clipped out by the clip chain, there
@@ -2670,35 +2749,41 @@ impl TileCacheInstance {
         match prim_instance.kind {
             PrimitiveInstanceKind::Picture { pic_index,.. } => {
                 // Pictures can depend on animated opacity bindings.
                 let pic = &pictures[pic_index.0];
                 if let Some(PictureCompositeMode::Filter(Filter::Opacity(binding, _))) = pic.requested_composite_mode {
                     prim_info.opacity_bindings.push(binding.into());
                 }
             }
-            PrimitiveInstanceKind::Rectangle { data_handle, opacity_binding_index, .. } => {
+            PrimitiveInstanceKind::Rectangle { data_handle, opacity_binding_index, color_binding_index, .. } => {
                 if opacity_binding_index == OpacityBindingIndex::INVALID {
                     // Rectangles can only form a backdrop candidate if they are known opaque.
                     // TODO(gw): We could resolve the opacity binding here, but the common
                     //           case for background rects is that they don't have animated opacity.
                     let color = match data_stores.prim[data_handle].kind {
-                        PrimitiveTemplateKind::Rectangle { color, .. } => color,
+                        PrimitiveTemplateKind::Rectangle { color, .. } => {
+                            frame_context.scene_properties.resolve_color(&color)
+                        }
                         _ => unreachable!(),
                     };
                     if color.a >= 1.0 {
                         backdrop_candidate = Some(BackdropKind::Color { color });
                     }
                 } else {
                     let opacity_binding = &opacity_binding_store[opacity_binding_index];
                     for binding in &opacity_binding.bindings {
                         prim_info.opacity_bindings.push(OpacityBinding::from(*binding));
                     }
                 }
 
+                if color_binding_index != ColorBindingIndex::INVALID {
+                    prim_info.color_binding = Some(color_bindings[color_binding_index].into());
+                }
+
                 prim_info.clip_by_tile = true;
             }
             PrimitiveInstanceKind::Image { data_handle, image_instance_index, .. } => {
                 let image_data = &data_stores.image[data_handle].kind;
                 let image_instance = &image_instances[image_instance_index];
                 let opacity_binding_index = image_instance.opacity_binding_index;
 
                 if opacity_binding_index == OpacityBindingIndex::INVALID {
@@ -3090,16 +3175,17 @@ impl TileCacheInstance {
 
         let ctx = TilePostUpdateContext {
             pic_to_world_mapper,
             global_device_pixel_scale: frame_context.global_device_pixel_scale,
             local_clip_rect: self.local_clip_rect,
             backdrop: self.backdrop,
             spatial_nodes: &self.spatial_nodes,
             opacity_bindings: &self.opacity_bindings,
+            color_bindings: &self.color_bindings,
             current_tile_size: self.current_tile_size,
             local_rect: self.local_rect,
         };
 
         let mut state = TilePostUpdateState {
             resource_cache: frame_state.resource_cache,
             composite_state: frame_state.composite_state,
             compare_cache: &mut self.compare_cache,
@@ -3973,22 +4059,24 @@ impl PicturePrimitive {
         if let Some(mut tile_cache) = self.tile_cache.take() {
             if !tile_cache.tiles.is_empty() {
                 retained_tiles.caches.insert(
                     tile_cache.slice,
                     PictureCacheState {
                         tiles: tile_cache.tiles,
                         spatial_nodes: tile_cache.spatial_nodes,
                         opacity_bindings: tile_cache.opacity_bindings,
+                        color_bindings: tile_cache.color_bindings,
                         root_transform: tile_cache.root_transform,
                         current_tile_size: tile_cache.current_tile_size,
                         native_surface_id: tile_cache.native_surface_id.take(),
                         is_opaque: tile_cache.is_opaque,
                         allocations: PictureCacheRecycledAllocations {
                             old_opacity_bindings: tile_cache.old_opacity_bindings,
+                            old_color_bindings: tile_cache.old_color_bindings,
                             compare_cache: tile_cache.compare_cache,
                         },
                     },
                 );
             }
         }
     }
 
@@ -5533,28 +5621,31 @@ pub struct ImageDependency {
 }
 
 /// A helper struct to compare a primitive and all its sub-dependencies.
 struct PrimitiveComparer<'a> {
     clip_comparer: CompareHelper<'a, ItemUid>,
     transform_comparer: CompareHelper<'a, SpatialNodeIndex>,
     image_comparer: CompareHelper<'a, ImageDependency>,
     opacity_comparer: CompareHelper<'a, OpacityBinding>,
+    color_comparer: CompareHelper<'a, ColorBinding>,
     resource_cache: &'a ResourceCache,
     spatial_nodes: &'a FastHashMap<SpatialNodeIndex, SpatialNodeDependency>,
     opacity_bindings: &'a FastHashMap<PropertyBindingId, OpacityBindingInfo>,
+    color_bindings: &'a FastHashMap<PropertyBindingId, ColorBindingInfo>,
 }
 
 impl<'a> PrimitiveComparer<'a> {
     fn new(
         prev: &'a TileDescriptor,
         curr: &'a TileDescriptor,
         resource_cache: &'a ResourceCache,
         spatial_nodes: &'a FastHashMap<SpatialNodeIndex, SpatialNodeDependency>,
         opacity_bindings: &'a FastHashMap<PropertyBindingId, OpacityBindingInfo>,
+        color_bindings: &'a FastHashMap<PropertyBindingId, ColorBindingInfo>,
     ) -> Self {
         let clip_comparer = CompareHelper::new(
             &prev.clips,
             &curr.clips,
         );
 
         let transform_comparer = CompareHelper::new(
             &prev.transforms,
@@ -5566,58 +5657,69 @@ impl<'a> PrimitiveComparer<'a> {
             &curr.images,
         );
 
         let opacity_comparer = CompareHelper::new(
             &prev.opacity_bindings,
             &curr.opacity_bindings,
         );
 
+        let color_comparer = CompareHelper::new(
+            &prev.color_bindings,
+            &curr.color_bindings,
+        );
+
         PrimitiveComparer {
             clip_comparer,
             transform_comparer,
             image_comparer,
             opacity_comparer,
+            color_comparer,
             resource_cache,
             spatial_nodes,
             opacity_bindings,
+            color_bindings,
         }
     }
 
     fn reset(&mut self) {
         self.clip_comparer.reset();
         self.transform_comparer.reset();
         self.image_comparer.reset();
         self.opacity_comparer.reset();
+        self.color_comparer.reset();
     }
 
     fn advance_prev(&mut self, prim: &PrimitiveDescriptor) {
         self.clip_comparer.advance_prev(prim.clip_dep_count);
         self.transform_comparer.advance_prev(prim.transform_dep_count);
         self.image_comparer.advance_prev(prim.image_dep_count);
         self.opacity_comparer.advance_prev(prim.opacity_binding_dep_count);
+        self.color_comparer.advance_prev(prim.color_binding_dep_count);
     }
 
     fn advance_curr(&mut self, prim: &PrimitiveDescriptor) {
         self.clip_comparer.advance_curr(prim.clip_dep_count);
         self.transform_comparer.advance_curr(prim.transform_dep_count);
         self.image_comparer.advance_curr(prim.image_dep_count);
         self.opacity_comparer.advance_curr(prim.opacity_binding_dep_count);
+        self.color_comparer.advance_curr(prim.color_binding_dep_count);
     }
 
     /// Check if two primitive descriptors are the same.
     fn compare_prim(
         &mut self,
         prev: &PrimitiveDescriptor,
         curr: &PrimitiveDescriptor,
         opt_detail: Option<&mut PrimitiveCompareResultDetail>,
     ) -> PrimitiveCompareResult {
         let resource_cache = self.resource_cache;
         let spatial_nodes = self.spatial_nodes;
         let opacity_bindings = self.opacity_bindings;
+        let color_bindings = self.color_bindings;
 
         // Check equality of the PrimitiveDescriptor
         if prev != curr {
             if let Some(detail) = opt_detail {
                 *detail = PrimitiveCompareResultDetail::Descriptor{ old: *prev, new: *curr };
             }
             return PrimitiveCompareResult::Descriptor;
         }
@@ -5687,16 +5789,40 @@ impl<'a> PrimitiveComparer<'a> {
             if opt_detail.is_some() { Some(&mut bind_result) } else { None },
         ) {
             if let Some(detail) = opt_detail {
                 *detail = PrimitiveCompareResultDetail::OpacityBinding{ detail: bind_result };
             }
             return PrimitiveCompareResult::OpacityBinding;
         }
 
+        // Check if any of the color bindings this prim has are different.
+        let mut bind_result = CompareHelperResult::Equal;
+        if !self.color_comparer.is_same(
+            prev.color_binding_dep_count,
+            curr.color_binding_dep_count,
+            |curr| {
+                if let ColorBinding::Binding(id) = curr {
+                    if color_bindings
+                        .get(id)
+                        .map_or(true, |info| info.changed) {
+                        return true;
+                    }
+                }
+
+                true
+            },
+            if opt_detail.is_some() { Some(&mut bind_result) } else { None },
+        ) {
+            if let Some(detail) = opt_detail {
+                *detail = PrimitiveCompareResultDetail::ColorBinding{ detail: bind_result };
+            }
+            return PrimitiveCompareResult::ColorBinding;
+        }
+
         PrimitiveCompareResult::Equal
     }
 }
 
 /// Details for a node in a quadtree that tracks dirty rects for a tile.
 #[cfg_attr(any(feature="capture",feature="replay"), derive(Clone))]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
--- a/gfx/wr/webrender/src/prim_store/mod.rs
+++ b/gfx/wr/webrender/src/prim_store/mod.rs
@@ -1,13 +1,13 @@
 /* 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 api::{BorderRadius, ClipMode, ColorF};
+use api::{BorderRadius, ClipMode, ColorF, ColorU};
 use api::{ImageRendering, RepeatMode, PrimitiveFlags};
 use api::{PremultipliedColorF, PropertyBinding, Shadow, GradientStop};
 use api::{BoxShadowClipMode, LineStyle, LineOrientation, BorderStyle};
 use api::{PrimitiveKeyKind};
 use api::units::*;
 use crate::border::{get_max_scale_for_border, build_border_instances};
 use crate::border::BorderSegmentCacheKey;
 use crate::clip::{ClipStore};
@@ -715,17 +715,17 @@ impl intern::InternDebug for PrimitiveKe
 
 /// The shared information for a given primitive. This is interned and retained
 /// both across frames and display lists, by comparing the matching PrimitiveKey.
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 #[derive(MallocSizeOf)]
 pub enum PrimitiveTemplateKind {
     Rectangle {
-        color: ColorF,
+        color: PropertyBinding<ColorF>,
     },
     Clear,
 }
 
 /// Construct the primitive template data from a primitive key. This
 /// is invoked when a primitive key is created and the interner
 /// doesn't currently contain a primitive with this key.
 impl From<PrimitiveKeyKind> for PrimitiveTemplateKind {
@@ -807,49 +807,51 @@ impl From<PrimitiveKey> for PrimitiveTem
         }
     }
 }
 
 impl PrimitiveTemplateKind {
     /// Write any GPU blocks for the primitive template to the given request object.
     fn write_prim_gpu_blocks(
         &self,
-        request: &mut GpuDataRequest
+        request: &mut GpuDataRequest,
+        scene_properties: &SceneProperties,
     ) {
         match *self {
             PrimitiveTemplateKind::Clear => {
                 // Opaque black with operator dest out
                 request.push(PremultipliedColorF::BLACK);
             }
             PrimitiveTemplateKind::Rectangle { ref color, .. } => {
-                request.push(color.premultiplied());
+                request.push(scene_properties.resolve_color(color).premultiplied())
             }
         }
     }
 }
 
 impl PrimitiveTemplate {
     /// Update the GPU cache for a given primitive template. This may be called multiple
     /// times per frame, by each primitive reference that refers to this interned
     /// template. The initial request call to the GPU cache ensures that work is only
     /// done if the cache entry is invalid (due to first use or eviction).
     pub fn update(
         &mut self,
         frame_state: &mut FrameBuildingState,
+        scene_properties: &SceneProperties,
     ) {
         if let Some(mut request) = frame_state.gpu_cache.request(&mut self.common.gpu_cache_handle) {
-            self.kind.write_prim_gpu_blocks(&mut request);
+            self.kind.write_prim_gpu_blocks(&mut request, scene_properties);
         }
 
         self.opacity = match self.kind {
             PrimitiveTemplateKind::Clear => {
                 PrimitiveOpacity::translucent()
             }
             PrimitiveTemplateKind::Rectangle { ref color, .. } => {
-                PrimitiveOpacity::from_alpha(color.a)
+                PrimitiveOpacity::from_alpha(scene_properties.resolve_color(color).a)
             }
         };
     }
 }
 
 type PrimitiveDataHandle = intern::Handle<PrimitiveKeyKind>;
 
 impl intern::Internable for PrimitiveKeyKind {
@@ -868,30 +870,37 @@ impl InternablePrimitive for PrimitiveKe
             info.rect.size,
             self,
         )
     }
 
     fn make_instance_kind(
         key: PrimitiveKey,
         data_handle: PrimitiveDataHandle,
-        _: &mut PrimitiveStore,
+        prim_store: &mut PrimitiveStore,
         _reference_frame_relative_offset: LayoutVector2D,
     ) -> PrimitiveInstanceKind {
         match key.kind {
             PrimitiveKeyKind::Clear => {
                 PrimitiveInstanceKind::Clear {
                     data_handle
                 }
             }
-            PrimitiveKeyKind::Rectangle { .. } => {
+            PrimitiveKeyKind::Rectangle { color, .. } => {
+                let color_binding_index = match color {
+                    PropertyBinding::Binding(..) => {
+                        prim_store.color_bindings.push(color)
+                    }
+                    PropertyBinding::Value(..) => ColorBindingIndex::INVALID,
+                };
                 PrimitiveInstanceKind::Rectangle {
                     data_handle,
                     opacity_binding_index: OpacityBindingIndex::INVALID,
                     segment_instance_index: SegmentInstanceIndex::INVALID,
+                    color_binding_index,
                 }
             }
         }
     }
 }
 
 // Maintains a list of opacity bindings that have been collapsed into
 // the color of a single primitive. This is an important optimization
@@ -1321,34 +1330,37 @@ impl IsVisible for PrimitiveKeyKind {
     //           we should move the logic for all other
     //           primitive types to use this.
     fn is_visible(&self) -> bool {
         match *self {
             PrimitiveKeyKind::Clear => {
                 true
             }
             PrimitiveKeyKind::Rectangle { ref color, .. } => {
-                color.a > 0
+                match *color {
+                    PropertyBinding::Value(value) => value.a > 0,
+                    PropertyBinding::Binding(..) => true,
+                }
             }
         }
     }
 }
 
 impl CreateShadow for PrimitiveKeyKind {
     // Create a clone of this PrimitiveContainer, applying whatever
     // changes are necessary to the primitive to support rendering
     // it as part of the supplied shadow.
     fn create_shadow(
         &self,
         shadow: &Shadow,
     ) -> PrimitiveKeyKind {
         match *self {
             PrimitiveKeyKind::Rectangle { .. } => {
                 PrimitiveKeyKind::Rectangle {
-                    color: shadow.color.into(),
+                    color: PropertyBinding::Value(shadow.color.into()),
                 }
             }
             PrimitiveKeyKind::Clear => {
                 panic!("bug: this prim is not supported in shadow contexts");
             }
         }
     }
 }
@@ -1399,16 +1411,17 @@ pub enum PrimitiveInstanceKind {
         /// Handle to the common interned data for this primitive.
         data_handle: ImageBorderDataHandle,
     },
     Rectangle {
         /// Handle to the common interned data for this primitive.
         data_handle: PrimitiveDataHandle,
         opacity_binding_index: OpacityBindingIndex,
         segment_instance_index: SegmentInstanceIndex,
+        color_binding_index: ColorBindingIndex,
     },
     YuvImage {
         /// Handle to the common interned data for this primitive.
         data_handle: YuvImageDataHandle,
         segment_instance_index: SegmentInstanceIndex,
         is_compositor_surface: bool,
     },
     Image {
@@ -1655,16 +1668,18 @@ pub struct SegmentedInstance {
     pub segments_range: SegmentsRange,
 }
 
 pub type GlyphKeyStorage = storage::Storage<GlyphKey>;
 pub type TextRunIndex = storage::Index<TextRunPrimitive>;
 pub type TextRunStorage = storage::Storage<TextRunPrimitive>;
 pub type OpacityBindingIndex = storage::Index<OpacityBinding>;
 pub type OpacityBindingStorage = storage::Storage<OpacityBinding>;
+pub type ColorBindingIndex = storage::Index<PropertyBinding<ColorU>>;
+pub type ColorBindingStorage = storage::Storage<PropertyBinding<ColorU>>;
 pub type BorderHandleStorage = storage::Storage<RenderTaskCacheEntryHandle>;
 pub type SegmentStorage = storage::Storage<BrushSegment>;
 pub type SegmentsRange = storage::Range<BrushSegment>;
 pub type SegmentInstanceStorage = storage::Storage<SegmentedInstance>;
 pub type SegmentInstanceIndex = storage::Index<SegmentedInstance>;
 pub type ImageInstanceStorage = storage::Storage<ImageInstance>;
 pub type ImageInstanceIndex = storage::Index<ImageInstance>;
 pub type GradientTileStorage = storage::Storage<VisibleGradientTile>;
@@ -1794,26 +1809,28 @@ impl PrimitiveScratchBuffer {
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 #[derive(Clone, Debug)]
 pub struct PrimitiveStoreStats {
     picture_count: usize,
     text_run_count: usize,
     opacity_binding_count: usize,
     image_count: usize,
     linear_gradient_count: usize,
+    color_binding_count: usize,
 }
 
 impl PrimitiveStoreStats {
     pub fn empty() -> Self {
         PrimitiveStoreStats {
             picture_count: 0,
             text_run_count: 0,
             opacity_binding_count: 0,
             image_count: 0,
             linear_gradient_count: 0,
+            color_binding_count: 0,
         }
     }
 }
 
 #[cfg_attr(feature = "capture", derive(Serialize))]
 pub struct PrimitiveStore {
     pub pictures: Vec<PicturePrimitive>,
     pub text_runs: TextRunStorage,
@@ -1821,36 +1838,40 @@ pub struct PrimitiveStore {
 
     /// A list of image instances. These are stored separately as
     /// storing them inline in the instance makes the structure bigger
     /// for other types.
     pub images: ImageInstanceStorage,
 
     /// List of animated opacity bindings for a primitive.
     pub opacity_bindings: OpacityBindingStorage,
+    /// animated color bindings for this primitive.
+    pub color_bindings: ColorBindingStorage,
 }
 
 impl PrimitiveStore {
     pub fn new(stats: &PrimitiveStoreStats) -> PrimitiveStore {
         PrimitiveStore {
             pictures: Vec::with_capacity(stats.picture_count),
             text_runs: TextRunStorage::new(stats.text_run_count),
             images: ImageInstanceStorage::new(stats.image_count),
             opacity_bindings: OpacityBindingStorage::new(stats.opacity_binding_count),
+            color_bindings: ColorBindingStorage::new(stats.color_binding_count),
             linear_gradients: LinearGradientStorage::new(stats.linear_gradient_count),
         }
     }
 
     pub fn get_stats(&self) -> PrimitiveStoreStats {
         PrimitiveStoreStats {
             picture_count: self.pictures.len(),
             text_run_count: self.text_runs.len(),
             image_count: self.images.len(),
             opacity_binding_count: self.opacity_bindings.len(),
             linear_gradient_count: self.linear_gradients.len(),
+            color_binding_count: self.color_bindings.len(),
         }
     }
 
     #[allow(unused)]
     pub fn print_picture_tree(&self, root: PictureIndex) {
         use crate::print_tree::PrintTree;
         let mut pt = PrintTree::new("picture tree");
         self.pictures[root.0].print(&self.pictures, root, &mut pt);
@@ -2145,16 +2166,17 @@ impl PrimitiveStore {
                             clip_chain.as_ref(),
                             prim_local_rect,
                             frame_context,
                             frame_state.data_stores,
                             frame_state.clip_store,
                             &self.pictures,
                             frame_state.resource_cache,
                             &self.opacity_bindings,
+                            &self.color_bindings,
                             &self.images,
                             &frame_state.surface_stack,
                             &frame_state.composite_state,
                         ) {
                             prim_instance.visibility_info = PrimitiveVisibilityIndex::INVALID;
                             // Ensure the primitive clip is popped - perhaps we can use
                             // some kind of scope to do this automatically in future.
                             frame_state.clip_chain_stack.pop_clip();
@@ -2970,17 +2992,17 @@ impl PrimitiveStore {
             }
             PrimitiveInstanceKind::Clear { data_handle, .. } => {
                 let prim_data = &mut data_stores.prim[*data_handle];
 
                 prim_data.common.may_need_repetition = false;
 
                 // Update the template this instane references, which may refresh the GPU
                 // cache with any shared template data.
-                prim_data.update(frame_state);
+                prim_data.update(frame_state, frame_context.scene_properties);
             }
             PrimitiveInstanceKind::NormalBorder { data_handle, ref mut cache_handles, .. } => {
                 let prim_data = &mut data_stores.normal_border[*data_handle];
                 let common_data = &mut prim_data.common;
                 let border_data = &mut prim_data.kind;
 
                 common_data.may_need_repetition =
                     matches!(border_data.border.top.style, BorderStyle::Dotted | BorderStyle::Dashed) ||
@@ -3060,38 +3082,63 @@ impl PrimitiveStore {
 
                 // TODO: get access to the ninepatch and to check whwther we need support
                 // for repetitions in the shader.
 
                 // Update the template this instane references, which may refresh the GPU
                 // cache with any shared template data.
                 prim_data.kind.update(&mut prim_data.common, frame_state);
             }
-            PrimitiveInstanceKind::Rectangle { data_handle, segment_instance_index, opacity_binding_index, .. } => {
+            PrimitiveInstanceKind::Rectangle { data_handle, segment_instance_index, opacity_binding_index, color_binding_index, .. } => {
                 let prim_data = &mut data_stores.prim[*data_handle];
                 prim_data.common.may_need_repetition = false;
 
+                if *color_binding_index != ColorBindingIndex::INVALID {
+                    match self.color_bindings[*color_binding_index] {
+                        PropertyBinding::Binding(..) => {
+                            // We explicitly invalidate the gpu cache
+                            // if the color is animating.
+                            let gpu_cache_handle =
+                                if *segment_instance_index == SegmentInstanceIndex::INVALID {
+                                    None
+                                } else if *segment_instance_index == SegmentInstanceIndex::UNUSED {
+                                    Some(&prim_data.common.gpu_cache_handle)
+                                } else {
+                                    Some(&scratch.segment_instances[*segment_instance_index].gpu_cache_handle)
+                                };
+                            if let Some(gpu_cache_handle) = gpu_cache_handle {
+                                frame_state.gpu_cache.invalidate(gpu_cache_handle);
+                            }
+                        }
+                        PropertyBinding::Value(..) => {},
+                    }
+                }
+
                 // Update the template this instane references, which may refresh the GPU
                 // cache with any shared template data.
-                prim_data.update(frame_state);
+                prim_data.update(
+                    frame_state,
+                    frame_context.scene_properties,
+                );
 
                 update_opacity_binding(
                     &mut self.opacity_bindings,
                     *opacity_binding_index,
                     frame_context.scene_properties,
                 );
 
                 write_segment(
                     *segment_instance_index,
                     frame_state,
                     &mut scratch.segments,
                     &mut scratch.segment_instances,
                     |request| {
                         prim_data.kind.write_prim_gpu_blocks(
                             request,
+                            frame_context.scene_properties,
                         );
                     }
                 );
             }
             PrimitiveInstanceKind::YuvImage { data_handle, segment_instance_index, .. } => {
                 let prim_data = &mut data_stores.yuv_image[*data_handle];
                 let common_data = &mut prim_data.common;
                 let yuv_image_data = &mut prim_data.kind;
@@ -4337,13 +4384,13 @@ fn test_struct_sizes() {
     // The sizes of these structures are critical for performance on a number of
     // talos stress tests. If you get a failure here on CI, there's two possibilities:
     // (a) You made a structure smaller than it currently is. Great work! Update the
     //     test expectations and move on.
     // (b) You made a structure larger. This is not necessarily a problem, but should only
     //     be done with care, and after checking if talos performance regresses badly.
     assert_eq!(mem::size_of::<PrimitiveInstance>(), 88, "PrimitiveInstance size changed");
     assert_eq!(mem::size_of::<PrimitiveInstanceKind>(), 40, "PrimitiveInstanceKind size changed");
-    assert_eq!(mem::size_of::<PrimitiveTemplate>(), 40, "PrimitiveTemplate size changed");
-    assert_eq!(mem::size_of::<PrimitiveTemplateKind>(), 20, "PrimitiveTemplateKind size changed");
-    assert_eq!(mem::size_of::<PrimitiveKey>(), 20, "PrimitiveKey size changed");
-    assert_eq!(mem::size_of::<PrimitiveKeyKind>(), 5, "PrimitiveKeyKind size changed");
+    assert_eq!(mem::size_of::<PrimitiveTemplate>(), 48, "PrimitiveTemplate size changed");
+    assert_eq!(mem::size_of::<PrimitiveTemplateKind>(), 28, "PrimitiveTemplateKind size changed");
+    assert_eq!(mem::size_of::<PrimitiveKey>(), 28, "PrimitiveKey size changed");
+    assert_eq!(mem::size_of::<PrimitiveKeyKind>(), 16, "PrimitiveKeyKind size changed");
 }
--- a/gfx/wr/webrender/src/scene.rs
+++ b/gfx/wr/webrender/src/scene.rs
@@ -17,25 +17,27 @@ use std::sync::Arc;
 /// Stores a map of the animated property bindings for the current display list. These
 /// can be used to animate the transform and/or opacity of a display list without
 /// re-submitting the display list itself.
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct SceneProperties {
     transform_properties: FastHashMap<PropertyBindingId, LayoutTransform>,
     float_properties: FastHashMap<PropertyBindingId, f32>,
+    color_properties: FastHashMap<PropertyBindingId, ColorF>,
     current_properties: DynamicProperties,
     pending_properties: Option<DynamicProperties>,
 }
 
 impl SceneProperties {
     pub fn new() -> Self {
         SceneProperties {
             transform_properties: FastHashMap::default(),
             float_properties: FastHashMap::default(),
+            color_properties: FastHashMap::default(),
             current_properties: DynamicProperties::default(),
             pending_properties: None,
         }
     }
 
     /// Set the current property list for this display list.
     pub fn set_properties(&mut self, properties: DynamicProperties) {
         self.pending_properties = Some(properties);
@@ -73,16 +75,21 @@ impl SceneProperties {
                         .insert(property.key.id, property.value);
                 }
 
                 for property in &pending_properties.floats {
                     self.float_properties
                         .insert(property.key.id, property.value);
                 }
 
+                for property in &pending_properties.colors {
+                    self.color_properties
+                        .insert(property.key.id, property.value);
+                }
+
                 self.current_properties = pending_properties.clone();
                 properties_changed = true;
             }
         }
 
         properties_changed
     }
 
@@ -116,16 +123,37 @@ impl SceneProperties {
                     .unwrap_or(v)
             }
         }
     }
 
     pub fn float_properties(&self) -> &FastHashMap<PropertyBindingId, f32> {
         &self.float_properties
     }
+
+    /// Get the current value for a color property.
+    pub fn resolve_color(
+        &self,
+        property: &PropertyBinding<ColorF>
+    ) -> ColorF {
+        match *property {
+            PropertyBinding::Value(value) => value,
+            PropertyBinding::Binding(ref key, v) => {
+                self.color_properties
+                    .get(&key.id)
+                    .cloned()
+                    .unwrap_or(v)
+            }
+        }
+    }
+
+    pub fn color_properties(&self) -> &FastHashMap<PropertyBindingId, ColorF> {
+        &self.color_properties
+    }
+
 }
 
 /// A representation of the layout within the display port for a given document or iframe.
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 #[derive(Clone)]
 pub struct ScenePipeline {
     pub pipeline_id: PipelineId,
--- a/gfx/wr/webrender/src/scene_building.rs
+++ b/gfx/wr/webrender/src/scene_building.rs
@@ -1209,17 +1209,17 @@ impl<'a> SceneBuilder<'a> {
                     &info.common,
                     None,
                     apply_pipeline_clip,
                 );
 
                 self.add_solid_rectangle(
                     clip_and_scroll,
                     &layout,
-                    ColorF::TRANSPARENT,
+                    PropertyBinding::Value(ColorF::TRANSPARENT),
                 );
             }
             DisplayItem::ClearRectangle(ref info) => {
                 let (layout, _, clip_and_scroll) = self.process_common_properties(
                     &info.common,
                     None,
                     apply_pipeline_clip,
                 );
@@ -2744,23 +2744,29 @@ impl<'a> SceneBuilder<'a> {
         _prim_instance: &PrimitiveInstance,
     ) {
     }
 
     pub fn add_solid_rectangle(
         &mut self,
         clip_and_scroll: ScrollNodeAndClipChain,
         info: &LayoutPrimitiveInfo,
-        color: ColorF,
+        color: PropertyBinding<ColorF>,
     ) {
-        if color.a == 0.0 {
-            // Don't add transparent rectangles to the draw list, but do consider them for hit
-            // testing. This allows specifying invisible hit testing areas.
-            self.add_primitive_to_hit_testing_list(info, clip_and_scroll);
-            return;
+        match color {
+            PropertyBinding::Value(value) => {
+                if value.a == 0.0 {
+                    // Don't add transparent rectangles to the draw list,
+                    // but do consider them for hit testing. This allows
+                    // specifying invisible hit testing areas.
+                    self.add_primitive_to_hit_testing_list(info, clip_and_scroll);
+                    return;
+                }
+            },
+            PropertyBinding::Binding(..) => {},
         }
 
         self.add_primitive(
             clip_and_scroll,
             info,
             Vec::new(),
             PrimitiveKeyKind::Rectangle {
                 color: color.into(),
--- a/gfx/wr/webrender_api/src/api.rs
+++ b/gfx/wr/webrender_api/src/api.rs
@@ -1149,17 +1149,17 @@ pub enum FilterDataIntern {}
 /// uniquely identifies a primitive template by key.
 #[derive(Debug, Clone, Eq, MallocSizeOf, PartialEq, Hash, Serialize, Deserialize)]
 pub enum PrimitiveKeyKind {
     /// Clear an existing rect, used for special effects on some platforms.
     Clear,
     ///
     Rectangle {
         ///
-        color: ColorU,
+        color: PropertyBinding<ColorU>,
     },
 }
 
 /// Meta-macro to enumerate the various interner identifiers and types.
 ///
 /// IMPORTANT: Keep this synchronized with the list in mozilla-central located at
 /// gfx/webrender_bindings/webrender_ffi.h
 ///
@@ -1816,17 +1816,17 @@ impl PropertyBindingId {
             uid: value as u32,
         }
     }
 }
 
 /// A unique key that is used for connecting animated property
 /// values to bindings in the display list.
 #[repr(C)]
-#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
+#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize, PeekPoke)]
 pub struct PropertyBindingKey<T> {
     ///
     pub id: PropertyBindingId,
     _phantom: PhantomData<T>,
 }
 
 /// Construct a property value from a given key and value.
 impl<T: Copy> PropertyBindingKey<T> {
@@ -1848,17 +1848,17 @@ impl<T> PropertyBindingKey<T> {
 
 /// A binding property can either be a specific value
 /// (the normal, non-animated case) or point to a binding location
 /// to fetch the current value from.
 /// Note that Binding has also a non-animated value, the value is
 /// used for the case where the animation is still in-delay phase
 /// (i.e. the animation doesn't produce any animation values).
 #[repr(C)]
-#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize, PeekPoke)]
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize, PeekPoke)]
 pub enum PropertyBinding<T> {
     /// Non-animated value.
     Value(T),
     /// Animated binding.
     Binding(PropertyBindingKey<T>, T),
 }
 
 impl<T: Default> Default for PropertyBinding<T> {
@@ -1868,16 +1868,56 @@ impl<T: Default> Default for PropertyBin
 }
 
 impl<T> From<T> for PropertyBinding<T> {
     fn from(value: T) -> PropertyBinding<T> {
         PropertyBinding::Value(value)
     }
 }
 
+impl From<PropertyBindingKey<ColorF>> for PropertyBindingKey<ColorU> {
+    fn from(key: PropertyBindingKey<ColorF>) -> PropertyBindingKey<ColorU> {
+        PropertyBindingKey {
+            id: key.id.clone(),
+            _phantom: PhantomData,
+        }
+    }
+}
+
+impl From<PropertyBindingKey<ColorU>> for PropertyBindingKey<ColorF> {
+    fn from(key: PropertyBindingKey<ColorU>) -> PropertyBindingKey<ColorF> {
+        PropertyBindingKey {
+            id: key.id.clone(),
+            _phantom: PhantomData,
+        }
+    }
+}
+
+impl From<PropertyBinding<ColorF>> for PropertyBinding<ColorU> {
+    fn from(value: PropertyBinding<ColorF>) -> PropertyBinding<ColorU> {
+        match value {
+            PropertyBinding::Value(value) => PropertyBinding::Value(value.into()),
+            PropertyBinding::Binding(k, v) => {
+                PropertyBinding::Binding(k.into(), v.into())
+            }
+        }
+    }
+}
+
+impl From<PropertyBinding<ColorU>> for PropertyBinding<ColorF> {
+    fn from(value: PropertyBinding<ColorU>) -> PropertyBinding<ColorF> {
+        match value {
+            PropertyBinding::Value(value) => PropertyBinding::Value(value.into()),
+            PropertyBinding::Binding(k, v) => {
+                PropertyBinding::Binding(k.into(), v.into())
+            }
+        }
+    }
+}
+
 /// The current value of an animated property. This is
 /// supplied by the calling code.
 #[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
 pub struct PropertyValue<T> {
     ///
     pub key: PropertyBindingKey<T>,
     ///
     pub value: T,
@@ -1885,18 +1925,20 @@ pub struct PropertyValue<T> {
 
 /// When using `generate_frame()`, a list of `PropertyValue` structures
 /// can optionally be supplied to provide the current value of any
 /// animated properties.
 #[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Default)]
 pub struct DynamicProperties {
     ///
     pub transforms: Vec<PropertyValue<LayoutTransform>>,
-    ///
+    /// opacity
     pub floats: Vec<PropertyValue<f32>>,
+    /// background color
+    pub colors: Vec<PropertyValue<ColorF>>,
 }
 
 /// A handler to integrate WebRender with the thread that contains the `Renderer`.
 pub trait RenderNotifier: Send {
     ///
     fn clone(&self) -> Box<dyn RenderNotifier>;
     /// Wake the thread containing the `Renderer` up (after updates have been put
     /// in the renderer's queue).
--- a/gfx/wr/webrender_api/src/display_item.rs
+++ b/gfx/wr/webrender_api/src/display_item.rs
@@ -294,21 +294,21 @@ pub struct ScrollFrameDisplayItem {
     /// The amount this scrollframe has already been scrolled by, in the caller.
     /// This means that all the display items that are inside the scrollframe
     /// will have their coordinates shifted by this amount, and this offset
     /// should be added to those display item coordinates in order to get a
     /// normalized value that is consistent across display lists.
     pub external_scroll_offset: LayoutVector2D,
 }
 
-/// A solid color to draw (may not actually be a rectangle due to complex clips)
+/// A solid or an animating color to draw (may not actually be a rectangle due to complex clips)
 #[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
 pub struct RectangleDisplayItem {
     pub common: CommonItemProperties,
-    pub color: ColorF,
+    pub color: PropertyBinding<ColorF>,
 }
 
 /// Clears all colors from the area, making it possible to cut holes in the window.
 /// (useful for things like the macos frosted-glass effect).
 #[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, PeekPoke)]
 pub struct ClearRectangleDisplayItem {
     pub common: CommonItemProperties,
 }
--- a/gfx/wr/webrender_api/src/display_list.rs
+++ b/gfx/wr/webrender_api/src/display_list.rs
@@ -1111,17 +1111,29 @@ impl DisplayListBuilder {
 
     pub fn push_rect(
         &mut self,
         common: &di::CommonItemProperties,
         color: ColorF,
     ) {
         let item = di::DisplayItem::Rectangle(di::RectangleDisplayItem {
             common: *common,
-            color
+            color: PropertyBinding::Value(color),
+        });
+        self.push_item(&item);
+    }
+
+    pub fn push_rect_with_animation(
+        &mut self,
+        common: &di::CommonItemProperties,
+        color: PropertyBinding<ColorF>,
+    ) {
+        let item = di::DisplayItem::Rectangle(di::RectangleDisplayItem {
+            common: *common,
+            color,
         });
         self.push_item(&item);
     }
 
     pub fn push_clear_rect(
         &mut self,
         common: &di::CommonItemProperties,
     ) {
--- a/gfx/wr/wrench/src/scene.rs
+++ b/gfx/wr/wrench/src/scene.rs
@@ -9,33 +9,40 @@ use webrender::api::units::{LayoutSize, 
 
 /// Stores a map of the animated property bindings for the current display list. These
 /// can be used to animate the transform and/or opacity of a display list without
 /// re-submitting the display list itself.
 #[derive(Default)]
 pub struct SceneProperties {
     transform_properties: HashMap<PropertyBindingId, LayoutTransform>,
     float_properties: HashMap<PropertyBindingId, f32>,
+    color_properties: HashMap<PropertyBindingId, ColorF>,
 }
 
 impl SceneProperties {
     /// Set the current property list for this display list.
     pub fn set_properties(&mut self, properties: &DynamicProperties) {
         self.transform_properties.clear();
         self.float_properties.clear();
+        self.color_properties.clear();
 
         for property in &properties.transforms {
             self.transform_properties
                 .insert(property.key.id, property.value);
         }
 
         for property in &properties.floats {
             self.float_properties
                 .insert(property.key.id, property.value);
         }
+
+        for property in &properties.colors {
+            self.color_properties
+                .insert(property.key.id, property.value);
+        }
     }
 
     /// Get the current value for a transform property.
     pub fn resolve_layout_transform(
         &self,
         property: &PropertyBinding<LayoutTransform>,
     ) -> LayoutTransform {
         match *property {
@@ -52,16 +59,27 @@ impl SceneProperties {
         match *property {
             PropertyBinding::Value(value) => value,
             PropertyBinding::Binding(ref key, v) => self.float_properties
                 .get(&key.id)
                 .cloned()
                 .unwrap_or(v),
         }
     }
+
+    /// Get the current value for a color property.
+    pub fn resolve_color(&self, property: &PropertyBinding<ColorF>) -> ColorF {
+        match *property {
+            PropertyBinding::Value(value) => value,
+            PropertyBinding::Binding(ref key, v) => self.color_properties
+                .get(&key.id)
+                .cloned()
+                .unwrap_or(v),
+        }
+    }
 }
 
 /// A representation of the layout within the display port for a given document or iframe.
 #[derive(Debug)]
 pub struct ScenePipeline {
     pub epoch: Epoch,
     pub viewport_size: LayoutSize,
     pub background_color: Option<ColorF>,
--- a/gfx/wr/wrench/src/yaml_frame_writer.rs
+++ b/gfx/wr/wrench/src/yaml_frame_writer.rs
@@ -997,17 +997,23 @@ impl YamlFrameWriter {
                 None => break,
             };
 
             let mut v = new_table();
             match *base.item() {
                 DisplayItem::Rectangle(item) => {
                     str_node(&mut v, "type", "rect");
                     common_node(&mut v, clip_id_mapper, &item.common);
-                    color_node(&mut v, "color", item.color);
+
+                    let key_label = match item.color {
+                        PropertyBinding::Value(..) => "color",
+                        PropertyBinding::Binding(..) => "animating-color",
+                    };
+                    color_node(&mut v, key_label,
+                               scene.properties.resolve_color(&item.color));
                 }
                 DisplayItem::HitTest(item) => {
                     str_node(&mut v, "type", "hit-test");
                     common_node(&mut v, clip_id_mapper, &item.common);
                 }
                 DisplayItem::ClearRectangle(item) => {
                     str_node(&mut v, "type", "clear-rect");
                     common_node(&mut v, clip_id_mapper, &item.common);
--- a/layout/painting/nsDisplayList.cpp
+++ b/layout/painting/nsDisplayList.cpp
@@ -5136,22 +5136,38 @@ bool nsDisplayBackgroundColor::CreateWeb
   if (mColor == Color()) {
     return true;
   }
 
   if (HasBackgroundClipText()) {
     return false;
   }
 
+  uint64_t animationsId = 0;
+  // We don't support background-color animations on table elements yet.
+  if (GetType() == DisplayItemType::TYPE_BACKGROUND_COLOR) {
+    animationsId = AddAnimationsForWebRender(
+        this, aManager, aDisplayListBuilder, aBuilder.GetRenderRoot());
+  }
+
   LayoutDeviceRect bounds = LayoutDeviceRect::FromAppUnits(
       mBackgroundRect, mFrame->PresContext()->AppUnitsPerDevPixel());
   wr::LayoutRect r = wr::ToLayoutRect(bounds);
 
-  aBuilder.PushRect(r, r, !BackfaceIsHidden(),
-                    wr::ToColorF(ToDeviceColor(mColor)));
+  if (animationsId) {
+    wr::WrAnimationProperty prop{
+        wr::WrAnimationType::BackgroundColor,
+        animationsId,
+    };
+    aBuilder.PushRectWithAnimation(r, r, !BackfaceIsHidden(),
+                                   wr::ToColorF(ToDeviceColor(mColor)), &prop);
+  } else {
+    aBuilder.PushRect(r, r, !BackfaceIsHidden(),
+                      wr::ToColorF(ToDeviceColor(mColor)));
+  }
 
   return true;
 }
 
 void nsDisplayBackgroundColor::PaintWithClip(nsDisplayListBuilder* aBuilder,
                                              gfxContext* aCtx,
                                              const DisplayItemClip& aClip) {
   MOZ_ASSERT(!HasBackgroundClipText());