Bug 1428766 - Update webrender to commit 722e8b104bf99d29d61ecb1fe456eebe89ad81fd. r?jrmuizel draft
authorKartikaya Gupta <kgupta@mozilla.com>
Thu, 11 Jan 2018 10:53:24 -0500
changeset 719197 92cb6ec2ee0ffa0b1a833e1aa1ea39559779bc69
parent 719102 c4e4613dbe32bb218957a140e5d0bd4fe7d1e98c
child 719198 a2d537fb17d2d21d72f2ef79989e10aa4a7634f5
push id95168
push userkgupta@mozilla.com
push dateThu, 11 Jan 2018 15:56:28 +0000
reviewersjrmuizel
bugs1428766
milestone59.0a1
Bug 1428766 - Update webrender to commit 722e8b104bf99d29d61ecb1fe456eebe89ad81fd. r?jrmuizel MozReview-Commit-ID: GsDbUIfQFlm
gfx/doc/README.webrender
gfx/webrender/src/batch.rs
gfx/webrender/src/picture.rs
gfx/webrender/src/prim_store.rs
gfx/webrender/src/record.rs
gfx/webrender/src/render_backend.rs
gfx/webrender/src/renderer.rs
gfx/webrender_api/src/api.rs
--- a/gfx/doc/README.webrender
+++ b/gfx/doc/README.webrender
@@ -170,9 +170,9 @@ 2. Sometimes autoland tip has changed en
    has an env var you can set to do this). In theory you can get the same
    result by resolving the conflict manually but Cargo.lock files are usually not
    trivial to merge by hand. If it's just the third_party/rust dir that has conflicts
    you can delete it and run |mach vendor rust| again to repopulate it.
 
 -------------------------------------------------------------------------------
 
 The version of WebRender currently in the tree is:
