diff --git a/tools/winscope/src/matrix_utils.js b/tools/winscope/src/matrix_utils.js index 2c7bd3ac5..e9cef2afe 100644 --- a/tools/winscope/src/matrix_utils.js +++ b/tools/winscope/src/matrix_utils.js @@ -200,4 +200,11 @@ function multiply_rect(matrix, rect) { return outrect; } -export {format_transform_type, fill_transform_data, is_simple_transform, multiply_rect}; \ No newline at end of file +// Returns true if the applying the transform on an an axis aligned rectangle +// results in another axis aligned rectangle. +function is_simple_rotation(transform) { + return !is_type_flag_set(transform, ROT_INVALID_VAL); +} + +export {format_transform_type, fill_transform_data, is_simple_transform, + multiply_rect, is_simple_rotation}; \ No newline at end of file diff --git a/tools/winscope/src/sf_visibility.js b/tools/winscope/src/sf_visibility.js new file mode 100644 index 000000000..f98ea7f37 --- /dev/null +++ b/tools/winscope/src/sf_visibility.js @@ -0,0 +1,299 @@ +/* + * Copyright 2020, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Utility class for deriving state and visibility from the hierarchy. This + * duplicates some of the logic in surface flinger. If the trace contains + * composition state (visibleRegion), it will be used otherwise it will be + * derived. + */ +import { multiply_rect, is_simple_rotation } from './matrix_utils.js' + +// Layer flags +const FLAG_HIDDEN = 0x01; +const FLAG_OPAQUE = 0x02; +const FLAG_SECURE = 0x80; + +function flags_to_string(flags) { + if (!flags) return ''; + var verboseFlags = []; + if (flags & FLAG_HIDDEN) verboseFlags.push("HIDDEN"); + if (flags & FLAG_OPAQUE) verboseFlags.push("OPAQUE"); + if (flags & FLAG_SECURE) verboseFlags.push("SECURE"); + return verboseFlags.join('|') + " (" + flags + ")"; +} + +function is_empty(region) { + return region == undefined || + region.rect == undefined || + region.rect.length == 0 || + region.rect.every(function(r) { return is_empty_rect(r) } ); +} + +function is_empty_rect(rect) { + var right = rect.right || 0; + var left = rect.left || 0; + var top = rect.top || 0; + var bottom = rect.bottom || 0; + + return (right - left) <= 0 || (bottom - top) <= 0; +} + +function is_rect_empty_and_valid(rect) { + return rect && + (rect.left - rect.right === 0 || rect.top - rect.bottom === 0); +} + +/** + * The transformation matrix is defined as the product of: + * | cos(a) -sin(a) | \/ | X 0 | + * | sin(a) cos(a) | /\ | 0 Y | + * + * where a is a rotation angle, and X and Y are scaling factors. + * A transformation matrix is invalid when either X or Y is zero, + * as a rotation matrix is valid for any angle. When either X or Y + * is 0, then the scaling matrix is not invertible, which makes the + * transformation matrix not invertible as well. A 2D matrix with + * components | A B | is not invertible if and only if AD - BC = 0. + * | C D | + * This check is included above. + */ +function is_transform_invalid(transform) { + return !transform || (transform.dsdx * transform.dtdy === + transform.dtdx * transform.dsdy); //determinant of transform +} + +function is_opaque(layer) { + return layer.color == undefined || (layer.color.a || 0) < 1 || layer.isOpaque; +} + +function fills_color(layer) { + return layer.color && layer.color.a > 0 && + layer.color.r >= 0 && layer.color.g >= 0 && + layer.color.b >= 0; +} + +function draws_shadows(layer) { + return layer.shadowRadius && layer.shadowRadius > 0; +} + +function has_blur(layer) { + return layer.backgroundBlurRadius && layer.backgroundBlurRadius > 0; +} + +function has_effects(layer) { + // Support previous color layer + if (layer.type === 'ColorLayer') return true; + + // Support newer effect layer + return layer.type === 'EffectLayer' && + (fills_color(layer) || draws_shadows(layer) || has_blur(layer)) +} + +function is_hidden_by_policy(layer) { + return layer.flags & FLAG_HIDDEN == FLAG_HIDDEN || + // offscreen layer root has a unique layer id + layer.id == 0x7FFFFFFD; +} + +/** + * Checks if the layer is visible based on its visibleRegion if available + * or its type, active buffer content, alpha and properties. + */ +function is_visible(layer, hiddenByPolicy, includesCompositionState) { + + if (includesCompositionState) { + return !is_empty(layer.visibleRegion); + } + + if (hiddenByPolicy) { + return false; + } + + if (!layer.activeBuffer && !has_effects(layer)) { + return false; + } + + if (!layer.color || !layer.color.a || layer.color.a == 0) { + return false; + } + + if (layer.occludedBy && layer.occludedBy.length > 0) { + return false; + } + + if (!layer.bounds || is_empty_rect(layer.bounds)) { + return false; + } + + return true; +} + +function get_visibility_reason(layer) { + if (layer.type === 'ContainerLayer') { + return 'ContainerLayer'; + } + + if (is_hidden_by_policy(layer)) { + return 'Flag is hidden'; + } + + if (layer.hidden) { + return 'Hidden by parent'; + } + + let isBufferLayer = (layer.type === 'BufferStateLayer' || layer.type === 'BufferQueueLayer'); + if (isBufferLayer && (!layer.activeBuffer || + layer.activeBuffer.height === 0 || layer.activeBuffer.width === 0)) { + return 'Buffer is empty'; + } + + if (!layer.color || !layer.color.a || layer.color.a == 0) { + return 'Alpha is 0'; + } + + if (is_rect_empty_and_valid(layer.crop)) { + return 'Crop is 0x0'; + } + + if (!layer.bounds || is_empty_rect(layer.bounds)) { + return 'Bounds is 0x0'; + } + + if (is_transform_invalid(layer.transform)) { + return 'Transform is invalid'; + } + if (layer.isRelativeOf && layer.zOrderRelativeOf == -1) { + return 'RelativeOf layer has been removed'; + } + + let isEffectLayer = (layer.type === 'EffectLayer'); + if (isEffectLayer && !fills_color(layer) && !draws_shadows(layer) && !has_blur(layer)) { + return 'Effect layer does not have color fill, shadow or blur'; + } + + if (layer.occludedBy && layer.occludedBy.length > 0) { + return 'Layer is occluded by:' + layer.occludedBy.join(); + } + + if (layer.visible) { + return "Unknown"; + }; +} + +// Returns true if rectA overlaps rectB +function overlaps(rectA, rectB) { + return rectA.left < rectB.right && rectA.right > rectB.left && + rectA.top < rectB.bottom && rectA.bottom > rectA.top; +} + +// Returns true if outer rect contains inner rect +function contains(outerLayer, innerLayer) { + if (!is_simple_rotation(outerLayer.transform) || !is_simple_rotation(innerLayer.transform)) { + return false; + } + const outer = screen_bounds(outerLayer); + const inner = screen_bounds(innerLayer); + return inner.left >= outer.left && inner.top >= outer.top && + inner.right <= outer.right && inner.bottom <= outer.bottom; +} + +function screen_bounds(layer) { + if (layer.screenBounds) return layer.screenBounds; + let transformMatrix = layer.transform; + var tx = layer.position ? layer.position.x || 0 : 0; + var ty = layer.position ? layer.position.y || 0 : 0; + + transformMatrix.tx = tx + transformMatrix.ty = ty + return multiply_rect(transformMatrix, layer.bounds); +} + +// Traverse in z-order from top to bottom and fill in occlusion data +function fill_occlusion_state(layerMap, rootLayers, includesCompositionState) { + const layers = rootLayers.filter(layer => !layer.isRelativeOf); + traverse_top_to_bottom(layerMap, layers, {opaqueRects:[], transparentRects:[], screenBounds:null}, (layer, globalState) => { + + if (layer.name.startsWith("Root#0") && layer.sourceBounds) { + globalState.screenBounds = {left:0, top:0, bottom:layer.sourceBounds.bottom, right:layer.sourceBounds.right}; + } + + const visible = is_visible(layer, layer.hidden, includesCompositionState); + if (visible) { + let fullyOccludes = (testLayer) => contains(testLayer, layer); + let partiallyOccludes = (testLayer) => overlaps(screen_bounds(testLayer), screen_bounds(layer)); + let covers = (testLayer) => overlaps(screen_bounds(testLayer), screen_bounds(layer)); + + layer.occludedBy = globalState.opaqueRects.filter(fullyOccludes).map(layer => layer.id); + layer.partiallyOccludedBy = globalState.opaqueRects.filter(partiallyOccludes).map(layer => layer.id); + layer.coveredBy = globalState.transparentRects.filter(covers).map(layer => layer.id); + + if (is_opaque(layer)) { + globalState.opaqueRects.push(layer); + } else { + globalState.transparentRects.push(layer); + } + } + + layer.visible = is_visible(layer, layer.hidden, includesCompositionState); + if (!layer.visible) { + layer.invisibleDueTo = get_visibility_reason(layer); + } + }); +} + +function traverse_top_to_bottom(layerMap, rootLayers, globalState, fn) { + for (var i = rootLayers.length-1; i >=0; i--) { + const relatives = rootLayers[i].relatives.map(id => layerMap[id]); + const children = rootLayers[i].children.map(id => layerMap[id]) + + // traverse through relatives and children that are not relatives + const traverseList = relatives.concat(children.filter(layer => !layer.isRelativeOf)); + traverseList.sort((lhs, rhs) => rhs.z - lhs.z); + + traverseList.filter((layer) => layer.z >=0).forEach(layer => { + traverse_top_to_bottom(layerMap, [layer], globalState, fn); + }); + + fn(rootLayers[i], globalState); + + traverseList.filter((layer) => layer.z < 0).forEach(layer => { + traverse_top_to_bottom(layerMap, [layer], globalState, fn); + }); + + } +} + +// Traverse all children and fill in any inherited states. +function fill_inherited_state(layerMap, rootLayers) { + traverse(layerMap, rootLayers, (layer, parent) => { + const parentHidden = parent && parent.hidden; + layer.hidden = is_hidden_by_policy(layer) || parentHidden; + layer.verboseFlags = flags_to_string(layer.flags); + }); +} + +function traverse(layerMap, rootLayers, fn) { + for (var i = rootLayers.length-1; i >=0; i--) { + const parentId = rootLayers[i].parent; + const parent = parentId == -1 ? null : layerMap[parentId]; + fn(rootLayers[i], parent); + const children = rootLayers[i].children.map(id => layerMap[id]); + traverse(layerMap, children, fn); + } +} + +export {fill_occlusion_state, fill_inherited_state}; \ No newline at end of file diff --git a/tools/winscope/src/transform_sf.js b/tools/winscope/src/transform_sf.js index 4dd158fd4..7b631c68f 100644 --- a/tools/winscope/src/transform_sf.js +++ b/tools/winscope/src/transform_sf.js @@ -15,11 +15,7 @@ */ import {transform, nanos_to_string, get_visible_chip} from './transform.js' - -// Layer flags -const FLAG_HIDDEN = 0x01; -const FLAG_OPAQUE = 0x02; -const FLAG_SECURE = 0x80; +import { fill_occlusion_state, fill_inherited_state } from './sf_visibility.js'; var RELATIVE_Z_CHIP = {short: 'RelZ', long: "Is relative Z-ordered to another surface", @@ -37,60 +33,7 @@ var HWC_CHIP = {short: 'HWC', long: "This layer was composed by Hardware Composer", class: 'hwc'}; -function transform_layer(layer, {parentBounds, parentHidden}) { - function get_size(layer) { - var size = layer.size || {w: 0, h: 0}; - return { - left: 0, - right: size.w, - top: 0, - bottom: size.h - }; - } - - function get_crop(layer) { - var crop = layer.crop || {left: 0, top: 0, right: 0 , bottom:0}; - return { - left: crop.left || 0, - right: crop.right || 0, - top: crop.top || 0, - bottom: crop.bottom || 0 - }; - } - - function intersect(bounds, crop) { - return { - left: Math.max(crop.left, bounds.left), - right: Math.min(crop.right, bounds.right), - top: Math.max(crop.top, bounds.top), - bottom: Math.min(crop.bottom, bounds.bottom), - }; - } - - function is_empty_rect(rect) { - var right = rect.right || 0; - var left = rect.left || 0; - var top = rect.top || 0; - var bottom = rect.bottom || 0; - - return (right - left) <= 0 || (bottom - top) <= 0; - } - - function get_cropped_bounds(layer, parentBounds) { - var size = get_size(layer); - var crop = get_crop(layer); - if (!is_empty_rect(size) && !is_empty_rect(crop)) { - return intersect(size, crop); - } - if (!is_empty_rect(size)) { - return size; - } - if (!is_empty_rect(crop)) { - return crop; - } - return parentBounds || { left: 0, right: 0, top: 0, bottom: 0 }; - } - +function transform_layer(layer) { function offset_to(bounds, x, y) { return { right: bounds.right - (bounds.left - x), @@ -100,125 +43,30 @@ function transform_layer(layer, {parentBounds, parentHidden}) { }; } - function transform_bounds(layer, parentBounds) { - var result = layer.bounds || get_cropped_bounds(layer, parentBounds); - var tx = (layer.position) ? layer.position.x || 0 : 0; - var ty = (layer.position) ? layer.position.y || 0 : 0; + function get_rect(layer) { + var result = layer.bounds; + var tx = layer.position ? layer.position.x || 0 : 0; + var ty = layer.position ? layer.position.y || 0 : 0; result = offset_to(result, 0, 0); result.label = layer.name; - result.transform = layer.transform || {dsdx:1, dtdx:0, dsdy:0, dtdy:1}; + result.transform = layer.transform; result.transform.tx = tx; result.transform.ty = ty; return result; } - function is_opaque(layer) { - return layer.color == undefined || (layer.color.a || 0) > 0; - } - - function is_empty(region) { - return region == undefined || - region.rect == undefined || - region.rect.length == 0 || - region.rect.every(function(r) { return is_empty_rect(r) } ); - } - - function is_rect_empty_and_valid(rect) { - return rect && - (rect.left - rect.right === 0 || rect.top - rect.bottom === 0); - } - - function is_transform_invalid(transform) { - return !transform || (transform.dsdx * transform.dtdy === - transform.dtdx * transform.dsdy); //determinant of transform - /** - * The transformation matrix is defined as the product of: - * | cos(a) -sin(a) | \/ | X 0 | - * | sin(a) cos(a) | /\ | 0 Y | - * - * where a is a rotation angle, and X and Y are scaling factors. - * A transformation matrix is invalid when either X or Y is zero, - * as a rotation matrix is valid for any angle. When either X or Y - * is 0, then the scaling matrix is not invertible, which makes the - * transformation matrix not invertible as well. A 2D matrix with - * components | A B | is uninvertible if and only if AD - BC = 0. - * | C D | - * This check is included above. - */ - } - - function fills_color(layer) { - return layer.color && layer.color.a > 0 && - layer.color.r >= 0 && layer.color.g >= 0 && - layer.color.b >= 0; - } - - function draws_shadows(layer) { - return layer.shadowRadius && layer.shadowRadius > 0; - } - - function has_effects(layer) { - // Support previous color layer - if (layer.type === 'ColorLayer') return true; - - // Support newer effect layer - return layer.type === 'EffectLayer' && - (fills_color(layer) || draws_shadows(layer)) - } - - /** - * Checks if the layer is visible on screen according to its type, - * active buffer content, alpha and visible regions. - * - * @param {layer} layer - * @returns if the layer is visible on screen or not - */ - function is_visible(layer) { - var visible = (layer.activeBuffer || has_effects(layer)) - && !hidden && is_opaque(layer); - visible &= !is_empty(layer.visibleRegion); - return visible; - } - function add_hwc_composition_type_chip(layer) { if (layer.hwcCompositionType === "CLIENT") { chips.push(GPU_CHIP); - } else if (layer.hwcCompositionType === "DEVICE") { + } else if (layer.hwcCompositionType === "DEVICE" || layer.hwcCompositionType === "SOLID_COLOR") { chips.push(HWC_CHIP); } } - function postprocess_flags(layer) { - if (!layer.flags) return; - var verboseFlags = []; - if (layer.flags & FLAG_HIDDEN) { - verboseFlags.push("HIDDEN"); - } - if (layer.flags & FLAG_OPAQUE) { - verboseFlags.push("OPAQUE"); - } - if (layer.flags & FLAG_SECURE) { - verboseFlags.push("SECURE"); - } - - layer.flags = verboseFlags.join('|') + " (" + layer.flags + ")"; - } - var chips = []; - var rect = transform_bounds(layer, parentBounds); - var hidden = (layer.flags & FLAG_HIDDEN) != 0 || parentHidden; - var visible = is_visible(layer); - if (visible) { + if (layer.visible) { chips.push(get_visible_chip()); - } else { - rect = undefined; } - - var bounds = undefined; - if (layer.name.startsWith("Display Root#0") && layer.sourceBounds) { - bounds = {width: layer.sourceBounds.right, height: layer.sourceBounds.bottom}; - } - if ((layer.zOrderRelativeOf || -1) !== -1) { chips.push(RELATIVE_Z_CHIP); } @@ -228,61 +76,22 @@ function transform_layer(layer, {parentBounds, parentHidden}) { if (layer.missing) { chips.push(MISSING_LAYER); } - function visibilityReason(layer) { - var reasons = []; - if (!layer.color || layer.color.a === 0) { - reasons.push('Alpha is 0'); - } - if (layer.flags && (layer.flags & FLAG_HIDDEN != 0)) { - reasons.push('Flag is hidden'); - } - if (is_rect_empty_and_valid(layer.crop)) { - reasons.push('Crop is zero'); - } - if (is_transform_invalid(layer.transform)) { - reasons.push('Transform is invalid'); - } - if (layer.isRelativeOf && layer.zOrderRelativeOf == -1) { - reasons.push('RelativeOf layer has been removed'); - } - return reasons.join(); - } - if (parentHidden) { - layer.invisibleDueTo = 'Hidden by parent with ID: ' + parentHidden; - } else { - let reasons_hidden = visibilityReason(layer); - let isBufferLayer = (layer.type === 'BufferStateLayer' || layer.type === 'BufferQueueLayer'); - if (reasons_hidden) { - layer.invisibleDueTo = reasons_hidden; - parentHidden = layer.id - } else if (layer.type === 'ContainerLayer') { - layer.invisibleDueTo = 'This is a ContainerLayer.'; - } else if (isBufferLayer && (!layer.activeBuffer || - layer.activeBuffer.height === 0 || layer.activeBuffer.width === 0)) { - layer.invisibleDueTo = 'The buffer is empty.'; - } else if (!visible) { - layer.invisibleDueTo = 'Unknown. Occluded by another layer?'; - } - } - var transform_layer_with_parent_hidden = - (layer) => transform_layer(layer, {parentBounds: rect, parentHidden: parentHidden}); - postprocess_flags(layer); add_hwc_composition_type_chip(layer); + const rect = layer.visible ? get_rect(layer) : undefined; + return transform({ obj: layer, kind: '', name: layer.id + ": " + layer.name, - children: [ - [layer.resolvedChildren, transform_layer_with_parent_hidden], - ], + children: [[layer.resolvedChildren, transform_layer]], rect, - bounds, + undefined /* bounds */, highlight: rect, chips, - visible, + visible: layer.visible, }); } - + function missingLayer(childId) { return { name: "layer #" + childId, @@ -292,7 +101,7 @@ function missingLayer(childId) { } } -function transform_layers(layers) { +function transform_layers(includesCompositionState, layers) { var idToItem = {}; var isChild = {} @@ -316,7 +125,8 @@ function transform_layers(layers) { }); var roots = layersList.filter((e) => !isChild[e.id]); - + fill_inherited_state(idToItem, roots); + fill_occlusion_state(idToItem, roots, includesCompositionState); function foreachTree(nodes, fun) { nodes.forEach((n) => { fun(n); @@ -358,12 +168,13 @@ function transform_layers(layers) { } function transform_layers_entry(entry) { + const includesCompositionState = !entry.excludesCompositionState; return transform({ obj: entry, kind: 'entry', name: nanos_to_string(entry.elapsedRealtimeNanos) + " - " + entry.where, children: [ - [[entry.layers], transform_layers], + [[entry.layers], (layer) => transform_layers(includesCompositionState, layer)], ], timestamp: entry.elapsedRealtimeNanos, stableId: 'entry',