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