-a422f907be948b92bf5c7003a01f7744391a795e
+722e8b104bf99d29d61ecb1fe456eebe89ad81fd
--- a/gfx/webrender/src/batch.rs
+++ b/gfx/webrender/src/batch.rs
@@ -577,71 +577,31 @@ impl AlphaBatcher {
             z,
         );
 
         let blend_mode = ctx.prim_store.get_blend_mode(prim_metadata, transform_kind);
 
         match prim_metadata.prim_kind {
             PrimitiveKind::Brush => {
                 let brush = &ctx.prim_store.cpu_brushes[prim_metadata.cpu_prim_index.0];
-                let base_instance = BrushInstance {
-                    picture_address: task_address,
-                    prim_address: prim_cache_address,
+
+                self.add_brush_to_batch(
+                    brush,
+                    prim_metadata,
+                    blend_mode,
                     clip_chain_rect_index,
+                    clip_task_address,
+                    item_bounding_rect,
+                    prim_cache_address,
                     scroll_id,
-                    clip_task_address,
+                    task_address,
+                    transform_kind,
                     z,
-                    segment_index: 0,
-                    user_data0: 0,
-                    user_data1: 0,
-                };
-
-                match brush.segment_desc {
-                    Some(ref segment_desc) => {
-                        let opaque_batch = self.batch_list.opaque_batch_list.get_suitable_batch(
-                            brush.get_batch_key(
-                                BlendMode::None
-                            ),
-                            item_bounding_rect
-                        );
-                        let alpha_batch = self.batch_list.alpha_batch_list.get_suitable_batch(
-                            brush.get_batch_key(
-                                BlendMode::PremultipliedAlpha
-                            ),
-                            item_bounding_rect
-                        );
-
-                        for (i, segment) in segment_desc.segments.iter().enumerate() {
-                            let is_inner = segment.edge_flags.is_empty();
-                            let needs_blending = !prim_metadata.opacity.is_opaque ||
-                                                 segment.clip_task_id.is_some() ||
-                                                 (!is_inner && transform_kind == TransformedRectKind::Complex);
-
-                            let clip_task_address = segment
-                                .clip_task_id
-                                .map_or(OPAQUE_TASK_ADDRESS, |id| render_tasks.get_task_address(id));
-
-                            let instance = PrimitiveInstance::from(BrushInstance {
-                                segment_index: i as i32,
-                                clip_task_address,
-                                ..base_instance
-                            });
-
-                            if needs_blending {
-                                alpha_batch.push(instance);
-                            } else {
-                                opaque_batch.push(instance);
-                            }
-                        }
-                    }
-                    None => {
-                        let batch = self.batch_list.get_suitable_batch(brush.get_batch_key(blend_mode), item_bounding_rect);
-                        batch.push(PrimitiveInstance::from(base_instance));
-                    }
-                }
+                    render_tasks,
+                );
             }
             PrimitiveKind::Border => {
                 let border_cpu =
                     &ctx.prim_store.cpu_borders[prim_metadata.cpu_prim_index.0];
                 // TODO(gw): Select correct blend mode for edges and corners!!
                 let corner_kind = BatchKind::Transformable(
                     transform_kind,
                     TransformBatchKind::BorderCorner,
@@ -1015,26 +975,30 @@ impl AlphaBatcher {
                                                     ],
                                                 };
                                                 let key = BatchKey::new(
                                                     BatchKind::HardwareComposite,
                                                     BlendMode::PremultipliedAlpha,
                                                     secondary_textures,
                                                 );
                                                 let batch = self.batch_list.get_suitable_batch(key, &item_bounding_rect);
-                                                let device_offset = (offset * LayerToWorldScale::new(1.0) * ctx.device_pixel_scale).round().to_i32();
+                                                let content_rect = prim_metadata.local_rect.translate(&-offset);
+                                                let rect =
+                                                    (content_rect * LayerToWorldScale::new(1.0) * ctx.device_pixel_scale).round()
+                                                                                                                         .to_i32();
+
                                                 let instance = CompositePrimitiveInstance::new(
                                                     task_address,
                                                     secondary_task_address,
                                                     RenderTaskAddress(0),
-                                                    item_bounding_rect.origin.x - device_offset.x,
-                                                    item_bounding_rect.origin.y - device_offset.y,
+                                                    rect.origin.x,
+                                                    rect.origin.y,
                                                     z,
-                                                    item_bounding_rect.size.width,
-                                                    item_bounding_rect.size.height,
+                                                    rect.size.width,
+                                                    rect.size.height,
                                                 );
 
                                                 batch.push(PrimitiveInstance::from(instance));
                                             }
                                             _ => {
                                                 let key = BatchKey::new(
                                                     BatchKind::Blend,
                                                     BlendMode::PremultipliedAlpha,
@@ -1251,16 +1215,88 @@ impl AlphaBatcher {
                 batch.push(base_instance.build(
                     uv_rect_addresses[0],
                     uv_rect_addresses[1],
                     uv_rect_addresses[2],
                 ));
             }
         }
     }
+
+    fn add_brush_to_batch(
+        &mut self,
+        brush: &BrushPrimitive,
+        prim_metadata: &PrimitiveMetadata,
+        blend_mode: BlendMode,
+        clip_chain_rect_index: ClipChainRectIndex,
+        clip_task_address: RenderTaskAddress,
+        item_bounding_rect: &DeviceIntRect,
+        prim_cache_address: GpuCacheAddress,
+        scroll_id: ClipScrollNodeIndex,
+        task_address: RenderTaskAddress,
+        transform_kind: TransformedRectKind,
+        z: i32,
+        render_tasks: &RenderTaskTree,
+    ) {
+        let base_instance = BrushInstance {
+            picture_address: task_address,
+            prim_address: prim_cache_address,
+            clip_chain_rect_index,
+            scroll_id,
+            clip_task_address,
+            z,
+            segment_index: 0,
+            user_data0: 0,
+            user_data1: 0,
+        };
+
+        match brush.segment_desc {
+            Some(ref segment_desc) => {
+                let opaque_batch = self.batch_list.opaque_batch_list.get_suitable_batch(
+                    brush.get_batch_key(
+                        BlendMode::None
+                    ),
+                    item_bounding_rect
+                );
+                let alpha_batch = self.batch_list.alpha_batch_list.get_suitable_batch(
+                    brush.get_batch_key(
+                        BlendMode::PremultipliedAlpha
+                    ),
+                    item_bounding_rect
+                );
+
+                for (i, segment) in segment_desc.segments.iter().enumerate() {
+                    let is_inner = segment.edge_flags.is_empty();
+                    let needs_blending = !prim_metadata.opacity.is_opaque ||
+                                         segment.clip_task_id.is_some() ||
+                                         (!is_inner && transform_kind == TransformedRectKind::Complex);
+
+                    let clip_task_address = segment
+                        .clip_task_id
+                        .map_or(OPAQUE_TASK_ADDRESS, |id| render_tasks.get_task_address(id));
+
+                    let instance = PrimitiveInstance::from(BrushInstance {
+                        segment_index: i as i32,
+                        clip_task_address,
+                        ..base_instance
+                    });
+
+                    if needs_blending {
+                        alpha_batch.push(instance);
+                    } else {
+                        opaque_batch.push(instance);
+                    }
+                }
+            }
+            None => {
+                let batch = self.batch_list.get_suitable_batch(brush.get_batch_key(blend_mode), item_bounding_rect);
+                batch.push(PrimitiveInstance::from(base_instance));
+            }
+        }
+    }
 }
 
 impl BrushPrimitive {
     fn get_batch_key(&self, blend_mode: BlendMode) -> BatchKey {
         match self.kind {
             BrushKind::Solid { .. } => {
                 BatchKey::new(
                     BatchKind::Brush(BrushBatchKind::Solid),
--- a/gfx/webrender/src/picture.rs
+++ b/gfx/webrender/src/picture.rs
@@ -295,16 +295,17 @@ impl PicturePrimitive {
     }
 
     pub fn prepare_for_render(
         &mut self,
         prim_index: PrimitiveIndex,
         prim_context: &PrimitiveContext,
         render_tasks: &mut RenderTaskTree,
         prim_screen_rect: &DeviceIntRect,
+        prim_local_rect: &LayerRect,
         child_tasks: Vec<RenderTaskId>,
         parent_tasks: &mut Vec<RenderTaskId>,
     ) {
         let content_scale = LayerToWorldScale::new(1.0) * prim_context.device_pixel_scale;
 
         match self.kind {
             PictureKind::Image {
                 ref mut secondary_render_task_id,
@@ -339,22 +340,22 @@ impl PicturePrimitive {
                             PremultipliedColorF::TRANSPARENT,
                             None,
                         );
 
                         let blur_render_task_id = render_tasks.add(blur_render_task);
                         self.render_task_id = Some(blur_render_task_id);
                     }
                     Some(PictureCompositeMode::Filter(FilterOp::DropShadow(offset, blur_radius, color))) => {
-                        let screen_offset = (offset * content_scale).round().to_i32();
+                        let rect = (prim_local_rect.translate(&-offset) * content_scale).round().to_i32();
                         let picture_task = RenderTask::new_picture(
-                            Some(prim_screen_rect.size),
+                            Some(rect.size),
                             prim_index,
                             RenderTargetKind::Color,
-                            ContentOrigin::Screen(prim_screen_rect.origin - screen_offset),
+                            ContentOrigin::Screen(rect.origin),
                             PremultipliedColorF::TRANSPARENT,
                             ClearMode::Transparent,
                             child_tasks,
                             None,
                             PictureType::Image,
                         );
 
                         let blur_std_deviation = blur_radius * prim_context.device_pixel_scale.0;
--- a/gfx/webrender/src/prim_store.rs
+++ b/gfx/webrender/src/prim_store.rs
@@ -1170,16 +1170,17 @@ impl PrimitiveStore {
             PrimitiveKind::Border | PrimitiveKind::Line => {}
             PrimitiveKind::Picture => {
                 self.cpu_pictures[metadata.cpu_prim_index.0]
                     .prepare_for_render(
                         prim_index,
                         prim_context,
                         render_tasks,
                         metadata.screen_rect.as_ref().expect("bug: trying to draw an off-screen picture!?"),
+                        &metadata.local_rect,
                         child_tasks,
                         parent_tasks,
                     );
             }
             PrimitiveKind::TextRun => {
                 let pic = &self.cpu_pictures[pic_index.0];
                 let text = &mut self.cpu_text_runs[metadata.cpu_prim_index.0];
                 // The transform only makes sense for screen space rasterization
@@ -1765,17 +1766,17 @@ impl PrimitiveStore {
                 result,
             );
         }
 
         let (local_rect, unclipped_device_rect) = {
             let metadata = &mut self.cpu_metadata[prim_index.0];
             if metadata.local_rect.size.width <= 0.0 ||
                metadata.local_rect.size.height <= 0.0 {
-                warn!("invalid primitive rect {:?}", metadata.local_rect);
+                //warn!("invalid primitive rect {:?}", metadata.local_rect);
                 return None;
             }
 
             let local_rect = metadata.local_clip_rect.intersection(&metadata.local_rect);
             let local_rect = match local_rect {
                 Some(local_rect) => local_rect,
                 None if perform_culling => return None,
                 None => LayerRect::zero(),
--- a/gfx/webrender/src/record.rs
+++ b/gfx/webrender/src/record.rs
@@ -61,18 +61,21 @@ impl ApiRecordingReceiver for BinaryReco
     }
 }
 
 pub fn should_record_msg(msg: &ApiMsg) -> bool {
     match *msg {
         ApiMsg::UpdateResources(..) |
         ApiMsg::AddDocument { .. } |
         ApiMsg::DeleteDocument(..) => true,
-        ApiMsg::UpdateDocument(_, ref msg) => {
-            match *msg {
-                DocumentMsg::GetScrollNodeState(..) |
-                DocumentMsg::HitTest(..) => false,
-                _ => true,
+        ApiMsg::UpdateDocument(_, ref msgs) => {
+            for msg in msgs {
+                match *msg {
+                    DocumentMsg::GetScrollNodeState(..) |
+                    DocumentMsg::HitTest(..) => {}
+                    _ => { return true; }
+                }
             }
+            false
         }
         _ => false,
     }
 }
--- a/gfx/webrender/src/render_backend.rs
+++ b/gfx/webrender/src/render_backend.rs
@@ -140,22 +140,43 @@ impl Document {
             pan,
             &mut resource_profile.texture_cache,
             &mut resource_profile.gpu_cache,
             &self.scene.properties,
         )
     }
 }
 
-enum DocumentOp {
-    Nop,
-    Built,
-    ScrolledNop,
-    Scrolled(RenderedDocument),
-    Rendered(RenderedDocument),
+struct DocumentOps {
+    scroll: bool,
+    build: bool,
+    render: bool,
+}
+
+impl DocumentOps {
+    fn nop() -> Self {
+        DocumentOps {
+            scroll: false,
+            build: false,
+            render: false,
+        }
+    }
+
+    fn build() -> Self {
+        DocumentOps {
+            build: true,
+            ..DocumentOps::nop()
+        }
+    }
+
+    fn combine(&mut self, other: Self) {
+        self.scroll = self.scroll || other.scroll;
+        self.build = self.build || other.build;
+        self.render = self.render || other.render;
+    }
 }
 
 /// The unique id for WR resource identification.
 static NEXT_NAMESPACE_ID: AtomicUsize = ATOMIC_USIZE_INIT;
 
 #[cfg(feature = "capture")]
 #[derive(Serialize, Deserialize)]
 struct PlainRenderBackend {
@@ -227,65 +248,68 @@ impl RenderBackend {
     }
 
     fn process_document(
         &mut self,
         document_id: DocumentId,
         message: DocumentMsg,
         frame_counter: u32,
         profile_counters: &mut BackendProfileCounters,
-    ) -> DocumentOp {
+    ) -> DocumentOps {
         let doc = self.documents.get_mut(&document_id).expect("No document?");
 
         match message {
             //TODO: move view-related messages in a separate enum?
             DocumentMsg::SetPageZoom(factor) => {
                 doc.view.page_zoom_factor = factor.get();
-                DocumentOp::Nop
+                DocumentOps::nop()
             }
             DocumentMsg::EnableFrameOutput(pipeline_id, enable) => {
                 if enable {
                     doc.output_pipelines.insert(pipeline_id);
                 } else {
                     doc.output_pipelines.remove(&pipeline_id);
                 }
-                DocumentOp::Nop
+                DocumentOps::nop()
             }
             DocumentMsg::SetPinchZoom(factor) => {
                 doc.view.pinch_zoom_factor = factor.get();
-                DocumentOp::Nop
+                DocumentOps::nop()
             }
             DocumentMsg::SetPan(pan) => {
                 doc.view.pan = pan;
-                DocumentOp::Nop
+                DocumentOps::nop()
             }
             DocumentMsg::SetWindowParameters {
                 window_size,
                 inner_rect,
                 device_pixel_ratio,
             } => {
                 doc.view.window_size = window_size;
                 doc.view.inner_rect = inner_rect;
                 doc.view.device_pixel_ratio = device_pixel_ratio;
-                DocumentOp::Nop
+                DocumentOps::nop()
             }
             DocumentMsg::SetDisplayList {
                 epoch,
                 pipeline_id,
                 background,
                 viewport_size,
                 content_size,
                 list_descriptor,
                 preserve_frame_state,
                 resources,
             } => {
-                profile_scope!("SetDisplayList");
+                // TODO: this will be removed from the SetDisplayList message soon.
+                self.resource_cache.update_resources(
+                    resources,
+                    &mut profile_counters.resources
+                );
 
-                self.resource_cache
-                    .update_resources(resources, &mut profile_counters.resources);
+                profile_scope!("SetDisplayList");
 
                 let mut data;
                 while {
                     data = self.payload_rx.recv_payload().unwrap();
                     data.epoch != epoch || data.pipeline_id != pipeline_id
                 } {
                     self.payload_tx.send_payload(data).unwrap()
                 }
@@ -311,17 +335,16 @@ impl RenderBackend {
                     doc.scene.set_display_list(
                         pipeline_id,
                         epoch,
                         built_display_list,
                         background,
                         viewport_size,
                         content_size,
                     );
-                    doc.build_scene(&mut self.resource_cache);
                 }
 
                 if let Some(ref mut ros) = doc.render_on_scroll {
                     *ros = false; //wait for `GenerateFrame`
                 }
 
                 // Note: this isn't quite right as auxiliary values will be
                 // pulled out somewhere in the prim_store, but aux values are
@@ -332,131 +355,141 @@ impl RenderBackend {
                     builder_start_time,
                     builder_finish_time,
                     send_start_time,
                     display_list_received_time,
                     display_list_consumed_time,
                     display_list_len,
                 );
 
-                DocumentOp::Built
+                DocumentOps::build()
+            }
+            DocumentMsg::UpdateResources(updates) => {
+                profile_scope!("UpdateResources");
+
+                self.resource_cache.update_resources(
+                    updates,
+                    &mut profile_counters.resources
+                );
+
+                DocumentOps::nop()
+            }
+            DocumentMsg::UpdateEpoch(pipeline_id, epoch) => {
+                doc.scene.update_epoch(pipeline_id, epoch);
+                doc.frame_ctx.update_epoch(pipeline_id, epoch);
+                DocumentOps::nop()
             }
             DocumentMsg::UpdatePipelineResources { resources, pipeline_id, epoch } => {
                 profile_scope!("UpdateResources");
 
                 self.resource_cache
                     .update_resources(resources, &mut profile_counters.resources);
 
                 doc.scene.update_epoch(pipeline_id, epoch);
                 doc.frame_ctx.update_epoch(pipeline_id, epoch);
 
-                DocumentOp::Nop
+                DocumentOps::nop()
             }
             DocumentMsg::SetRootPipeline(pipeline_id) => {
                 profile_scope!("SetRootPipeline");
 
                 doc.scene.set_root_pipeline_id(pipeline_id);
                 if doc.scene.pipelines.get(&pipeline_id).is_some() {
-                    let _timer = profile_counters.total_time.timer();
-                    doc.build_scene(&mut self.resource_cache);
-                    DocumentOp::Built
+                    DocumentOps::build()
                 } else {
-                    DocumentOp::Nop
+                    DocumentOps::nop()
                 }
             }
             DocumentMsg::RemovePipeline(pipeline_id) => {
                 profile_scope!("RemovePipeline");
 
                 doc.scene.remove_pipeline(pipeline_id);
-                DocumentOp::Nop
+                DocumentOps::nop()
             }
             DocumentMsg::Scroll(delta, cursor, move_phase) => {
                 profile_scope!("Scroll");
                 let _timer = profile_counters.total_time.timer();
 
-                if doc.frame_ctx.scroll(delta, cursor, move_phase) && doc.render_on_scroll == Some(true)
-                {
-                    let frame = doc.render(
-                        &mut self.resource_cache,
-                        &mut self.gpu_cache,
-                        &mut profile_counters.resources,
-                    );
-                    DocumentOp::Scrolled(frame)
-                } else {
-                    DocumentOp::ScrolledNop
+                let should_render = doc.frame_ctx.scroll(delta, cursor, move_phase)
+                    && doc.render_on_scroll == Some(true);
+
+                DocumentOps {
+                    scroll: true,
+                    build: false,
+                    render: should_render,
                 }
             }
             DocumentMsg::HitTest(pipeline_id, point, flags, tx) => {
                 profile_scope!("HitTest");
                 let cst = doc.frame_ctx.get_clip_scroll_tree();
                 let result = doc.frame_builder
                     .as_ref()
                     .unwrap()
                     .hit_test(cst, pipeline_id, point, flags);
                 tx.send(result).unwrap();
-                DocumentOp::Nop
+                DocumentOps::nop()
             }
             DocumentMsg::ScrollNodeWithId(origin, id, clamp) => {
                 profile_scope!("ScrollNodeWithScrollId");
                 let _timer = profile_counters.total_time.timer();
 
-                if doc.frame_ctx.scroll_node(origin, id, clamp) && doc.render_on_scroll == Some(true) {
-                    let frame = doc.render(
-                        &mut self.resource_cache,
-                        &mut self.gpu_cache,
-                        &mut profile_counters.resources,
-                    );
-                    DocumentOp::Scrolled(frame)
-                } else {
-                    DocumentOp::ScrolledNop
+                let should_render = doc.frame_ctx.scroll_node(origin, id, clamp)
+                    && doc.render_on_scroll == Some(true);
+
+                DocumentOps {
+                    scroll: true,
+                    build: false,
+                    render: should_render,
                 }
             }
             DocumentMsg::TickScrollingBounce => {
                 profile_scope!("TickScrollingBounce");
                 let _timer = profile_counters.total_time.timer();
 
                 doc.frame_ctx.tick_scrolling_bounce_animations();
-                if doc.render_on_scroll == Some(true) {
-                    let frame = doc.render(
-                        &mut self.resource_cache,
-                        &mut self.gpu_cache,
-                        &mut profile_counters.resources,
-                    );
-                    DocumentOp::Scrolled(frame)
-                } else {
-                    DocumentOp::ScrolledNop
+
+                DocumentOps {
+                    scroll: true,
+                    build: false,
+                    render: doc.render_on_scroll == Some(true),
                 }
             }
             DocumentMsg::GetScrollNodeState(tx) => {
                 profile_scope!("GetScrollNodeState");
                 tx.send(doc.frame_ctx.get_scroll_node_state()).unwrap();
-                DocumentOp::Nop
+                DocumentOps::nop()
             }
-            DocumentMsg::GenerateFrame(property_bindings) => {
-                profile_scope!("GenerateFrame");
+            DocumentMsg::UpdateDynamicProperties(property_bindings) => {
+                // Ideally, when there are property bindings present,
+                // we won't need to rebuild the entire frame here.
+                // However, to avoid conflicts with the ongoing work to
+                // refactor how scroll roots + transforms work, this
+                // just rebuilds the frame if there are animated property
+                // bindings present for now.
+                // TODO(gw): Once the scrolling / reference frame changes
+                //           are completed, optimize the internals of
+                //           animated properties to not require a full
+                //           rebuild of the frame!
+                doc.scene.properties.set_properties(property_bindings);
+                DocumentOps::build()
+            }
+            DocumentMsg::GenerateFrame => {
                 let _timer = profile_counters.total_time.timer();
 
-                if let Some(property_bindings) = property_bindings {
-                    doc.scene.properties.set_properties(property_bindings);
-                }
+                let mut op = DocumentOps::nop();
 
                 if let Some(ref mut ros) = doc.render_on_scroll {
                     *ros = true;
                 }
 
                 if doc.scene.root_pipeline_id.is_some() {
-                    let frame = doc.render(
-                        &mut self.resource_cache,
-                        &mut self.gpu_cache,
-                        &mut profile_counters.resources,
-                    );
-                    DocumentOp::Rendered(frame)
-                } else {
-                    DocumentOp::ScrolledNop
+                    op.render = true;
                 }
+
+                op
             }
         }
     }
 
     fn next_namespace_id(&self) -> IdNamespace {
         IdNamespace(NEXT_NAMESPACE_ID.fetch_add(1, Ordering::Relaxed) as u32)
     }
 
@@ -510,40 +543,16 @@ impl RenderBackend {
                         self.frame_config.clone(),
                         initial_size,
                         layer,
                         self.enable_render_on_scroll,
                         self.default_device_pixel_ratio,
                     );
                     self.documents.insert(document_id, document);
                 }
-                ApiMsg::UpdateDocument(document_id, doc_msg) => match self.process_document(
-                    document_id,
-                    doc_msg,
-                    frame_counter,
-                    &mut profile_counters,
-                ) {
-                    DocumentOp::Nop => {}
-                    DocumentOp::Built => {}
-                    DocumentOp::ScrolledNop => {
-                        self.notify_compositor_of_new_scroll_document(document_id, false);
-                    }
-                    DocumentOp::Scrolled(doc) => {
-                        self.publish_document(document_id, doc, &mut profile_counters);
-                        self.notify_compositor_of_new_scroll_document(document_id, true);
-                    }
-                    DocumentOp::Rendered(doc) => {
-                        frame_counter += 1;
-                        self.publish_document_and_notify_compositor(
-                            document_id,
-                            doc,
-                            &mut profile_counters,
-                        );
-                    }
-                },
                 ApiMsg::DeleteDocument(document_id) => {
                     self.documents.remove(&document_id);
                 }
                 ApiMsg::ExternalEvent(evt) => {
                     self.notifier.external_event(evt);
                 }
                 ApiMsg::ClearNamespace(namespace_id) => {
                     self.resource_cache.clear_namespace(namespace_id);
@@ -609,52 +618,81 @@ impl RenderBackend {
                     };
                     self.result_tx.send(msg).unwrap();
                     self.notifier.wake_up();
                 }
                 ApiMsg::ShutDown => {
                     self.notifier.shut_down();
                     break;
                 }
+                ApiMsg::UpdateDocument(document_id, doc_msgs) => {
+                    self.update_document(
+                        document_id,
+                        doc_msgs,
+                        &mut frame_counter,
+                        &mut profile_counters
+                    )
+                }
             }
         }
     }
 
-    fn publish_document(
-        &mut self,
-        document_id: DocumentId,
-        document: RenderedDocument,
-        profile_counters: &mut BackendProfileCounters,
-    ) {
-        let pending_update = self.resource_cache.pending_updates();
-        let msg = ResultMsg::PublishDocument(document_id, document, pending_update, profile_counters.clone());
-        self.result_tx.send(msg).unwrap();
-        profile_counters.reset();
-    }
-
-    fn publish_document_and_notify_compositor(
+    fn update_document(
         &mut self,
         document_id: DocumentId,
-        document: RenderedDocument,
+        doc_msgs: Vec<DocumentMsg>,
+        frame_counter: &mut u32,
         profile_counters: &mut BackendProfileCounters,
     ) {
-        self.publish_document(document_id, document, profile_counters);
+        let mut op = DocumentOps::nop();
+        for doc_msg in doc_msgs {
+            op.combine(
+                self.process_document(
+                    document_id,
+                    doc_msg,
+                    *frame_counter,
+                    profile_counters,
+                )
+            );
+        }
 
-        self.notifier.new_document_ready(document_id, false, true);
-    }
+        let doc = self.documents.get_mut(&document_id).unwrap();
+
+        if op.build {
+            profile_scope!("build scene");
+            doc.build_scene(&mut self.resource_cache);
+        }
+
+        if op.render {
+            profile_scope!("generate frame");
 
-    fn notify_compositor_of_new_scroll_document(
-        &self,
-        document_id: DocumentId,
-        composite_needed: bool,
-    ) {
-        self.notifier.new_document_ready(document_id, true, composite_needed);
+            *frame_counter += 1;
+            let rendered_document = doc.render(
+                &mut self.resource_cache,
+                &mut self.gpu_cache,
+                &mut profile_counters.resources,
+            );
+
+            // Publish the frame
+            let pending_update = self.resource_cache.pending_updates();
+            let msg = ResultMsg::PublishDocument(
+                document_id,
+                rendered_document,
+                pending_update,
+                profile_counters.clone()
+            );
+            self.result_tx.send(msg).unwrap();
+            profile_counters.reset();
+        }
+
+        if op.render || op.scroll {
+            self.notifier.new_document_ready(document_id, op.scroll, op.render);
+        }
     }
 
-
     #[cfg(not(feature = "debugger"))]
     fn get_docs_for_debugger(&self) -> String {
         String::new()
     }
 
     #[cfg(feature = "debugger")]
     fn traverse_items<'a>(
         &self,
@@ -865,14 +903,25 @@ impl RenderBackend {
             };
 
             doc.build_scene(&mut self.resource_cache);
             let render_doc = doc.render(
                 &mut self.resource_cache,
                 &mut self.gpu_cache,
                 &mut profile_counters.resources,
             );
-            self.publish_document_and_notify_compositor(id, render_doc, profile_counters);
+
+            let pending_update = self.resource_cache.pending_updates();
+            let msg = ResultMsg::PublishDocument(
+                id,
+                render_doc,
+                pending_update,
+                profile_counters.clone()
+            );
+            self.result_tx.send(msg).unwrap();
+            profile_counters.reset();
+
+            self.notifier.new_document_ready(id, false, true);
 
             self.documents.insert(id, doc);
         }
     }
 }
--- a/gfx/webrender/src/renderer.rs
+++ b/gfx/webrender/src/renderer.rs
@@ -2466,16 +2466,26 @@ impl Renderer {
         String::new()
     }
 
     #[cfg(feature = "debugger")]
     fn debug_alpha_target(target: &AlphaRenderTarget) -> debug_server::Target {
         let mut debug_target = debug_server::Target::new("A8");
 
         debug_target.add(
+            debug_server::BatchKind::Cache,
+            "Scalings",
+            target.scalings.len(),
+        );
+        debug_target.add(
+            debug_server::BatchKind::Cache,
+            "Zero Clears",
+            target.zero_clears.len(),
+        );
+        debug_target.add(
             debug_server::BatchKind::Clip,
             "Clear",
             target.clip_batcher.border_clears.len(),
         );
         debug_target.add(
             debug_server::BatchKind::Clip,
             "Borders",
             target.clip_batcher.borders.len(),
@@ -2513,16 +2523,26 @@ impl Renderer {
     }
 
     #[cfg(feature = "debugger")]
     fn debug_color_target(target: &ColorRenderTarget) -> debug_server::Target {
         let mut debug_target = debug_server::Target::new("RGBA8");
 
         debug_target.add(
             debug_server::BatchKind::Cache,
+            "Scalings",
+            target.scalings.len(),
+        );
+        debug_target.add(
+            debug_server::BatchKind::Cache,
+            "Readbacks",
+            target.readbacks.len(),
+        );
+        debug_target.add(
+            debug_server::BatchKind::Cache,
             "Vertical Blur",
             target.vertical_blurs.len(),
         );
         debug_target.add(
             debug_server::BatchKind::Cache,
             "Horizontal Blur",
             target.horizontal_blurs.len(),
         );
--- a/gfx/webrender_api/src/api.rs
+++ b/gfx/webrender_api/src/api.rs
@@ -118,16 +118,184 @@ impl ResourceUpdates {
         self.updates.append(&mut other.updates);
     }
 
     pub fn clear(&mut self) {
         self.updates.clear()
     }
 }
 
+/// A Transaction is a group of commands to apply atomically to a document.
+///
+/// This mechanism ensures that:
+///  - no other message can be interleaved between two commands that need to be applied together.
+///  - no redundant work is performed if two commands in the same transaction cause the scene or
+///    the frame to be rebuilt.
+pub struct Transaction {
+    ops: Vec<DocumentMsg>,
+    payloads: Vec<Payload>,
+}
+
+impl Transaction {
+    pub fn new() -> Self {
+        Transaction {
+            ops: Vec::new(),
+            payloads: Vec::new(),
+        }
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.ops.is_empty()
+    }
+
+    pub fn update_epoch(&mut self, pipeline_id: PipelineId, epoch: Epoch) {
+        self.ops.push(DocumentMsg::UpdateEpoch(pipeline_id, epoch));
+    }
+
+    /// Sets the root pipeline.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// # use webrender_api::{DeviceUintSize, PipelineId, RenderApiSender, Transaction};
+    /// # fn example() {
+    /// let pipeline_id = PipelineId(0, 0);
+    /// let mut txn = Transaction::new();
+    /// txn.set_root_pipeline(pipeline_id);
+    /// # }
+    /// ```
+    pub fn set_root_pipeline(&mut self, pipeline_id: PipelineId) {
+        self.ops.push(DocumentMsg::SetRootPipeline(pipeline_id));
+    }
+
+    /// Removes data associated with a pipeline from the internal data structures.
+    /// If the specified `pipeline_id` is for the root pipeline, the root pipeline
+    /// is reset back to `None`.
+    pub fn remove_pipeline(&mut self, pipeline_id: PipelineId) {
+        self.ops.push(DocumentMsg::RemovePipeline(pipeline_id));
+    }
+
+    /// Supplies a new frame to WebRender.
+    ///
+    /// Non-blocking, it notifies a worker process which processes the display list.
+    /// When it's done and a RenderNotifier has been set in `webrender::Renderer`,
+    /// [new_frame_ready()][notifier] gets called.
+    ///
+    /// Note: Scrolling doesn't require an own Frame.
+    ///
+    /// Arguments:
+    ///
+    /// * `document_id`: Target Document ID.
+    /// * `epoch`: The unique Frame ID, monotonically increasing.
+    /// * `background`: The background color of this pipeline.
+    /// * `viewport_size`: The size of the viewport for this frame.
+    /// * `pipeline_id`: The ID of the pipeline that is supplying this display list.
+    /// * `content_size`: The total screen space size of this display list's display items.
+    /// * `display_list`: The root Display list used in this frame.
+    /// * `preserve_frame_state`: If a previous frame exists which matches this pipeline
+    ///                           id, this setting determines if frame state (such as scrolling
+    ///                           position) should be preserved for this new display list.
+    /// * `resources`: A set of resource updates that must be applied at the same time as the
+    ///                display list.
+    ///
+    /// [notifier]: trait.RenderNotifier.html#tymethod.new_frame_ready
+    pub fn set_display_list(
+        &mut self,
+        epoch: Epoch,
+        background: Option<ColorF>,
+        viewport_size: LayoutSize,
+        (pipeline_id, content_size, display_list): (PipelineId, LayoutSize, BuiltDisplayList),
+        preserve_frame_state: bool,
+    ) {
+        let (display_list_data, list_descriptor) = display_list.into_data();
+        self.ops.push(
+            DocumentMsg::SetDisplayList {
+                epoch,
+                pipeline_id,
+                background,
+                viewport_size,
+                content_size,
+                list_descriptor,
+                preserve_frame_state,
+                resources: ResourceUpdates::new(),
+            }
+        );
+        self.payloads.push(Payload { epoch, pipeline_id, display_list_data });
+    }
+
+    pub fn update_resources(&mut self, resources: ResourceUpdates) {
+        self.ops.push(DocumentMsg::UpdateResources(resources));
+    }
+
+    pub fn set_window_parameters(
+        &mut self,
+        window_size: DeviceUintSize,
+        inner_rect: DeviceUintRect,
+        device_pixel_ratio: f32,
+    ) {
+        self.ops.push(
+            DocumentMsg::SetWindowParameters {
+                window_size,
+                inner_rect,
+                device_pixel_ratio,
+            },
+        );
+    }
+
+    /// Scrolls the scrolling layer under the `cursor`
+    ///
+    /// WebRender looks for the layer closest to the user
+    /// which has `ScrollPolicy::Scrollable` set.
+    pub fn scroll(
+        &mut self,
+        scroll_location: ScrollLocation,
+        cursor: WorldPoint,
+        phase: ScrollEventPhase,
+    ) {
+        self.ops.push(DocumentMsg::Scroll(scroll_location, cursor, phase));
+    }
+
+    pub fn scroll_node_with_id(
+        &mut self,
+        origin: LayoutPoint,
+        id: ClipId,
+        clamp: ScrollClamping,
+    ) {
+        self.ops.push(DocumentMsg::ScrollNodeWithId(origin, id, clamp));
+    }
+
+    pub fn set_page_zoom(&mut self, page_zoom: ZoomFactor) {
+        self.ops.push(DocumentMsg::SetPageZoom(page_zoom));
+    }
+
+    pub fn set_pinch_zoom(&mut self, pinch_zoom: ZoomFactor) {
+        self.ops.push(DocumentMsg::SetPinchZoom(pinch_zoom));
+    }
+
+    pub fn set_pan(&mut self, pan: DeviceIntPoint) {
+        self.ops.push(DocumentMsg::SetPan(pan));
+    }
+
+    pub fn tick_scrolling_bounce_animations(&mut self) {
+        self.ops.push(DocumentMsg::TickScrollingBounce);
+    }
+
+    /// Generate a new frame.
+    pub fn generate_frame(&mut self) {
+        self.ops.push(DocumentMsg::GenerateFrame);
+    }
+
+    /// Supply a list of animated property bindings that should be used to resolve
+    /// bindings in the current display list.
+    pub fn update_dynamic_properties(&mut self, properties: DynamicProperties) {
+        self.ops.push(DocumentMsg::UpdateDynamicProperties(properties));
+    }
+}
+
+
 #[derive(Clone, Deserialize, Serialize)]
 pub struct AddImage {
     pub key: ImageKey,
     pub descriptor: ImageDescriptor,
     pub data: ImageData,
     pub tiling: Option<TileSize>,
 }
 
@@ -194,37 +362,41 @@ pub enum DocumentMsg {
         epoch: Epoch,
         pipeline_id: PipelineId,
         background: Option<ColorF>,
         viewport_size: LayoutSize,
         content_size: LayoutSize,
         preserve_frame_state: bool,
         resources: ResourceUpdates,
     },
+    UpdateResources(ResourceUpdates),
+    // TODO(nical): Remove this once gecko doesn't use it anymore.
     UpdatePipelineResources {
         resources: ResourceUpdates,
         pipeline_id: PipelineId,
         epoch: Epoch,
     },
+    UpdateEpoch(PipelineId, Epoch),
     SetPageZoom(ZoomFactor),
     SetPinchZoom(ZoomFactor),
     SetPan(DeviceIntPoint),
     SetRootPipeline(PipelineId),
     RemovePipeline(PipelineId),
     EnableFrameOutput(PipelineId, bool),
     SetWindowParameters {
         window_size: DeviceUintSize,
         inner_rect: DeviceUintRect,
         device_pixel_ratio: f32,
     },
     Scroll(ScrollLocation, WorldPoint, ScrollEventPhase),
     ScrollNodeWithId(LayoutPoint, ClipId, ScrollClamping),
     TickScrollingBounce,
     GetScrollNodeState(MsgSender<Vec<ScrollLayerState>>),
-    GenerateFrame(Option<DynamicProperties>),
+    GenerateFrame,
+    UpdateDynamicProperties(DynamicProperties),
 }
 
 impl fmt::Debug for DocumentMsg {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         f.write_str(match *self {
             DocumentMsg::SetDisplayList { .. } => "DocumentMsg::SetDisplayList",
             DocumentMsg::UpdatePipelineResources { .. } => "DocumentMsg::UpdatePipelineResources",
             DocumentMsg::HitTest(..) => "DocumentMsg::HitTest",
@@ -233,18 +405,21 @@ impl fmt::Debug for DocumentMsg {
             DocumentMsg::SetPan(..) => "DocumentMsg::SetPan",
             DocumentMsg::SetRootPipeline(..) => "DocumentMsg::SetRootPipeline",
             DocumentMsg::RemovePipeline(..) => "DocumentMsg::RemovePipeline",
             DocumentMsg::SetWindowParameters { .. } => "DocumentMsg::SetWindowParameters",
             DocumentMsg::Scroll(..) => "DocumentMsg::Scroll",
             DocumentMsg::ScrollNodeWithId(..) => "DocumentMsg::ScrollNodeWithId",
             DocumentMsg::TickScrollingBounce => "DocumentMsg::TickScrollingBounce",
             DocumentMsg::GetScrollNodeState(..) => "DocumentMsg::GetScrollNodeState",
-            DocumentMsg::GenerateFrame(..) => "DocumentMsg::GenerateFrame",
+            DocumentMsg::GenerateFrame => "DocumentMsg::GenerateFrame",
             DocumentMsg::EnableFrameOutput(..) => "DocumentMsg::EnableFrameOutput",
+            DocumentMsg::UpdateResources(..) => "DocumentMsg::UpdateResources",
+            DocumentMsg::UpdateEpoch(..) => "DocumentMsg::UpdateEpoch",
+            DocumentMsg::UpdateDynamicProperties(..) => "DocumentMsg::UpdateDynamicProperties",
         })
     }
 }
 
 #[derive(Debug, Clone, Deserialize, Serialize)]
 pub enum DebugCommand {
     /// Display the frame profiler on screen.
     EnableProfiler(bool),
@@ -288,17 +463,17 @@ pub enum ApiMsg {
     ),
     /// Gets the glyph indices from a string
     GetGlyphIndices(FontKey, String, MsgSender<Vec<Option<u32>>>),
     /// Adds a new document namespace.
     CloneApi(MsgSender<IdNamespace>),
     /// Adds a new document with given initial size.
     AddDocument(DocumentId, DeviceUintSize, DocumentLayer),
     /// A message targeted at a particular document.
-    UpdateDocument(DocumentId, DocumentMsg),
+    UpdateDocument(DocumentId, Vec<DocumentMsg>),
     /// Deletes an existing document.
     DeleteDocument(DocumentId),
     /// An opaque handle that must be passed to the render notifier. It is used by Gecko
     /// to forward gecko-specific messages to the render thread preserving the ordering
     /// within the other messages.
     ExternalEvent(ExternalEvent),
     /// Removes all resources associated with a namespace.
     ClearNamespace(IdNamespace),
@@ -565,20 +740,25 @@ impl RenderApi {
     }
 
     /// A helper method to send document messages.
     fn send(&self, document_id: DocumentId, msg: DocumentMsg) {
         // This assertion fails on Servo use-cases, because it creates different
         // `RenderApi` instances for layout and compositor.
         //assert_eq!(document_id.0, self.namespace_id);
         self.api_sender
-            .send(ApiMsg::UpdateDocument(document_id, msg))
+            .send(ApiMsg::UpdateDocument(document_id, vec![msg]))
             .unwrap()
     }
 
+    // TODO(nical) - decide what to do with the methods that are duplicated in Transaction.
+    // I think that we should remove them from RenderApi but we could also leave them here if
+    // it makes things easier for servo.
+    // They are all equivalent to creating a transaction with a single command.
+
     /// Sets the root pipeline.
     ///
     /// # Examples
     ///
     /// ```
     /// # use webrender_api::{DeviceUintSize, PipelineId, RenderApiSender};
     /// # fn example(sender: RenderApiSender) {
     /// let api = sender.create_api();
@@ -625,18 +805,24 @@ impl RenderApi {
     pub fn set_display_list(
         &self,
         document_id: DocumentId,
         epoch: Epoch,
         background: Option<ColorF>,
         viewport_size: LayoutSize,
         (pipeline_id, content_size, display_list): (PipelineId, LayoutSize, BuiltDisplayList),
         preserve_frame_state: bool,
-        resources: ResourceUpdates,
+        resources: ResourceUpdates, // TODO: this will be removed soon.
     ) {
+        // TODO(nical) set_display_list uses the epoch to match the displaylist and the payload
+        // coming from different channels when receiving in the render backend.
+        // It would be cleaner to use a separate id that is implicitly generated for the displaylist-payload
+        // matching so that the semantics of epochs is really up to the api user and so that the latter can't
+        // introduce bugs by accidently using the same epoch twice.
+
         let (display_list_data, list_descriptor) = display_list.into_data();
         self.send(
             document_id,
             DocumentMsg::SetDisplayList {
                 epoch,
                 pipeline_id,
                 background,
                 viewport_size,
@@ -651,16 +837,23 @@ impl RenderApi {
             .send_payload(Payload {
                 epoch,
                 pipeline_id,
                 display_list_data,
             })
             .unwrap();
     }
 
+    pub fn send_transaction(&mut self, document_id: DocumentId, transaction: Transaction) {
+        for payload in transaction.payloads {
+            self.payload_sender.send_payload(payload).unwrap();
+        }
+        self.api_sender.send(ApiMsg::UpdateDocument(document_id, transaction.ops)).unwrap();
+    }
+
     /// Scrolls the scrolling layer under the `cursor`
     ///
     /// WebRender looks for the layer closest to the user
     /// which has `ScrollPolicy::Scrollable` set.
     pub fn scroll(
         &self,
         document_id: DocumentId,
         scroll_location: ScrollLocation,
@@ -759,17 +952,20 @@ impl RenderApi {
     /// Generate a new frame. Optionally, supply a list of animated
     /// property bindings that should be used to resolve bindings
     /// in the current display list.
     pub fn generate_frame(
         &self,
         document_id: DocumentId,
         property_bindings: Option<DynamicProperties>,
     ) {
-        self.send(document_id, DocumentMsg::GenerateFrame(property_bindings));
+        if let Some(properties) = property_bindings {
+            self.send(document_id, DocumentMsg::UpdateDynamicProperties(properties));
+        }
+        self.send(document_id, DocumentMsg::GenerateFrame);
     }
 
     /// Save a capture of the current frame state for debugging.
     pub fn save_capture(&self, path: PathBuf) {
         let msg = ApiMsg::DebugCommand(DebugCommand::SaveCapture(path));
         self.send_message(msg);
     }