Bug 1579235 - Part 6 - Support an opaque/alpha native surface per slice. r=Bert
authorGlenn Watson <gw@intuitionlibrary.com>
Sun, 01 Mar 2020 03:55:54 +0000
changeset 516285 6814870342efc284b2edec7c9aeb45240cb0fe09
parent 516284 810e31bf46f160c31e8cfa8d77aead4c902f2276
child 516286 8f267ed8f2e3d09ced8c665af0a6db3694197116
push id37172
push usermalexandru@mozilla.com
push dateMon, 02 Mar 2020 09:48:18 +0000
treeherdermozilla-central@51efc4b931f7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersBert
bugs1579235
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 1579235 - Part 6 - Support an opaque/alpha native surface per slice. r=Bert Previously, a native compositor surface was considered to be completely opaque, or completely translucent. This is due to a limitation in how alpha is handled in the DirectComposition API level. With this patch, each picture cache slice maintains both an opaque and translucent native surface handle. Tiles are assigned to one of those surfaces based on their current opacity. This is a performance optimization in some cases, since: - Even if part of a cache is translucent, opaque tiles can still participate in occlusion at the compositor level. - If a tile is changing from opaque to translucent, it now invalidates only that tile, rather than the entire surface. The primary benefit of this patch is that it allows compositor surfaces to be drawn sliced in between the opaque surface and any overlay / alpha tiles. Differential Revision: https://phabricator.services.mozilla.com/D64495
gfx/wr/webrender/src/composite.rs
gfx/wr/webrender/src/frame_builder.rs
gfx/wr/webrender/src/picture.rs
--- a/gfx/wr/webrender/src/composite.rs
+++ b/gfx/wr/webrender/src/composite.rs
@@ -231,17 +231,16 @@ struct Occluder {
     device_rect: DeviceIntRect,
 }
 
 /// Describes the properties that identify a tile composition uniquely.
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 #[derive(PartialEq, Clone)]
 pub struct CompositeSurfaceDescriptor {
-    pub slice: usize,
     pub surface_id: Option<NativeSurfaceId>,
     pub offset: DevicePoint,
     pub clip_rect: DeviceRect,
 }
 
 /// Describes surface properties used to composite a frame. This
 /// is used to compare compositions between frames.
 #[cfg_attr(feature = "capture", derive(Serialize))]
