Bug 1594305 - Only allocate compositor surfaces for tiles that are not occluded r=nical
authorGlenn Watson <git@intuitionlibrary.com>
Wed, 06 Nov 2019 19:05:43 +0000
changeset 500923 70442369cd48e37b05d2982f4109622cf8209bc6
parent 500922 fd706e42d049c1ccca0e372bc240019a41178361
child 500924 55bcd7df1f7ad1748065e0fa8d1989fc9345c6f6
push id114166
push userapavel@mozilla.com
push dateThu, 07 Nov 2019 10:04:01 +0000
treeherdermozilla-inbound@d271c572a9bc [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnical
bugs1594305
milestone72.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 1594305 - Only allocate compositor surfaces for tiles that are not occluded r=nical Tiles that are occluded are generally never seen, or only seen occasionally. To reduce the number of compositor surfaces: * Defer native surface allocation until after occlusion culling occurs. * If a tile has a native surface, then becomes occluded, drop the surface. With this scheme, the number of unused native surfaces will always be 0 on a page that doesn't have scrolling. For a page that has a scrollable region, there will be a small number of unused tiles retained. The unused tiles are those that are (a) not occluded (b) not currently visible (c) are in the display port. We retain these for a small amount of time in case they get scrolled back on screen. This makes the allocation patterns for native surfaces match the way that picture cache surfaces are allocated for simple compositing mode. Differential Revision: https://phabricator.services.mozilla.com/D51973
gfx/wr/webrender/src/composite.rs
gfx/wr/webrender/src/picture.rs
--- a/gfx/wr/webrender/src/composite.rs
+++ b/gfx/wr/webrender/src/composite.rs
@@ -1,23 +1,27 @@
 /* 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::ColorF;
 use api::units::{DeviceRect, DeviceIntSize, DeviceIntRect, DeviceIntPoint, WorldRect, DevicePixelScale};
 use crate::gpu_types::{ZBufferId, ZBufferIdGenerator};
-use crate::picture::{ResolvedSurfaceTexture, SurfaceTextureDescriptor};
+use crate::picture::{ResolvedSurfaceTexture};
 use std::{ops, u64};
+use std::sync::atomic::{AtomicU64, Ordering};
 
 /*
  Types and definitions related to compositing picture cache tiles
  and/or OS compositor integration.
  */
 
+// Counter for generating unique native surface ids
+static NEXT_NATIVE_SURFACE_ID: AtomicU64 = AtomicU64::new(0);
+
 /// Describes details of an operation to apply to a native surface
 #[derive(Debug, Clone)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub enum NativeSurfaceOperationDetails {
     CreateSurface {
         size: DeviceIntSize,
         is_opaque: bool,
@@ -187,34 +191,32 @@ impl CompositeState {
             occluders: Vec::new(),
         }
     }
 
     /// Queue up allocation of a new OS native compositor surface with the
     /// specified id and dimensions.
     pub fn create_surface(
         &mut self,
-        id: NativeSurfaceId,
         size: DeviceIntSize,
         is_opaque: bool,
-    ) -> SurfaceTextureDescriptor {
+    ) -> NativeSurfaceId {
+        let id = NativeSurfaceId(NEXT_NATIVE_SURFACE_ID.fetch_add(1, Ordering::Relaxed));
+
         self.native_surface_updates.push(
             NativeSurfaceOperation {
                 id,
                 details: NativeSurfaceOperationDetails::CreateSurface {
                     size,
                     is_opaque,
                 }
             }
         );
 
-        SurfaceTextureDescriptor::NativeSurface {
-            id,
-            size,
-        }
+        id
     }
 
     /// Queue up destruction of an existing native OS surface. This is used when
     /// a picture cache tile is dropped or resized.
     pub fn destroy_surface(
         &mut self,
         id: NativeSurfaceId,
     ) {
--- a/gfx/wr/webrender/src/picture.rs
+++ b/gfx/wr/webrender/src/picture.rs
@@ -435,17 +435,17 @@ pub enum SurfaceTextureDescriptor {
     /// in the WR texture cache.
     TextureCache {
         handle: TextureCacheHandle
     },
     /// When using an OS compositor, the tile is drawn into a native
     /// surface identified by arbitrary id.
     NativeSurface {
         /// The arbitrary id of this surface.
-        id: NativeSurfaceId,
+        id: Option<NativeSurfaceId>,
         /// Size in device pixels of the native surface.
         size: DeviceIntSize,
     },
 }
 
 /// This is the same as a `SurfaceTextureDescriptor` but has been resolved
 /// into a texture cache handle (if appropriate) that can be used by the
 /// batching and compositing code in the renderer.
@@ -479,17 +479,17 @@ impl SurfaceTextureDescriptor {
 
                 ResolvedSurfaceTexture::TextureCache {
                     texture: cache_item.texture_id,
                     layer: cache_item.texture_layer,
                 }
             }
             SurfaceTextureDescriptor::NativeSurface { id, size } => {
                 ResolvedSurfaceTexture::NativeSurface {
-                    id: *id,
+                    id: id.expect("bug: native surface not allocated"),
                     size: *size,
                 }
             }
         }
     }
 }
 
 /// The backing surface for this tile.
