/* * Copyright 2017, 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. */ 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; var RELATIVE_Z_CHIP = {short: 'RelZ', long: "Is relative Z-ordered to another surface", class: 'warn'}; var RELATIVE_Z_PARENT_CHIP = {short: 'RelZParent', long: "Something is relative Z-ordered to this surface", class: 'warn'}; var MISSING_LAYER = {short: 'MissingLayer', long: "This layer was referenced from the parent, but not present in the trace", class: 'error'}; 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 offset_to(bounds, x, y) { return { right: bounds.right - (bounds.left - x), bottom: bounds.bottom - (bounds.top - y), left: x, top: y, }; } 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; result = offset_to(result, 0, 0); result.label = layer.name; result.transform = layer.transform || {dsdx:1, dtdx:0, dsdy:0, dtdy:1}; 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. */ } /** * 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 || layer.type === 'ColorLayer') && !hidden && is_opaque(layer); visible &= !is_empty(layer.visibleRegion); return visible; } 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) { 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); } if (layer.zOrderRelativeParentOf !== undefined) { chips.push(RELATIVE_Z_PARENT_CHIP); } 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); return transform({ obj: layer, kind: '', name: layer.id + ": " + layer.name, children: [ [layer.resolvedChildren, transform_layer_with_parent_hidden], ], rect, bounds, highlight: rect, chips, visible, }); } function missingLayer(childId) { return { name: "layer #" + childId, missing: true, zOrderRelativeOf: -1, transform: {dsdx:1, dtdx:0, dsdy:0, dtdy:1}, } } function transform_layers(layers) { var idToItem = {}; var isChild = {} var layersList = layers.layers || []; layersList.forEach((e) => { idToItem[e.id] = e; }); layersList.forEach((e) => { e.resolvedChildren = []; if (Array.isArray(e.children)) { e.resolvedChildren = e.children.map( (childId) => idToItem[childId] || missingLayer(childId)); e.children.forEach((childId) => { isChild[childId] = true; }); } if ((e.zOrderRelativeOf || -1) !== -1) { idToItem[e.zOrderRelativeOf].zOrderRelativeParentOf = e.id; } }); var roots = layersList.filter((e) => !isChild[e.id]); function foreachTree(nodes, fun) { nodes.forEach((n) => { fun(n); foreachTree(n.children, fun); }); } var idToTransformed = {}; var transformed_roots = roots.map((r) => transform_layer(r, {parentBounds: {left: 0, right: 0, top: 0, bottom: 0}, parentHidden: null})); foreachTree(transformed_roots, (n) => { idToTransformed[n.obj.id] = n; }); var flattened = []; layersList.forEach((e) => { flattened.push(idToTransformed[e.id]); }); return transform({ obj: {}, kind: 'layers', name: 'layers', children: [ [transformed_roots, (c) => c], ], rects_transform (r) { var res = []; flattened.forEach((l) => { if (l.rect) { res.push(l.rect); } }); return res.reverse(); }, flattened, }); } function transform_layers_entry(entry) { return transform({ obj: entry, kind: 'entry', name: nanos_to_string(entry.elapsedRealtimeNanos) + " - " + entry.where, children: [ [[entry.layers], transform_layers], ], timestamp: entry.elapsedRealtimeNanos, stableId: 'entry', }); } function transform_layers_trace(entries) { var r = transform({ obj: entries, kind: 'layerstrace', name: 'layerstrace', children: [ [entries.entry, transform_layers_entry], ], }); return r; } export {transform_layers, transform_layers_trace};