@@ -380,52 +379,54 @@ impl CompositeState {
         tile_cache: &TileCacheInstance,
         device_clip_rect: DeviceRect,
         z_id: ZBufferId,
         global_device_pixel_scale: DevicePixelScale,
         resource_cache: &ResourceCache,
         gpu_cache: &mut GpuCache,
         deferred_resolves: &mut Vec<DeferredResolve>,
     ) {
-        let mut visible_tile_count = 0;
+        let mut visible_opaque_tile_count = 0;
+        let mut visible_alpha_tile_count = 0;
 
         for tile in tile_cache.tiles.values() {
             if !tile.is_visible {
                 // This can occur when a tile is found to be occluded during frame building.
                 continue;
             }
 
-            visible_tile_count += 1;
-
             let device_rect = (tile.world_tile_rect * global_device_pixel_scale).round();
             let surface = tile.surface.as_ref().expect("no tile surface set!");
 
-            let (surface, is_opaque) = match surface {
+            let (surface, is_opaque, tile_id) = match surface {
                 TileSurface::Color { color } => {
-                    (CompositeTileSurface::Color { color: *color }, true)
+                    (CompositeTileSurface::Color { color: *color }, true, None)
                 }
                 TileSurface::Clear => {
-                    (CompositeTileSurface::Clear, false)
+                    (CompositeTileSurface::Clear, false, None)
                 }
                 TileSurface::Texture { descriptor, .. } => {
                     let surface = descriptor.resolve(resource_cache, tile_cache.current_tile_size);
+                    let tile_id = match surface {
+                        ResolvedSurfaceTexture::Native { id, .. } => Some(id),
+                        ResolvedSurfaceTexture::TextureCache { .. } => None,
+                    };
                     (
                         CompositeTileSurface::Texture { surface },
                         tile.is_opaque || tile_cache.is_opaque(),
+                        tile_id,
                     )
                 }
             };
 
-            let tile_id = tile_cache.native_surface_id.map(|surface_id| {
-                NativeTileId {
-                    surface_id,
-                    x: tile.tile_offset.x,
-                    y: tile.tile_offset.y,
-                }
-            });
+            if is_opaque {
+                visible_opaque_tile_count += 1;
+            } else {
+                visible_alpha_tile_count += 1;
+            }
 
             // Determine ordering of this tile, based on presence of compositor
             // surfaces that intersect the tile.
             let mut mode = TileCompositeMode::Default;
 
             if tile.has_compositor_surface {
                 // TODO(gw): This will almost always select over blend, due to the
                 //           background rectangle. In future, we can optimize this
@@ -520,21 +521,30 @@ impl CompositeState {
                 yuv_color_space: external_surface.yuv_color_space,
                 yuv_format: external_surface.yuv_format,
                 yuv_rescale: external_surface.yuv_rescale,
                 image_buffer_kind: get_buffer_kind(yuv_planes[0].texture),
                 yuv_planes,
             });
         }
 
-        if visible_tile_count > 0 {
+        if visible_opaque_tile_count > 0 {
             self.descriptor.surfaces.push(
                 CompositeSurfaceDescriptor {
-                    slice: tile_cache.slice,
-                    surface_id: tile_cache.native_surface_id,
+                    surface_id: tile_cache.native_surface.as_ref().map(|s| s.opaque),
+                    offset: tile_cache.device_position,
+                    clip_rect: device_clip_rect,
+                }
+            );
+        }
+
+        if visible_alpha_tile_count > 0 {
+            self.descriptor.surfaces.push(
+                CompositeSurfaceDescriptor {
+                    surface_id: tile_cache.native_surface.as_ref().map(|s| s.alpha),
                     offset: tile_cache.device_position,
                     clip_rect: device_clip_rect,
                 }
             );
         }
     }
 
     /// Add a tile to the appropriate array, depending on tile properties and compositor mode.
--- a/gfx/wr/webrender/src/frame_builder.rs
+++ b/gfx/wr/webrender/src/frame_builder.rs
@@ -397,18 +397,19 @@ impl FrameBuilder {
             // and surfaces that match. Once the `update_visibility` call above is
             // complete, any tiles that are left remaining in the `retained_tiles`
             // map are not needed and will be dropped. For simple compositing mode,
             // this is fine, since texture cache handles are garbage collected at
             // the end of each frame. However, if we're in native compositor mode,
             // we need to manually clean up any native compositor surfaces that were
             // allocated by these tiles.
             for (_, mut cache_state) in visibility_state.retained_tiles.caches.drain() {
-                if let Some(native_surface_id) = cache_state.native_surface_id.take() {
-                    visibility_state.resource_cache.destroy_compositor_surface(native_surface_id);
+                if let Some(native_surface) = cache_state.native_surface.take() {
+                    visibility_state.resource_cache.destroy_compositor_surface(native_surface.opaque);
+                    visibility_state.resource_cache.destroy_compositor_surface(native_surface.alpha);
                 }
             }
         }
 
         let mut frame_state = FrameBuildingState {
             render_tasks,
             profile_counters,
             clip_store: &mut scene.clip_store,
--- a/gfx/wr/webrender/src/picture.rs
+++ b/gfx/wr/webrender/src/picture.rs
@@ -269,19 +269,17 @@ pub struct PictureCacheState {
     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 native_surface: Option<NativeSurface>,
 }
 
 pub struct PictureCacheRecycledAllocations {
     old_opacity_bindings: FastHashMap<PropertyBindingId, OpacityBindingInfo>,
     old_color_bindings: FastHashMap<PropertyBindingId, ColorBindingInfo>,
     compare_cache: FastHashMap<PrimitiveComparisonKey, PrimitiveCompareResult>,
 }
 
@@ -1207,17 +1205,36 @@ impl Tile {
             .unwrap_or_else(DeviceRect::zero);
 
         // Check if this tile can be considered opaque. Opacity state must be updated only
         // after all early out checks have been performed. Otherwise, we might miss updating
         // the native surface next time this tile becomes visible.
         let clipped_rect = self.current_descriptor.local_valid_rect
             .intersection(&ctx.local_clip_rect)
             .unwrap_or_else(PictureRect::zero);
-        self.is_opaque = ctx.backdrop.rect.contains_rect(&clipped_rect);
+        let is_opaque = ctx.backdrop.rect.contains_rect(&clipped_rect);
+
+        if is_opaque != self.is_opaque {
+            // If opacity changed, the native compositor surface and all tiles get invalidated.
+            // (this does nothing if not using native compositor mode).
+            // TODO(gw): This property probably changes very rarely, so it is OK to invalidate
+            //           everything in this case. If it turns out that this isn't true, we could
+            //           consider other options, such as per-tile opacity (natively supported
+            //           on CoreAnimation, and supported if backed by non-virtual surfaces in
+            //           DirectComposition).
+            if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { ref mut id, .. }, .. }) = self.surface {
+                if let Some(id) = id.take() {
+                    state.resource_cache.destroy_compositor_tile(id);
+                }
+            }
+
+            // Invalidate the entire tile to force a redraw.
+            self.invalidate(None, InvalidationReason::SurfaceOpacityChanged { became_opaque: is_opaque });
+            self.is_opaque = is_opaque;
+        }
 
         // Check if the selected composite mode supports dirty rect updates. For Draw composite
         // mode, we can always update the content with smaller dirty rects. For native composite
         // mode, we can only use dirty rects if the compositor supports partial surface updates.
         let (supports_dirty_rects, supports_simple_prims) = match state.composite_state.compositor_kind {
             CompositorKind::Draw { .. } => {
                 (true, true)
             }
@@ -2033,16 +2050,34 @@ impl TileCacheLogger {
             output.write_all(b"// interning data\n").unwrap();
             output.write_all(self.frames[index].update_lists.to_ron().as_bytes()).unwrap();
 
             files_written = files_written + 1;
         }
     }
 }
 
+/// Represents the native surfaces created for a picture cache, if using
+/// a native compositor. An opaque and alpha surface is always created,
+/// but tiles are added to a surface based on current opacity. If the
+/// calculated opacity of a tile changes, the tile is invalidated and
+/// attached to a different native surface. This means that we don't
+/// need to invalidate the entire surface if only some tiles are changing
+/// opacity. It also means we can take advantage of opaque tiles on cache
+/// slices where only some of the tiles are opaque. There is an assumption
+/// that creating a native surface is cheap, and only when a tile is added
+/// to a surface is there a significant cost. This assumption holds true
+/// for the current native compositor implementations on Windows and Mac.
+pub struct NativeSurface {
+    /// Native surface for opaque tiles
+    pub opaque: NativeSurfaceId,
+    /// Native surface for alpha tiles
+    pub alpha: NativeSurfaceId,
+}
+
 /// Represents a cache of tiles that make up a picture primitives.
 pub struct TileCacheInstance {
     /// Index of the tile cache / slice for this frame builder. It's determined
     /// by the setup_picture_caching method during flattening, which splits the
     /// picture tree into multiple slices. It's used as a simple input to the tile
     /// keys. It does mean we invalidate tiles if a new layer gets inserted / removed
     /// between display lists - this seems very unlikely to occur on most pages, but
     /// can be revisited if we ever notice that.
@@ -2118,25 +2153,23 @@ pub struct TileCacheInstance {
     /// we don't want to constantly invalidate and reallocate different tile size
     /// configuration each frame.
     frames_until_size_eval: usize,
     /// The current fractional offset of the cached picture
     fract_offset: PictureVector2D,
     /// keep around the hash map used as compare_cache to avoid reallocating it each
     /// frame.
     compare_cache: FastHashMap<PrimitiveComparisonKey, PrimitiveCompareResult>,
-    /// The allocated compositor surface for this picture cache. May be None if
+    /// The allocated compositor surfaces for this picture cache. May be None if
     /// not using native compositor, or if the surface was destroyed and needs
     /// to be reallocated next time this surface contains valid tiles.
-    pub native_surface_id: Option<NativeSurfaceId>,
+    pub native_surface: Option<NativeSurface>,
     /// The current device position of this cache. Used to set the compositor
     /// offset of the surface when building the visual tree.
     pub device_position: DevicePoint,
-    /// True if the entire picture cache surface is opaque.
-    is_opaque: bool,
     /// The currently considered tile size override. Used to check if we should
     /// re-evaluate tile size, even if the frame timer hasn't expired.
     tile_size_override: Option<DeviceIntSize>,
     /// List of external surfaces that have been promoted from primitives
     /// in this tile cache.
     pub external_surfaces: Vec<ExternalSurfaceDescriptor>,
 }
 
@@ -2182,19 +2215,18 @@ impl TileCacheInstance {
             subpixel_mode: SubpixelMode::Allow,
             root_transform: TransformKey::Local,
             shared_clips,
             shared_clip_chain,
             current_tile_size: DeviceIntSize::zero(),
             frames_until_size_eval: 0,
             fract_offset: PictureVector2D::zero(),
             compare_cache: FastHashMap::default(),
-            native_surface_id: None,
+            native_surface: None,
             device_position: DevicePoint::zero(),
-            is_opaque: true,
             tile_size_override: None,
             external_surfaces: Vec::new(),
         }
     }
 
     /// Returns true if this tile cache is considered opaque.
     pub fn is_opaque(&self) -> bool {
         // If known opaque due to background clear color and being the first slice.
@@ -2309,18 +2341,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;
+            self.native_surface = prev_state.native_surface;
 
             fn recycle_map<K: std::cmp::Eq + std::hash::Hash, V>(
                 ideal_len: usize,
                 dest: &mut FastHashMap<K, V>,
                 src: FastHashMap<K, V>,
             ) {
                 if dest.capacity() < src.capacity() {
                     if src.capacity() < 3 * ideal_len {
@@ -2372,18 +2403,19 @@ impl TileCacheInstance {
                 }
             };
 
             // If the desired tile size has changed, then invalidate and drop any
             // existing tiles.
             if desired_tile_size != self.current_tile_size {
                 // Destroy any native surfaces on the tiles that will be dropped due
                 // to resizing.
-                if let Some(native_surface_id) = self.native_surface_id.take() {
-                    frame_state.resource_cache.destroy_compositor_surface(native_surface_id);
+                if let Some(native_surface) = self.native_surface.take() {
+                    frame_state.resource_cache.destroy_compositor_surface(native_surface.opaque);
+                    frame_state.resource_cache.destroy_compositor_surface(native_surface.alpha);
                 }
                 self.tiles.clear();
                 self.current_tile_size = desired_tile_size;
             }
 
             // Reset counter until next evaluating the desired tile size. This is an
             // arbitrary value.
             self.frames_until_size_eval = 120;
@@ -2567,43 +2599,45 @@ impl TileCacheInstance {
             //     result in allocating _much_ smaller GPU surfaces for cases where the
             //     true off-screen surface size is very large.
             if tile.is_visible {
                 world_culling_rect = world_culling_rect.union(&tile.world_tile_rect);
             }
         }
 
         // If compositor mode is changed, need to drop all incompatible tiles.
-        match (frame_context.config.compositor_kind, self.native_surface_id) {
-            (CompositorKind::Draw { .. }, Some(_)) => {
-                frame_state.composite_state.destroy_native_tiles(
-                    self.tiles.values_mut(),
-                    frame_state.resource_cache,
-                );
+        match frame_context.config.compositor_kind {
+            CompositorKind::Draw { .. } => {
                 for tile in self.tiles.values_mut() {
-                    tile.surface = None;
-                    // Invalidate the entire tile to force a redraw.
-                    tile.invalidate(None, InvalidationReason::CompositorKindChanged);
+                    if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { ref mut id, .. }, .. }) = tile.surface {
+                        if let Some(id) = id.take() {
+                            frame_state.resource_cache.destroy_compositor_tile(id);
+                            tile.surface = None;
+                            // Invalidate the entire tile to force a redraw.
+                            tile.invalidate(None, InvalidationReason::CompositorKindChanged);
+                        }
+                    }
                 }
-                if let Some(native_surface_id) = self.native_surface_id.take() {
-                    frame_state.resource_cache.destroy_compositor_surface(native_surface_id);
+
+                if let Some(native_surface) = self.native_surface.take() {
+                    frame_state.resource_cache.destroy_compositor_surface(native_surface.opaque);
+                    frame_state.resource_cache.destroy_compositor_surface(native_surface.alpha);
                 }
             }
-            (CompositorKind::Native { .. }, None) => {
+            CompositorKind::Native { .. } => {
                 // This could hit even when compositor mode is not changed,
                 // then we need to check if there are incompatible tiles.
                 for tile in self.tiles.values_mut() {
                     if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::TextureCache { .. }, .. }) = tile.surface {
                         tile.surface = None;
                         // Invalidate the entire tile to force a redraw.
                         tile.invalidate(None, InvalidationReason::CompositorKindChanged);
                     }
                 }
             }
-            (_, _) => {}
         }
 
         world_culling_rect
     }
 
     /// Update the dependencies for each tile for a given primitive instance.
     pub fn update_prim_dependencies(
         &mut self,
@@ -3188,48 +3222,18 @@ impl TileCacheInstance {
         let mut state = TilePostUpdateState {
             resource_cache: frame_state.resource_cache,
             composite_state: frame_state.composite_state,
             compare_cache: &mut self.compare_cache,
         };
 
         // Step through each tile and invalidate if the dependencies have changed. Determine
         // the current opacity setting and whether it's changed.
-        let mut tile_cache_is_opaque = true;
         for tile in self.tiles.values_mut() {
-            if tile.post_update(&ctx, &mut state, frame_context) {
-                tile_cache_is_opaque &= tile.is_opaque;
-            }
-        }
-
-        // If opacity changed, the native compositor surface and all tiles get invalidated.
-        // (this does nothing if not using native compositor mode).
-        // TODO(gw): This property probably changes very rarely, so it is OK to invalidate
-        //           everything in this case. If it turns out that this isn't true, we could
-        //           consider other options, such as per-tile opacity (natively supported
-        //           on CoreAnimation, and supported if backed by non-virtual surfaces in
-        //           DirectComposition).
-        if self.is_opaque != tile_cache_is_opaque {
-            if let Some(native_surface_id) = self.native_surface_id.take() {
-                // Since the native surface will be destroyed, need to clear the compositor tile
-                // handle for all tiles. This means the tiles will be reallocated on demand
-                // when the tiles are added to render tasks.
-                for tile in self.tiles.values_mut() {
-                    if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { ref mut id, .. }, .. }) = tile.surface {
-                        *id = None;
-                    }
-                    // Invalidate the entire tile to force a redraw.
-                    tile.invalidate(None, InvalidationReason::SurfaceOpacityChanged {
-                                            became_opaque: tile_cache_is_opaque });
-                }
-                // Destroy the compositor surface. It will be reallocated with the correct
-                // opacity flag when render tasks are generated for tiles.
-                frame_state.resource_cache.destroy_compositor_surface(native_surface_id);
-            }
-            self.is_opaque = tile_cache_is_opaque;
+            tile.post_update(&ctx, &mut state, frame_context);
         }
 
         // When under test, record a copy of the dirty region to support
         // invalidation testing in wrench.
         if frame_context.config.testing {
             frame_state.scratch.recorded_dirty_regions.push(self.dirty_region.record());
         }
     }
@@ -4051,29 +4055,28 @@ impl PicturePrimitive {
     /// Destroy an existing picture. This is called just before
     /// a frame builder is replaced with a newly built scene. It
     /// gives a picture a chance to retain any cached tiles that
     /// may be useful during the next scene build.
     pub fn destroy(
         &mut self,
         retained_tiles: &mut RetainedTiles,
     ) {
-        if let Some(mut tile_cache) = self.tile_cache.take() {
+        if let Some(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,
+                        native_surface: tile_cache.native_surface,
                         allocations: PictureCacheRecycledAllocations {
                             old_opacity_bindings: tile_cache.old_opacity_bindings,
                             old_color_bindings: tile_cache.old_color_bindings,
                             compare_cache: tile_cache.compare_cache,
                         },
                     },
                 );
             }
@@ -4619,30 +4622,46 @@ impl PicturePrimitive {
                                             );
                                         }
                                     }
                                     SurfaceTextureDescriptor::Native { id } => {
                                         if id.is_none() {
                                             // Allocate a native surface id if we're in native compositing mode,
                                             // and we don't have a surface yet (due to first frame, or destruction
                                             // due to tile size changing etc).
-                                            if tile_cache.native_surface_id.is_none() {
-                                                let surface_id = frame_state
+                                            if tile_cache.native_surface.is_none() {
+                                                let opaque = frame_state
+                                                    .resource_cache
+                                                    .create_compositor_surface(
+                                                        tile_cache.current_tile_size,
+                                                        true,
+                                                    );
+
+                                                let alpha = frame_state
                                                     .resource_cache
                                                     .create_compositor_surface(
                                                         tile_cache.current_tile_size,
-                                                        tile_cache.is_opaque,
+                                                        false,
                                                     );
 
-                                                tile_cache.native_surface_id = Some(surface_id);
+                                                tile_cache.native_surface = Some(NativeSurface {
+                                                    opaque,
+                                                    alpha,
+                                                });
                                             }
 
                                             // Create the tile identifier and allocate it.
+                                            let surface_id = if tile.is_opaque {
+                                                tile_cache.native_surface.as_ref().unwrap().opaque
+                                            } else {
+                                                tile_cache.native_surface.as_ref().unwrap().alpha
+                                            };
+
                                             let tile_id = NativeTileId {
-                                                surface_id: tile_cache.native_surface_id.unwrap(),
+                                                surface_id,
                                                 x: tile.tile_offset.x,
                                                 y: tile.tile_offset.y,
                                             };
 
                                             frame_state.resource_cache.create_compositor_tile(tile_id);
 
                                             *id = Some(tile_id);
                                         }