xref: /aosp_15_r20/development/tools/winscope/src/parsers/surface_flinger/computations/rects_computation.ts (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
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(`${assertDefined(layer.getEagerPropertyByName('id')).getValue()}`)
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