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