@@ -866,36 +866,35 @@ impl Tile {
                 }
             }
         } else {
             // If this tile will be backed by a surface, we want to retain
             // the texture handle from the previous frame, if possible. If
             // the tile was previously a color, or not set, then just set
             // up a new texture cache handle.
             match self.surface.take() {
-                Some(TileSurface::Texture { descriptor, visibility_mask }) => {
+                Some(TileSurface::Texture { mut descriptor, visibility_mask }) => {
                     // If opacity changed, and this is a native OS compositor surface,
                     // it needs to be recreated.
                     // TODO(gw): This is a limitation of the DirectComposite APIs. It might
                     //           make sense on other platforms to be able to change this as
                     //           a property on a surface, if we ever see pages where this
                     //           is changing frequently.
                     if opacity_changed {
-                        if let SurfaceTextureDescriptor::NativeSurface { id, size } = descriptor {
+                        if let SurfaceTextureDescriptor::NativeSurface { ref mut id, .. } = descriptor {
                             // Reset the dirty rect and tile validity in this case, to
                             // force the new tile to be completely redrawn.
                             self.dirty_rect = self.rect;
                             self.is_valid = false;
 
-                            state.composite_state.destroy_surface(id);
-                            state.composite_state.create_surface(
-                                id,
-                                size,
-                                self.is_opaque,
-                            );
+                            // If this tile has a currently allocated native surface, destroy it. It
+                            // will be re-allocated next time it's determined to be visible.
+                            if let Some(id) = id.take() {
+                                state.composite_state.destroy_surface(id);
+                            }
                         }
                     }
 
                     // Reuse the existing descriptor and vis mask
                     TileSurface::Texture {
                         descriptor,
                         visibility_mask,
                     }
@@ -909,23 +908,23 @@ impl Tile {
                         CompositorKind::Draw { .. } => {
                             // For a texture cache entry, create an invalid handle that
                             // will be allocated when update_picture_cache is called.
                             SurfaceTextureDescriptor::TextureCache {
                                 handle: TextureCacheHandle::invalid(),
                             }
                         }
                         CompositorKind::Native { .. } => {
-                            // For a new native OS surface, we need to queue up creation
-                            // of a native surface to be passed to the compositor interface.
-                            state.composite_state.create_surface(
-                                NativeSurfaceId(self.id.0 as u64),
-                                ctx.current_tile_size,
-                                self.is_opaque,
-                            )
+                            // Create a native surface surface descriptor, but don't allocate
+                            // a surface yet. The surface is allocated *after* occlusion
+                            // culling occurs, so that only visible tiles allocate GPU memory.
+                            SurfaceTextureDescriptor::NativeSurface {
+                                id: None,
+                                size: ctx.current_tile_size,
+                            }
                         }
                     };
 
                     TileSurface::Texture {
                         descriptor,
                         visibility_mask: PrimitiveVisibilityMask::empty(),
                     }
                 }
@@ -3323,37 +3322,47 @@ impl PicturePrimitive {
                             let tile_draw_rect = match world_clip_rect.intersection(&tile.world_rect) {
                                 Some(rect) => rect,
                                 None => {
                                     tile.is_visible = false;
                                     continue;
                                 }
                             };
 
+                            let surface = tile.surface.as_mut().expect("no tile surface set!");
+
                             // If that draw rect is occluded by some set of tiles in front of it,
                             // then mark it as not visible and skip drawing. When it's not occluded
                             // it will fail this test, and get rasterized by the render task setup
                             // code below.
                             if frame_state.composite_state.is_tile_occluded(tile_cache.slice, tile_draw_rect) {
+                                // If this tile has an allocated native surface, free it, since it's completely
+                                // occluded. We will need to re-allocate this surface if it becomes visible,
+                                // but that's likely to be rare (e.g. when there is no content display list
+                                // for a frame or two during a tab switch).
+                                if let TileSurface::Texture { descriptor: SurfaceTextureDescriptor::NativeSurface { id, .. }, .. } = surface {
+                                    if let Some(id) = id.take() {
+                                        frame_state.composite_state.destroy_surface(id);
+                                    }
+                                }
+
                                 tile.is_visible = false;
                                 continue;
                             }
 
                             // Register active image keys of valid tile.
                             // TODO(gw): For now, we will register images on any visible
                             //           tiles as active. This is a hotfix because video
                             //           images on Mac/Linux are not being correctly detected
                             //           as non-cacheable. We should investigate / fix the
                             //           root cause of this.
                             for image_key in &tile.current_descriptor.image_keys {
                                 frame_state.resource_cache.set_image_active(*image_key);
                             }
 
-                            let surface = tile.surface.as_mut().expect("no tile surface set!");
-
                             if frame_context.debug_flags.contains(DebugFlags::PICTURE_CACHING_DBG) {
                                 tile.root.draw_debug_rects(
                                     &map_pic_to_world,
                                     tile.is_opaque,
                                     scratch,
                                     frame_context.global_device_pixel_scale,
                                 );
 
@@ -3368,52 +3377,73 @@ impl PicturePrimitive {
                                                 tile_cache.slice,
                                                 tile.is_opaque,
                                                 surface.kind(),
                                         ),
                                     );
                                 }
                             }
 
-                            if let TileSurface::Texture { descriptor: SurfaceTextureDescriptor::TextureCache { ref handle, .. }, .. } = surface {
-                                // Invalidate if the backing texture was evicted.
-                                if frame_state.resource_cache.texture_cache.is_allocated(handle) {
-                                    // Request the backing texture so it won't get evicted this frame.
-                                    // We specifically want to mark the tile texture as used, even
-                                    // if it's detected not visible below and skipped. This is because
-                                    // we maintain the set of tiles we care about based on visibility
-                                    // during pre_update. If a tile still exists after that, we are
-                                    // assuming that it's either visible or we want to retain it for
-                                    // a while in case it gets scrolled back onto screen soon.
-                                    // TODO(gw): Consider switching to manual eviction policy?
-                                    frame_state.resource_cache.texture_cache.request(handle, frame_state.gpu_cache);
-                                } else {
-                                    // If the texture was evicted on a previous frame, we need to assume
-                                    // that the entire tile rect is dirty.
-                                    tile.is_valid = false;
-                                    tile.dirty_rect = tile.rect;
+                            if let TileSurface::Texture { descriptor, .. } = surface {
+                                match descriptor {
+                                    SurfaceTextureDescriptor::TextureCache { ref handle, .. } => {
+                                        // Invalidate if the backing texture was evicted.
+                                        if frame_state.resource_cache.texture_cache.is_allocated(handle) {
+                                            // Request the backing texture so it won't get evicted this frame.
+                                            // We specifically want to mark the tile texture as used, even
+                                            // if it's detected not visible below and skipped. This is because
+                                            // we maintain the set of tiles we care about based on visibility
+                                            // during pre_update. If a tile still exists after that, we are
+                                            // assuming that it's either visible or we want to retain it for
+                                            // a while in case it gets scrolled back onto screen soon.
+                                            // TODO(gw): Consider switching to manual eviction policy?
+                                            frame_state.resource_cache.texture_cache.request(handle, frame_state.gpu_cache);
+                                        } else {
+                                            // If the texture was evicted on a previous frame, we need to assume
+                                            // that the entire tile rect is dirty.
+                                            tile.is_valid = false;
+                                            tile.dirty_rect = tile.rect;
+                                        }
+                                    }
+                                    SurfaceTextureDescriptor::NativeSurface { id, .. } => {
+                                        if id.is_none() {
+                                            // There is no current surface allocation, so ensure the entire tile is invalidated
+                                            tile.is_valid = false;
+                                            tile.dirty_rect = tile.rect;
+                                        }
+                                    }
                                 }
                             }
 
                             // Update the world dirty rect
                             tile.world_dirty_rect = map_pic_to_world.map(&tile.dirty_rect).expect("bug");
 
                             if tile.is_valid {
                                 continue;
                             }
 
                             // Ensure that this texture is allocated.
                             if let TileSurface::Texture { ref mut descriptor, ref mut visibility_mask } = surface {
-                                if let SurfaceTextureDescriptor::TextureCache { ref mut handle } = descriptor {
-                                    if !frame_state.resource_cache.texture_cache.is_allocated(handle) {
-                                        frame_state.resource_cache.texture_cache.update_picture_cache(
-                                            tile_cache.current_tile_size,
-                                            handle,
-                                            frame_state.gpu_cache,
-                                        );
+                                match descriptor {
+                                    SurfaceTextureDescriptor::TextureCache { ref mut handle } => {
+                                        if !frame_state.resource_cache.texture_cache.is_allocated(handle) {
+                                            frame_state.resource_cache.texture_cache.update_picture_cache(
+                                                tile_cache.current_tile_size,
+                                                handle,
+                                                frame_state.gpu_cache,
+                                            );
+                                        }
+                                    }
+                                    SurfaceTextureDescriptor::NativeSurface { id, size } => {
+                                        if id.is_none() {
+                                            *id = Some(frame_state.composite_state.create_surface(
+                                                *size,
+                                                tile.is_opaque,
+                                            ));
+                                        }
                                     }
                                 }
 
                                 *visibility_mask = PrimitiveVisibilityMask::empty();
                                 let dirty_region_index = tile_cache.dirty_region.dirty_rects.len();
 
                                 // If we run out of dirty regions, then force the last dirty region to
                                 // be a union of any remaining regions. This is an inefficiency, in that
@@ -4866,14 +4896,16 @@ impl CompositeState {
         // by the texture cache. For native compositor mode, we need to explicitly
         // invoke a callback to the client to destroy that surface.
         if let CompositorKind::Native { .. } = self.compositor_kind {
             for tile in tiles_iter {
                 // Only destroy native surfaces that have been allocated. It's
                 // possible for display port tiles to be created that never
                 // come on screen, and thus never get a native surface allocated.
                 if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::NativeSurface { id, .. }, .. }) = tile.surface {
-                    self.destroy_surface(id);
+                    if let Some(id) = id {
+                        self.destroy_surface(id);
+                    }
                 }
             }
         }
     }
 }