• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {Region} from 'common/geometry/region';
20import {Size} from 'common/geometry/size';
21import {TransformMatrix} from 'common/geometry/transform_matrix';
22import {
23  Transform,
24  TransformType,
25} from 'parsers/surface_flinger/transform_utils';
26import {GeometryFactory} from 'trace/geometry_factory';
27import {TraceRect} from 'trace/trace_rect';
28import {TraceRectBuilder} from 'trace/trace_rect_builder';
29import {Computation} from 'trace/tree_node/computation';
30import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node';
31import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
32import {LayerExtractor} from './layer_extractor';
33
34function getDisplaySize(display: PropertyTreeNode): Size {
35  const displaySize = assertDefined(display.getChildByName('size'));
36  const w = assertDefined(displaySize.getChildByName('w')?.getValue());
37  const h = assertDefined(displaySize.getChildByName('h')?.getValue());
38  const transformType =
39    display.getChildByName('transform')?.getChildByName('type')?.getValue() ??
40    0;
41  const typeFlags = TransformType.getTypeFlags(transformType);
42  const isRotated =
43    typeFlags.includes('ROT_90') || typeFlags.includes('ROT_270');
44  return {
45    width: isRotated ? h : w,
46    height: isRotated ? w : h,
47  };
48}
49
50// InputConfig constants defined in the platform:
51//   frameworks/native/libs/input/android/os/InputConfig.aidl
52export enum InputConfig {
53  NOT_TOUCHABLE = 1 << 3,
54  IS_WALLPAPER = 1 << 6,
55  SPY = 1 << 14,
56}
57
58class RectSfFactory {
59  static makeDisplayRects(displays: readonly PropertyTreeNode[]): TraceRect[] {
60    const nameCounts = new Map<string, number>();
61    return displays.map((display, index) => {
62      const layerStackSpaceRect = assertDefined(
63        display.getChildByName('layerStackSpaceRect'),
64      );
65
66      let displayRect = GeometryFactory.makeRect(layerStackSpaceRect);
67      const isEmptyLayerStackRect = displayRect.isEmpty();
68
69      if (isEmptyLayerStackRect) {
70        const size = getDisplaySize(display);
71        displayRect = new Rect(0, 0, size.width, size.height);
72      }
73
74      const layerStack = assertDefined(
75        display.getChildByName('layerStack'),
76      ).getValue();
77      let displayName = display.getChildByName('name')?.getValue();
78      if (!displayName) {
79        displayName = 'Unknown Display';
80      }
81      const id = assertDefined(display.getChildByName('id')).getValue();
82
83      const existingNameCount = nameCounts.get(displayName);
84      if (existingNameCount !== undefined) {
85        nameCounts.set(displayName, existingNameCount + 1);
86        displayName += ` (${existingNameCount + 1})`;
87      } else {
88        nameCounts.set(displayName, 1);
89      }
90
91      const isOn = display.getChildByName('isOn')?.getValue() ?? false;
92      const isVirtual =
93        display.getChildByName('isVirtual')?.getValue() ?? false;
94
95      return new TraceRectBuilder()
96        .setX(displayRect.x)
97        .setY(displayRect.y)
98        .setWidth(displayRect.w)
99        .setHeight(displayRect.h)
100        .setId(`Display - ${id}`)
101        .setName(displayName)
102        .setCornerRadius(0)
103        .setTransform(Transform.EMPTY.matrix)
104        .setGroupId(layerStack)
105        .setIsVisible(false)
106        .setIsDisplay(true)
107        .setIsActiveDisplay(isOn && !isVirtual)
108        .setDepth(index)
109        .setIsSpy(false)
110        .build();
111    });
112  }
113
114  static makeLayerRect(
115    layer: HierarchyTreeNode,
116    layerStack: number,
117    absoluteZ: number,
118  ): TraceRect {
119    const isVisible = assertDefined(
120      layer.getEagerPropertyByName('isComputedVisible'),
121    ).getValue();
122
123    const name = assertDefined(layer.getEagerPropertyByName('name')).getValue();
124    const bounds = assertDefined(layer.getEagerPropertyByName('bounds'));
125    const boundsRect = GeometryFactory.makeRect(bounds);
126
127    let opacity = layer
128      .getEagerPropertyByName('color')
129      ?.getChildByName('a')
130      ?.getValue();
131    if (isVisible && opacity === undefined) opacity = 0;
132
133    return new TraceRectBuilder()
134      .setX(boundsRect.x)
135      .setY(boundsRect.y)
136      .setWidth(boundsRect.w)
137      .setHeight(boundsRect.h)
138      .setId(
139        `${assertDefined(
140          layer.getEagerPropertyByName('id'),
141        ).getValue()} ${name}`,
142      )
143      .setName(name)
144      .setCornerRadius(
145        assertDefined(layer.getEagerPropertyByName('cornerRadius')).getValue(),
146      )
147      .setTransform(
148        Transform.from(assertDefined(layer.getEagerPropertyByName('transform')))
149          .matrix,
150      )
151      .setGroupId(layerStack)
152      .setIsVisible(isVisible)
153      .setIsDisplay(false)
154      .setDepth(absoluteZ)
155      .setOpacity(opacity)
156      .setIsSpy(false)
157      .build();
158  }
159
160  static makeInputWindowRect(
161    layer: HierarchyTreeNode,
162    layerStack: number,
163    absoluteZ: number,
164    invalidBoundsFromDisplays: Rect[],
165    display?: TraceRect,
166    displayTransform?: TransformMatrix,
167  ): TraceRect {
168    const name = assertDefined(layer.getEagerPropertyByName('name')).getValue();
169    const inputWindowInfo = assertDefined(
170      layer.getEagerPropertyByName('inputWindowInfo'),
171    );
172
173    const layerTransform = Transform.from(
174      assertDefined(layer.getEagerPropertyByName('transform')),
175    ).matrix;
176    const inverseLayerTransform = layerTransform.inverse();
177
178    // The input frame is given in the display space.
179    let inputWindowRect = GeometryFactory.makeRect(
180      assertDefined(inputWindowInfo.getChildByName('frame')),
181    );
182    // Transform it to layer space.
183    inputWindowRect = inverseLayerTransform.transformRect(
184      displayTransform?.transformRect(inputWindowRect) ?? inputWindowRect,
185    );
186
187    const inputConfig = assertDefined(
188      inputWindowInfo.getChildByName('inputConfig'),
189    ).getValue();
190
191    const shouldCropToDisplay =
192      inputWindowRect.isEmpty() ||
193      (inputConfig & InputConfig.IS_WALLPAPER) !== 0 ||
194      (invalidBoundsFromDisplays !== undefined &&
195        invalidBoundsFromDisplays.some((invalid) =>
196          inputWindowRect.isAlmostEqual(invalid, 0.01),
197        ));
198    if (shouldCropToDisplay && display !== undefined) {
199      inputWindowRect = inputWindowRect.cropRect(display);
200    }
201
202    const isVisible =
203      inputWindowInfo.getChildByName('visible')?.getValue() ??
204      assertDefined(
205        layer.getEagerPropertyByName('isComputedVisible'),
206      ).getValue();
207
208    let touchableRegion: Region | undefined;
209    const isTouchable = (inputConfig & InputConfig.NOT_TOUCHABLE) === 0;
210    const touchableRegionNode =
211      inputWindowInfo.getChildByName('touchableRegion');
212
213    if (!isTouchable) {
214      touchableRegion = Region.createEmpty();
215    } else if (touchableRegionNode !== undefined) {
216      // The touchable region is given in the display space.
217      touchableRegion = GeometryFactory.makeRegion(touchableRegionNode);
218      // Transform it to layer space.
219      touchableRegion = inverseLayerTransform.transformRegion(
220        displayTransform?.transformRegion(touchableRegion) ?? touchableRegion,
221      );
222      if (shouldCropToDisplay && display !== undefined) {
223        touchableRegion = new Region(
224          touchableRegion.rects.map((rect) => {
225            return rect.cropRect(display);
226          }),
227        );
228      }
229    }
230
231    return new TraceRectBuilder()
232      .setX(inputWindowRect.x)
233      .setY(inputWindowRect.y)
234      .setWidth(inputWindowRect.w)
235      .setHeight(inputWindowRect.h)
236      .setId(layer.id)
237      .setName(name)
238      .setCornerRadius(0)
239      .setTransform(layerTransform)
240      .setGroupId(layerStack)
241      .setIsVisible(isVisible)
242      .setIsDisplay(false)
243      .setDepth(absoluteZ)
244      .setIsSpy((inputConfig & InputConfig.SPY) !== 0)
245      .setFillRegion(touchableRegion)
246      .build();
247  }
248}
249
250export class RectsComputation implements Computation {
251  private static readonly DEFAULT_INVALID_BOUNDS = new Rect(
252    -50000,
253    -50000,
254    100000,
255    100000,
256  );
257
258  private root?: HierarchyTreeNode;
259  private displaysByLayerStack?: Map<number, TraceRect>;
260  private displayTransformsByLayerStack?: Map<number, TransformMatrix>;
261  private invalidBoundsFromDisplays?: Rect[];
262
263  setRoot(value: HierarchyTreeNode): this {
264    this.root = value;
265    return this;
266  }
267
268  // synced with getMaxDisplayBounds() in main/frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp
269  private static getInvalidBoundsFromDisplays(
270    displays: readonly PropertyTreeNode[],
271  ): Rect[] {
272    if (displays.length === 0) return [];
273
274    // foldables expand rects to fill display space before all displays are available
275    // make invalid bounds for each individual display, and for the rect of max dimensions
276
277    const invalidBounds: Rect[] = [];
278
279    const maxSize = displays.reduce(
280      (size, display) => {
281        const displaySize = getDisplaySize(display);
282        invalidBounds.push(
283          ...RectsComputation.makeInvalidBoundsFromSize(displaySize),
284        );
285        return {
286          width: Math.max(size.width, displaySize.width),
287          height: Math.max(size.height, displaySize.height),
288        };
289      },
290      {width: 0, height: 0},
291    );
292    invalidBounds.push(...RectsComputation.makeInvalidBoundsFromSize(maxSize));
293
294    return invalidBounds;
295  }
296
297  private static makeInvalidBoundsFromSize(size: Size): Rect[] {
298    const [invalidX, invalidY] = [size.width * 10, size.height * 10];
299    const invalidBounds = new Rect(
300      -invalidX,
301      -invalidY,
302      invalidX * 2,
303      invalidY * 2,
304    );
305    const rotatedInvalidBounds = new Rect(
306      invalidBounds.y,
307      invalidBounds.x,
308      invalidBounds.h,
309      invalidBounds.w,
310    );
311    return [invalidBounds, rotatedInvalidBounds];
312  }
313
314  executeInPlace(): void {
315    this.processDisplays();
316    this.processLayers(
317      RectsComputation.hasLayerRect,
318      RectSfFactory.makeLayerRect,
319      true,
320    );
321    this.processLayers(
322      RectsComputation.hasInputWindowRect,
323      RectSfFactory.makeInputWindowRect,
324      false,
325    );
326  }
327
328  private processDisplays() {
329    if (!this.root) {
330      throw new Error('root not set in SF rects computation');
331    }
332    const displays =
333      this.root.getEagerPropertyByName('displays')?.getAllChildren() ?? [];
334    const displayRects = RectSfFactory.makeDisplayRects(displays);
335    this.root.setRects(displayRects);
336
337    this.displaysByLayerStack = new Map(
338      displayRects.map((rect) => [rect.groupId, rect]),
339    );
340
341    this.invalidBoundsFromDisplays =
342      RectsComputation.getInvalidBoundsFromDisplays(displays);
343
344    this.displayTransformsByLayerStack = new Map();
345    displays.forEach((display) => {
346      const layerStack = assertDefined(
347        display.getChildByName('layerStack'),
348      ).getValue();
349      const matrix = RectsComputation.extractDisplayTransform(display);
350      if (matrix) {
351        assertDefined(this.displayTransformsByLayerStack).set(
352          layerStack,
353          matrix,
354        );
355      }
356    });
357  }
358
359  private static extractDisplayTransform(
360    display: PropertyTreeNode,
361  ): TransformMatrix | undefined {
362    const transformNode = display.getChildByName('transform');
363    const layerStackSpaceRectNode = assertDefined(
364      display.getChildByName('layerStackSpaceRect'),
365    );
366    if (!transformNode || !layerStackSpaceRectNode) {
367      return undefined;
368    }
369    const transform = Transform.from(transformNode);
370    let tx = transform.matrix.tx;
371    let ty = transform.matrix.ty;
372    const layerStackSpaceRect = GeometryFactory.makeRect(
373      layerStackSpaceRectNode,
374    );
375
376    const typeFlags = TransformType.getTypeFlags(transform.type);
377    if (typeFlags.includes('ROT_180')) {
378      tx += layerStackSpaceRect.w;
379      ty += layerStackSpaceRect.h;
380    } else if (typeFlags.includes('ROT_270')) {
381      tx += layerStackSpaceRect.w;
382    } else if (typeFlags.includes('ROT_90')) {
383      ty += layerStackSpaceRect.h;
384    }
385    return TransformMatrix.from({tx, ty}, transform.matrix);
386  }
387
388  private processLayers(
389    shouldIncludeLayer: (
390      node: HierarchyTreeNode,
391      invalidBoundsFromDisplays: Rect[],
392    ) => boolean,
393    makeTraceRect: (
394      layer: HierarchyTreeNode,
395      layerStack: number,
396      absoluteZ: number,
397      invalidBoundsFromDisplays: Rect[],
398      display?: TraceRect,
399      displayTransform?: TransformMatrix,
400    ) => TraceRect,
401    isPrimaryRects: boolean,
402  ) {
403    const curAbsoluteZByLayerStack = new Map<number, number>();
404    for (const layerStack of assertDefined(this.displaysByLayerStack).keys()) {
405      curAbsoluteZByLayerStack.set(layerStack, 1);
406    }
407
408    const layersWithRects = LayerExtractor.extractLayersTopToBottom(
409      assertDefined(this.root),
410    ).filter((node) =>
411      shouldIncludeLayer(node, assertDefined(this.invalidBoundsFromDisplays)),
412    );
413
414    for (let i = layersWithRects.length - 1; i > -1; i--) {
415      const layer = layersWithRects[i];
416      const layerStack = assertDefined(
417        layer.getEagerPropertyByName('layerStack'),
418      ).getValue();
419      const absoluteZ = curAbsoluteZByLayerStack.get(layerStack) ?? 0;
420      const rect = makeTraceRect(
421        layer,
422        layerStack,
423        absoluteZ,
424        assertDefined(this.invalidBoundsFromDisplays),
425        this.displaysByLayerStack?.get(layerStack),
426        this.displayTransformsByLayerStack?.get(layerStack),
427      );
428      isPrimaryRects ? layer.setRects([rect]) : layer.setSecondaryRects([rect]);
429      curAbsoluteZByLayerStack.set(layerStack, absoluteZ + 1);
430    }
431  }
432
433  private static hasLayerRect(
434    node: HierarchyTreeNode,
435    invalidBoundsFromDisplays: Rect[],
436  ): boolean {
437    const isVisible = node
438      .getEagerPropertyByName('isComputedVisible')
439      ?.getValue();
440    if (isVisible === undefined) {
441      throw new Error(
442        'SF rects computation attempted before visibility computation',
443      );
444    }
445
446    const screenBounds = node.getEagerPropertyByName('screenBounds');
447    if (!screenBounds) return false;
448
449    if (screenBounds && !isVisible) {
450      const screenBoundsRect = GeometryFactory.makeRect(screenBounds);
451      const isInvalidFromDisplays =
452        invalidBoundsFromDisplays.length > 0 &&
453        invalidBoundsFromDisplays.some((invalid) => {
454          return screenBoundsRect.isAlmostEqual(invalid, 0.01);
455        });
456      return (
457        !isInvalidFromDisplays &&
458        !screenBoundsRect.isAlmostEqual(
459          RectsComputation.DEFAULT_INVALID_BOUNDS,
460          0.01,
461        )
462      );
463    }
464
465    return true;
466  }
467
468  private static hasInputWindowRect(node: HierarchyTreeNode): boolean {
469    const inputWindowInfo = node.getEagerPropertyByName('inputWindowInfo');
470    return (
471      inputWindowInfo !== undefined &&
472      inputWindowInfo.getChildByName('inputConfig') !== undefined
473    );
474  }
475}
476