Bug 1591526 - Use occlusion culling to reduce number of picture cache tiles. r=nical

During the visibility pass, picture caches register themselves as
occluders, if they have a valid opaque region.

During the primitive preparation pass, each tile queries if it is
occluded. If so, it skips rasterization and compositing of itself
for this frame. In this way, tiles that are always occluded never
end up allocating GPU memory for a surface.

Also add support during primitive dependency generation for opaque
images to be considered as part of the opaque backdrop region.

Differential Revision: https://phabricator.services.mozilla.com/D51555

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Glenn Watson 2019-11-04 19:27:52 +00:00
parent 3ff4d952e6
commit 56dcacf7f8
4 changed files with 320 additions and 46 deletions

View File

@ -1202,6 +1202,10 @@ impl BatchBuilder {
let z_id = composite_state.z_generator.next();
for key in &tile_cache.tiles_to_draw {
let tile = &tile_cache.tiles[key];
if !tile.is_visible {
// This can occur when a tile is found to be occluded during frame building.
continue;
}
let device_rect = (tile.world_rect * ctx.global_device_pixel_scale).round();
let dirty_rect = (tile.world_dirty_rect * ctx.global_device_pixel_scale).round();
let surface = tile.surface.as_ref().expect("no tile surface set!");

View File

@ -3,9 +3,10 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use api::ColorF;
use api::units::{DeviceRect, DeviceIntSize, DeviceIntRect, DeviceIntPoint};
use api::units::{DeviceRect, DeviceIntSize, DeviceIntRect, DeviceIntPoint, WorldRect, DevicePixelScale};
use crate::gpu_types::{ZBufferId, ZBufferIdGenerator};
use crate::picture::{ResolvedSurfaceTexture, SurfaceTextureDescriptor};
use std::ops;
/*
Types and definitions related to compositing picture cache tiles
@ -120,6 +121,14 @@ impl Default for CompositorKind {
}
}
/// Information about an opaque surface used to occlude tiles.
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
struct Occluder {
slice: usize,
device_rect: DeviceIntRect,
}
/// The list of tiles to be drawn this frame
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
@ -142,6 +151,10 @@ pub struct CompositeState {
pub compositor_kind: CompositorKind,
/// Picture caching may be disabled dynamically, based on debug flags, pinch zoom etc.
pub picture_caching_is_enabled: bool,
/// The overall device pixel scale, used for tile occlusion conversions.
global_device_pixel_scale: DevicePixelScale,
/// List of registered occluders
occluders: Vec<Occluder>,
}
impl CompositeState {
@ -150,6 +163,7 @@ impl CompositeState {
pub fn new(
compositor_kind: CompositorKind,
mut picture_caching_is_enabled: bool,
global_device_pixel_scale: DevicePixelScale,
) -> Self {
// The native compositor interface requires picture caching to work, so
// force it here and warn if it was disabled.
@ -169,6 +183,8 @@ impl CompositeState {
native_surface_updates: Vec::new(),
compositor_kind,
picture_caching_is_enabled,
global_device_pixel_scale,
occluders: Vec::new(),
}
}
@ -209,6 +225,52 @@ impl CompositeState {
}
);
}
/// Register an occluder during picture cache updates that can be
/// used during frame building to occlude tiles.
pub fn register_occluder(
&mut self,
slice: usize,
rect: WorldRect,
) {
let device_rect = (rect * self.global_device_pixel_scale).round().to_i32();
self.occluders.push(Occluder {
device_rect,
slice,
});
}
/// Returns true if a tile with the specified rectangle and slice
/// is occluded by an opaque surface in front of it.
pub fn is_tile_occluded(
&self,
slice: usize,
rect: WorldRect,
) -> bool {
// It's often the case that a tile is only occluded by considering multiple
// picture caches in front of it (for example, the background tiles are
// often occluded by a combination of the content slice + the scrollbar slices).
// The basic algorithm is:
// For every occluder:
// If this occluder is in front of the tile we are querying:
// Clip the occluder rectangle to the query rectangle.
// Calculate the total non-overlapping area of those clipped occluders.
// If the cumulative area of those occluders is the same as the area of the query tile,
// Then the entire tile must be occluded and can be skipped during rasterization and compositing.
// Get the reference area we will compare against.
let device_rect = (rect * self.global_device_pixel_scale).round().to_i32();
let ref_area = device_rect.size.width * device_rect.size.height;
// Calculate the non-overlapping area of the valid occluders.
let cover_area = area_of_occluders(&self.occluders, slice, &device_rect);
debug_assert!(cover_area <= ref_area);
// Check if the tile area is completely covered
ref_area == cover_area
}
}
/// An arbitrary identifier for a native (OS compositor) surface
@ -311,3 +373,112 @@ pub trait Compositor {
/// that the OS composite transaction should be applied.
fn end_frame(&mut self);
}
/// Return the total area covered by a set of occluders, accounting for
/// overlapping areas between those rectangles.
fn area_of_occluders(
occluders: &[Occluder],
slice: usize,
clip_rect: &DeviceIntRect,
) -> i32 {
// This implementation is based on the article https://leetcode.com/articles/rectangle-area-ii/.
// This is not a particularly efficient implementation (it skips building segment trees), however
// we typically use this where the length of the rectangles array is < 10, so simplicity is more important.
let mut area = 0;
// Whether this event is the start or end of a rectangle
#[derive(Debug)]
enum EventKind {
Begin,
End,
}
// A list of events on the y-axis, with the rectangle range that it affects on the x-axis
#[derive(Debug)]
struct Event {
y: i32,
x_range: ops::Range<i32>,
kind: EventKind,
}
impl Event {
fn new(y: i32, kind: EventKind, x0: i32, x1: i32) -> Self {
Event {
y,
x_range: ops::Range {
start: x0,
end: x1,
},
kind,
}
}
}
// Step through each rectangle and build the y-axis event list
let mut events = Vec::with_capacity(occluders.len() * 2);
for occluder in occluders {
// Only consider occluders in front of this rect
if occluder.slice > slice {
// Clip the source rect to the rectangle we care about, since we only
// want to record area for the tile we are comparing to.
if let Some(rect) = occluder.device_rect.intersection(clip_rect) {
let x0 = rect.origin.x;
let x1 = x0 + rect.size.width;
events.push(Event::new(rect.origin.y, EventKind::Begin, x0, x1));
events.push(Event::new(rect.origin.y + rect.size.height, EventKind::End, x0, x1));
}
}
}
// If we didn't end up with any valid events, the area must be 0
if events.is_empty() {
return 0;
}
// Sort the events by y-value
events.sort_by_key(|e| e.y);
let mut active: Vec<ops::Range<i32>> = Vec::new();
let mut cur_y = events[0].y;
// Step through each y interval
for event in &events {
// This is the dimension of the y-axis we are accumulating areas for
let dy = event.y - cur_y;
// If we have active events covering x-ranges in this y-interval, process them
if dy != 0 && !active.is_empty() {
assert!(dy > 0);
// Step through the x-ranges, ordered by x0 of each event
active.sort_by_key(|i| i.start);
let mut query = 0;
let mut cur = active[0].start;
// Accumulate the non-overlapping x-interval that contributes to area for this y-interval.
for interval in &active {
cur = interval.start.max(cur);
query += (interval.end - cur).max(0);
cur = cur.max(interval.end);
}
// Accumulate total area for this y-interval
area += query * dy;
}
// Update the active events list
match event.kind {
EventKind::Begin => {
active.push(event.x_range.clone());
}
EventKind::End => {
let index = active.iter().position(|i| *i == event.x_range).unwrap();
active.remove(index);
}
}
cur_y = event.y;
}
area
}

View File

@ -155,6 +155,7 @@ pub struct FrameBuildingState<'a> {
pub segment_builder: SegmentBuilder,
pub surfaces: &'a mut Vec<SurfaceInfo>,
pub dirty_region_stack: Vec<DirtyRegion>,
pub composite_state: &'a mut CompositeState,
}
impl<'a> FrameBuildingState<'a> {
@ -379,6 +380,7 @@ impl FrameBuilder {
segment_builder: SegmentBuilder::new(),
surfaces,
dirty_region_stack: Vec::new(),
composite_state,
};
frame_state
@ -516,6 +518,7 @@ impl FrameBuilder {
let mut composite_state = CompositeState::new(
scene.config.compositor_kind,
picture_caching_is_enabled,
global_device_pixel_scale,
);
let main_render_task_id = self.build_layer_screen_rects_and_cull_layers(

View File

@ -95,7 +95,7 @@ use smallvec::SmallVec;
use std::{mem, u8, marker, u32};
use std::sync::atomic::{AtomicUsize, Ordering};
use crate::texture_cache::TextureCacheHandle;
use crate::util::{TransformedRectKind, MatrixHelpers, MaxRect, scale_factors, VecHelper};
use crate::util::{TransformedRectKind, MatrixHelpers, MaxRect, scale_factors, VecHelper, RectHelpers};
use crate::filterdata::{FilterDataHandle};
/// Specify whether a surface allows subpixel AA text rendering.
@ -824,6 +824,7 @@ impl Tile {
// color tiles. We can definitely support this in DC, so this
// should be added as a follow up.
let is_simple_prim =
ctx.backdrop.kind.can_be_promoted_to_compositor_surface() &&
self.current_descriptor.prims.len() == 1 &&
self.is_opaque &&
supports_simple_prims;
@ -842,6 +843,10 @@ impl Tile {
BackdropKind::Clear => {
TileSurface::Clear
}
BackdropKind::Image => {
// This should be prevented by the is_simple_prim check above.
unreachable!();
}
}
} else {
// If this tile will be backed by a surface, we want to retain
@ -1216,6 +1221,17 @@ enum BackdropKind {
color: ColorF,
},
Clear,
Image,
}
impl BackdropKind {
/// Returns true if the compositor can directly draw this backdrop.
fn can_be_promoted_to_compositor_surface(&self) -> bool {
match self {
BackdropKind::Color { .. } | BackdropKind::Clear => true,
BackdropKind::Image => false,
}
}
}
/// Stores information about the calculated opaque backdrop of this slice.
@ -1749,6 +1765,10 @@ impl TileCacheInstance {
}
}
// Certain primitives may select themselves to be a backdrop candidate, which is
// then applied below.
let mut backdrop_candidate = None;
// For pictures, we don't (yet) know the valid clip rect, so we can't correctly
// use it to calculate the local bounding rect for the tiles. If we include them
// then we may calculate a bounding rect that is too large, since it won't include
@ -1770,46 +1790,16 @@ impl TileCacheInstance {
}
PrimitiveInstanceKind::Rectangle { data_handle, opacity_binding_index, .. } => {
if opacity_binding_index == OpacityBindingIndex::INVALID {
// Check a number of conditions to see if we can consider this
// primitive as an opaque rect. Several of these are conservative
// checks and could be relaxed in future. However, these checks
// are quick and capture the common cases of background rects.
// Specifically, we currently require:
// - No opacity binding (to avoid resolving the opacity here).
// - Color.a >= 1.0 (the primitive is opaque).
// - Same coord system as picture cache (ensures rects are axis-aligned).
// - No clip masks exist.
let on_picture_surface = surface_index == self.surface_index;
// Rectangles can only form a backdrop candidate if they are known opaque.
// TODO(gw): We could resolve the opacity binding here, but the common
// case for background rects is that they don't have animated opacity.
let color = match data_stores.prim[data_handle].kind {
PrimitiveTemplateKind::Rectangle { color, .. } => color,
_ => unreachable!(),
};
let prim_is_opaque = color.a >= 1.0;
let same_coord_system = {
let prim_spatial_node = &clip_scroll_tree
.spatial_nodes[prim_spatial_node_index.0 as usize];
let surface_spatial_node = &clip_scroll_tree
.spatial_nodes[self.spatial_node_index.0 as usize];
prim_spatial_node.coordinate_system_id == surface_spatial_node.coordinate_system_id
};
if let Some(ref clip_chain) = prim_clip_chain {
if prim_is_opaque && same_coord_system && !clip_chain.needs_mask && on_picture_surface {
if clip_chain.pic_clip_rect.contains_rect(&self.backdrop.rect) {
self.backdrop = BackdropInfo {
rect: clip_chain.pic_clip_rect,
kind: BackdropKind::Color {
color,
},
};
}
}
};
if color.a >= 1.0 {
backdrop_candidate = Some(BackdropKind::Color { color });
}
} else {
let opacity_binding = &opacity_binding_store[opacity_binding_index];
for binding in &opacity_binding.bindings {
@ -1824,7 +1814,14 @@ impl TileCacheInstance {
let image_instance = &image_instances[image_instance_index];
let opacity_binding_index = image_instance.opacity_binding_index;
if opacity_binding_index != OpacityBindingIndex::INVALID {
if opacity_binding_index == OpacityBindingIndex::INVALID {
if let Some(image_properties) = resource_cache.get_image_properties(image_data.key) {
// If this image is opaque, it can be considered as a possible opaque backdrop
if image_properties.descriptor.is_opaque {
backdrop_candidate = Some(BackdropKind::Image);
}
}
} else {
let opacity_binding = &opacity_binding_store[opacity_binding_index];
for binding in &opacity_binding.bindings {
prim_info.opacity_bindings.push(OpacityBinding::from(*binding));
@ -1872,12 +1869,7 @@ impl TileCacheInstance {
}
}
PrimitiveInstanceKind::Clear { .. } => {
if let Some(ref clip_chain) = prim_clip_chain {
self.backdrop = BackdropInfo {
rect: clip_chain.pic_clip_rect,
kind: BackdropKind::Clear,
};
}
backdrop_candidate = Some(BackdropKind::Clear);
}
PrimitiveInstanceKind::LineDecoration { .. } |
PrimitiveInstanceKind::NormalBorder { .. } |
@ -1888,6 +1880,53 @@ impl TileCacheInstance {
}
};
// If this primitive considers itself a backdrop candidate, apply further
// checks to see if it matches all conditions to be a backdrop.
if let Some(backdrop_candidate) = backdrop_candidate {
let is_suitable_backdrop = match backdrop_candidate {
BackdropKind::Clear => {
// Clear prims are special - they always end up in their own slice,
// and always set the backdrop. In future, we hope to completely
// remove clear prims, since they don't integrate with the compositing
// system cleanly.
true
}
BackdropKind::Image | BackdropKind::Color { .. } => {
// Check a number of conditions to see if we can consider this
// primitive as an opaque backdrop rect. Several of these are conservative
// checks and could be relaxed in future. However, these checks
// are quick and capture the common cases of background rects and images.
// Specifically, we currently require:
// - The primitive is on the main picture cache surface.
// - Same coord system as picture cache (ensures rects are axis-aligned).
// - No clip masks exist.
let on_picture_surface = surface_index == self.surface_index;
let same_coord_system = {
let prim_spatial_node = &clip_scroll_tree
.spatial_nodes[prim_spatial_node_index.0 as usize];
let surface_spatial_node = &clip_scroll_tree
.spatial_nodes[self.spatial_node_index.0 as usize];
prim_spatial_node.coordinate_system_id == surface_spatial_node.coordinate_system_id
};
same_coord_system && on_picture_surface
}
};
if is_suitable_backdrop {
if let Some(ref clip_chain) = prim_clip_chain {
if !clip_chain.needs_mask && clip_chain.pic_clip_rect.contains_rect(&self.backdrop.rect) {
self.backdrop = BackdropInfo {
rect: clip_chain.pic_clip_rect,
kind: backdrop_candidate,
}
}
}
}
}
// Record any new spatial nodes in the used list.
self.used_spatial_nodes.extend(&prim_info.spatial_nodes);
@ -1924,6 +1963,34 @@ impl TileCacheInstance {
self.tiles_to_draw.clear();
self.dirty_region.clear();
// Register the opaque region of this tile cache as an occluder, which
// is used later in the frame to occlude other tiles.
if self.backdrop.rect.is_well_formed_and_nonempty() {
let backdrop_rect = self.backdrop.rect
.intersection(&self.local_rect)
.and_then(|r| {
r.intersection(&self.local_clip_rect)
});
if let Some(backdrop_rect) = backdrop_rect {
let map_pic_to_world = SpaceMapper::new_with_target(
ROOT_SPATIAL_NODE_INDEX,
self.spatial_node_index,
frame_context.global_screen_world_rect,
frame_context.clip_scroll_tree,
);
let world_backdrop_rect = map_pic_to_world
.map(&backdrop_rect)
.expect("bug: unable to map backdrop to world space");
frame_state.composite_state.register_occluder(
self.slice,
world_backdrop_rect,
);
}
}
// Detect if the picture cache was scrolled or scaled. In this case,
// the device space dirty rects aren't applicable (until we properly
// integrate with OS compositors that can handle scrolling slices).
@ -3222,9 +3289,37 @@ impl PicturePrimitive {
let tile_cache = self.tile_cache.as_mut().unwrap();
let mut first = true;
// Get the overall world space rect of the picture cache. Used to clip
// the tile rects below for occlusion testing to the relevant area.
let local_clip_rect = tile_cache.local_rect
.intersection(&tile_cache.local_clip_rect)
.unwrap_or(PictureRect::zero());
let world_clip_rect = map_pic_to_world
.map(&local_clip_rect)
.expect("bug: unable to map clip rect");
for key in &tile_cache.tiles_to_draw {
let tile = tile_cache.tiles.get_mut(key).expect("bug: no tile found!");
// Get the world space rect that this tile will actually occupy on screem
let tile_draw_rect = match world_clip_rect.intersection(&tile.world_rect) {
Some(rect) => rect,
None => {
tile.is_visible = false;
continue;
}
};
// 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) {
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
@ -3251,8 +3346,9 @@ impl PicturePrimitive {
scratch.push_debug_string(
tile_device_rect.origin + label_offset,
debug_colors::RED,
format!("{:?}: is_opaque={} surface={}",
format!("{:?}: s={} is_opaque={} surface={}",
tile.id,
tile_cache.slice,
tile.is_opaque,
surface.kind(),
),