1/* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17import {assertDefined} from 'common/assert_utils'; 18import {Rect} from 'common/geometry/rect'; 19import {RawDataUtils} from 'parsers/raw_data_utils'; 20import {LayerFlag} from 'parsers/surface_flinger/layer_flag'; 21import { 22 Transform, 23 TransformType, 24} from 'parsers/surface_flinger/transform_utils'; 25import {GeometryFactory} from 'trace/geometry_factory'; 26import {Computation} from 'trace/tree_node/computation'; 27import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node'; 28import {PropertyTreeNode} from 'trace/tree_node/property_tree_node'; 29import {DEFAULT_PROPERTY_TREE_NODE_FACTORY} from 'trace/tree_node/property_tree_node_factory'; 30import {LayerExtractor} from './layer_extractor'; 31 32export class VisibilityPropertiesComputation implements Computation { 33 private root: HierarchyTreeNode | undefined; 34 private rootLayers: HierarchyTreeNode[] | undefined; 35 private displays: PropertyTreeNode[] = []; 36 private static readonly OFFSCREEN_LAYER_ROOT_ID = 0x7ffffffd; 37 38 setRoot(value: HierarchyTreeNode): VisibilityPropertiesComputation { 39 this.root = value; 40 this.rootLayers = value.getAllChildren().slice(); 41 return this; 42 } 43 44 executeInPlace(): void { 45 if (!this.root || !this.rootLayers) { 46 throw new Error('root not set in SF visibility computation'); 47 } 48 49 this.displays = 50 this.root.getEagerPropertyByName('displays')?.getAllChildren().slice() ?? 51 []; 52 const topDownTraversal = LayerExtractor.extractLayersTopToBottom( 53 assertDefined(this.root), 54 ); 55 56 const opaqueLayers: HierarchyTreeNode[] = []; 57 const translucentLayers: HierarchyTreeNode[] = []; 58 59 for (const layer of topDownTraversal) { 60 let isVisible = this.getIsVisible(layer); 61 if (!isVisible) { 62 layer.addEagerProperty( 63 DEFAULT_PROPERTY_TREE_NODE_FACTORY.makeCalculatedProperty( 64 layer.id, 65 'isComputedVisible', 66 isVisible, 67 ), 68 ); 69 layer.addEagerProperty( 70 DEFAULT_PROPERTY_TREE_NODE_FACTORY.makeCalculatedProperty( 71 layer.id, 72 'isHiddenByPolicy', 73 this.isHiddenByPolicy(layer), 74 ), 75 ); 76 } else { 77 const displaySize = this.getDisplaySize(layer); 78 79 const occludedBy = opaqueLayers 80 .filter((other) => { 81 if ( 82 this.getDefinedValue(other, 'layerStack') !== 83 this.getDefinedValue(layer, 'layerStack') 84 ) { 85 return false; 86 } 87 if (!this.layerContains(other, layer, displaySize)) { 88 return false; 89 } 90 const cornerRadiusOther = this.getDefinedValue( 91 other, 92 'cornerRadius', 93 ); 94 95 return ( 96 cornerRadiusOther <= 0 || 97 cornerRadiusOther === this.getDefinedValue(layer, 'cornerRadius') 98 ); 99 }) 100 .map((other) => other.id); 101 102 if (occludedBy.length > 0) { 103 isVisible = false; 104 } 105 106 layer.addEagerProperty( 107 DEFAULT_PROPERTY_TREE_NODE_FACTORY.makeCalculatedProperty( 108 layer.id, 109 'isComputedVisible', 110 isVisible, 111 ), 112 ); 113 layer.addEagerProperty( 114 DEFAULT_PROPERTY_TREE_NODE_FACTORY.makeCalculatedProperty( 115 layer.id, 116 'occludedBy', 117 occludedBy, 118 ), 119 ); 120 121 const partiallyOccludedBy = opaqueLayers 122 .filter((other) => { 123 if ( 124 this.getDefinedValue(other, 'layerStack') !== 125 this.getDefinedValue(layer, 'layerStack') 126 ) { 127 return false; 128 } 129 if (!this.layerOverlaps(other, layer, displaySize)) { 130 return false; 131 } 132 return !occludedBy.includes(other.id); 133 }) 134 .map((other) => other.id); 135 136 layer.addEagerProperty( 137 DEFAULT_PROPERTY_TREE_NODE_FACTORY.makeCalculatedProperty( 138 layer.id, 139 'partiallyOccludedBy', 140 partiallyOccludedBy, 141 ), 142 ); 143 144 const coveredBy = translucentLayers 145 .filter((other) => { 146 if ( 147 this.getDefinedValue(other, 'layerStack') !== 148 this.getDefinedValue(layer, 'layerStack') 149 ) { 150 return false; 151 } 152 return this.layerOverlaps(other, layer, displaySize); 153 }) 154 .map((other) => other.id); 155 156 layer.addEagerProperty( 157 DEFAULT_PROPERTY_TREE_NODE_FACTORY.makeCalculatedProperty( 158 layer.id, 159 'coveredBy', 160 coveredBy, 161 ), 162 ); 163 164 this.isOpaque(layer) 165 ? opaqueLayers.push(layer) 166 : translucentLayers.push(layer); 167 } 168 169 if (!isVisible) { 170 layer.addEagerProperty( 171 DEFAULT_PROPERTY_TREE_NODE_FACTORY.makeCalculatedProperty( 172 layer.id, 173 'visibilityReason', 174 this.getVisibilityReasons(layer), 175 ), 176 ); 177 } 178 } 179 } 180 181 private getIsVisible(layer: HierarchyTreeNode): boolean { 182 if (this.isHiddenByParent(layer) || this.isHiddenByPolicy(layer)) { 183 return false; 184 } 185 if (this.hasZeroAlpha(layer)) { 186 return false; 187 } 188 if ( 189 this.isActiveBufferEmpty(layer.getEagerPropertyByName('activeBuffer')) && 190 !this.hasEffects(layer) 191 ) { 192 return false; 193 } 194 return this.hasVisibleRegion(layer); 195 } 196 197 private hasVisibleRegion(layer: HierarchyTreeNode): boolean { 198 let hasVisibleRegion = false; 199 if (layer.getEagerPropertyByName('excludesCompositionState')?.getValue()) { 200 // Doesn't include state sent during composition like visible region and 201 // composition type, so we fallback on the bounds as the visible region 202 const bounds = layer.getEagerPropertyByName('bounds'); 203 hasVisibleRegion = 204 bounds !== undefined && !RawDataUtils.isEmptyObj(bounds); 205 } else { 206 const visibleRegion = layer.getEagerPropertyByName('visibleRegion'); 207 if ( 208 visibleRegion === undefined || 209 visibleRegion.getAllChildren().length === 0 210 ) { 211 hasVisibleRegion = false; 212 } else { 213 hasVisibleRegion = !this.hasValidEmptyVisibleRegion(visibleRegion); 214 } 215 } 216 return hasVisibleRegion; 217 } 218 219 private hasValidEmptyVisibleRegion(visibleRegion: PropertyTreeNode): boolean { 220 const visibleRegionRectsNode = visibleRegion.getChildByName('rect'); 221 if (!visibleRegionRectsNode) return false; 222 223 const rects = visibleRegionRectsNode.getAllChildren(); 224 return rects.every((node) => { 225 return RawDataUtils.isEmptyObj(node); 226 }); 227 } 228 229 private getVisibilityReasons(layer: HierarchyTreeNode): string[] { 230 const reasons: string[] = []; 231 232 if (this.isHiddenByPolicy(layer)) reasons.push('flag is hidden'); 233 234 if (this.isHiddenByParent(layer)) { 235 reasons.push(`hidden by parent ${this.getDefinedValue(layer, 'parent')}`); 236 } 237 238 if ( 239 this.isActiveBufferEmpty(layer.getEagerPropertyByName('activeBuffer')) 240 ) { 241 reasons.push('buffer is empty'); 242 } 243 244 if (this.hasZeroAlpha(layer)) { 245 reasons.push('alpha is 0'); 246 } 247 248 const bounds = layer.getEagerPropertyByName('bounds'); 249 if (bounds && RawDataUtils.isEmptyObj(bounds)) { 250 reasons.push('bounds is 0x0'); 251 } 252 253 const color = this.getColor(layer); 254 if ( 255 color && 256 bounds && 257 RawDataUtils.isEmptyObj(bounds) && 258 RawDataUtils.isEmptyObj(color) 259 ) { 260 reasons.push('crop is 0x0'); 261 } 262 const transform = layer.getEagerPropertyByName('transform'); 263 if (transform && !Transform.from(transform).matrix.isValid()) { 264 reasons.push('transform is invalid'); 265 } 266 267 const zOrderRelativeOf = layer 268 .getEagerPropertyByName('isRelativeOf') 269 ?.getValue(); 270 if (zOrderRelativeOf === -1) { 271 reasons.push('relativeOf layer has been removed'); 272 } 273 274 if ( 275 this.isActiveBufferEmpty(layer.getEagerPropertyByName('activeBuffer')) && 276 !this.hasEffects(layer) && 277 !this.hasBlur(layer) 278 ) { 279 reasons.push('does not have color fill, shadow or blur'); 280 } 281 282 const visibleRegionNode = layer.getEagerPropertyByName('visibleRegion'); 283 if ( 284 visibleRegionNode && 285 this.hasValidEmptyVisibleRegion(visibleRegionNode) 286 ) { 287 reasons.push('visible region calculated by Composition Engine is empty'); 288 } 289 290 if ( 291 visibleRegionNode?.getValue() === null && 292 !layer.getEagerPropertyByName('excludesCompositionState')?.getValue() 293 ) { 294 reasons.push('null visible region'); 295 } 296 297 const occludedByNode = layer.getEagerPropertyByName('occludedBy'); 298 if (occludedByNode && occludedByNode.getAllChildren().length > 0) { 299 reasons.push('occluded'); 300 } 301 302 if (reasons.length === 0) reasons.push('unknown'); 303 return reasons; 304 } 305 306 private getRect(rectNode: PropertyTreeNode): Rect | undefined { 307 if (rectNode.getAllChildren().length === 0) return undefined; 308 return GeometryFactory.makeRect(rectNode); 309 } 310 311 private getColor(layer: HierarchyTreeNode): PropertyTreeNode | undefined { 312 const colorNode = layer.getEagerPropertyByName('color'); 313 if (!colorNode || !colorNode.getChildByName('a')) return undefined; 314 return colorNode; 315 } 316 317 private getDisplaySize(layer: HierarchyTreeNode): Rect { 318 const displaySize = new Rect(0, 0, 0, 0); 319 const matchingDisplay = this.displays.find( 320 (display) => 321 this.getDefinedValue(display, 'layerStack') === 322 this.getDefinedValue(layer, 'layerStack'), 323 ); 324 if (matchingDisplay) { 325 const rectNode = assertDefined( 326 matchingDisplay.getChildByName('layerStackSpaceRect'), 327 ); 328 return this.getRect(rectNode) ?? displaySize; 329 } 330 return displaySize; 331 } 332 333 private layerContains( 334 layer: HierarchyTreeNode, 335 other: HierarchyTreeNode, 336 crop: Rect, 337 ): boolean { 338 if ( 339 !TransformType.isSimpleRotation( 340 assertDefined(layer.getEagerPropertyByName('transform')) 341 .getChildByName('type') 342 ?.getValue() ?? 0, 343 ) || 344 !TransformType.isSimpleRotation( 345 assertDefined(other.getEagerPropertyByName('transform')) 346 .getChildByName('type') 347 ?.getValue() ?? 0, 348 ) 349 ) { 350 return false; 351 } else { 352 const layerBounds = this.getCroppedScreenBounds(layer, crop); 353 const otherBounds = this.getCroppedScreenBounds(other, crop); 354 return layerBounds && otherBounds 355 ? layerBounds.containsRect(otherBounds) 356 : false; 357 } 358 } 359 360 private layerOverlaps( 361 layer: HierarchyTreeNode, 362 other: HierarchyTreeNode, 363 crop: Rect, 364 ): boolean { 365 const layerBounds = this.getCroppedScreenBounds(layer, crop); 366 const otherBounds = this.getCroppedScreenBounds(other, crop); 367 return layerBounds && otherBounds 368 ? layerBounds.intersectsRect(otherBounds) 369 : false; 370 } 371 372 private getCroppedScreenBounds( 373 layer: HierarchyTreeNode, 374 crop: Rect, 375 ): Rect | undefined { 376 const layerScreenBoundsNode = assertDefined( 377 layer.getEagerPropertyByName('screenBounds'), 378 ); 379 const layerScreenBounds = this.getRect(layerScreenBoundsNode); 380 381 if (layerScreenBounds && !crop.isEmpty()) { 382 return layerScreenBounds.cropRect(crop); 383 } 384 385 return layerScreenBounds; 386 } 387 388 private isHiddenByParent(layer: HierarchyTreeNode): boolean { 389 const parentLayer = assertDefined(layer.getParent()); 390 return ( 391 !parentLayer.isRoot() && 392 (this.isHiddenByPolicy(parentLayer) || this.isHiddenByParent(parentLayer)) 393 ); 394 } 395 396 private isHiddenByPolicy(layer: HierarchyTreeNode): boolean { 397 return ( 398 (this.getDefinedValue(layer, 'flags') & LayerFlag.HIDDEN) !== 0x0 || 399 this.getDefinedValue(layer, 'id') === 400 VisibilityPropertiesComputation.OFFSCREEN_LAYER_ROOT_ID 401 ); 402 } 403 404 private hasZeroAlpha(layer: HierarchyTreeNode): boolean { 405 const alpha = this.getColor(layer)?.getChildByName('a')?.getValue() ?? 0; 406 return alpha === 0; 407 } 408 409 private isOpaque(layer: HierarchyTreeNode): boolean { 410 const alpha = this.getColor(layer)?.getChildByName('a')?.getValue(); 411 if (alpha !== 1) { 412 return false; 413 } 414 return this.getDefinedValue(layer, 'isOpaque'); 415 } 416 417 private isActiveBufferEmpty(buffer: PropertyTreeNode | undefined): boolean { 418 if (buffer === undefined) return true; 419 return ( 420 buffer.getAllChildren().length === 0 || 421 (this.getDefinedValue(buffer, 'width') === 0 && 422 this.getDefinedValue(buffer, 'height') === 0 && 423 this.getDefinedValue(buffer, 'stride') === 0 && 424 this.getDefinedValue(buffer, 'format') === 0) 425 ); 426 } 427 428 private hasEffects(layer: HierarchyTreeNode): boolean { 429 const color = this.getColor(layer); 430 return ( 431 (color && !RawDataUtils.isEmptyObj(color)) || 432 (layer.getEagerPropertyByName('shadowRadius')?.getValue() ?? 0) > 0 433 ); 434 } 435 436 private hasBlur(layer: HierarchyTreeNode): boolean { 437 return ( 438 (layer.getEagerPropertyByName('backgroundBlurRadius')?.getValue() ?? 0) > 439 0 440 ); 441 } 442 443 private getDefinedValue( 444 node: HierarchyTreeNode | PropertyTreeNode, 445 name: string, 446 ): any { 447 if (node instanceof HierarchyTreeNode) { 448 return assertDefined(node.getEagerPropertyByName(name)).getValue(); 449 } else { 450 return assertDefined(node.getChildByName(name)).getValue(); 451 } 452 } 453} 454