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