blob: 81143c85b40ddb73453d01c05fac120554693f34 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "flutter/display_list/benchmarking/dl_complexity_gl.h"
// The numbers and weightings used in this file stem from taking the
// data from the DisplayListBenchmarks suite run on an Pixel 4 and
// applying very rough analysis on them to identify the approximate
// trends.
//
// See the comments in display_list_complexity_helper.h for details on the
// process and rationale behind coming up with these numbers.
namespace flutter {
DisplayListGLComplexityCalculator*
DisplayListGLComplexityCalculator::instance_ = nullptr;
DisplayListGLComplexityCalculator*
DisplayListGLComplexityCalculator::GetInstance() {
if (instance_ == nullptr) {
instance_ = new DisplayListGLComplexityCalculator();
}
return instance_;
}
unsigned int DisplayListGLComplexityCalculator::GLHelper::BatchedComplexity() {
// Calculate the impact of saveLayer.
unsigned int save_layer_complexity;
if (save_layer_count_ == 0) {
save_layer_complexity = 0;
} else {
// m = 1/5
// c = 10
save_layer_complexity = (save_layer_count_ + 50) * 40000;
}
unsigned int draw_text_blob_complexity;
if (draw_text_blob_count_ == 0) {
draw_text_blob_complexity = 0;
} else {
// m = 1/240
// c = 0.25
draw_text_blob_complexity = (draw_text_blob_count_ + 60) * 2500 / 3;
}
return save_layer_complexity + draw_text_blob_complexity;
}
void DisplayListGLComplexityCalculator::GLHelper::saveLayer(
const SkRect& bounds,
const SaveLayerOptions options,
const DlImageFilter* backdrop) {
if (IsComplex()) {
return;
}
if (backdrop) {
// Flutter does not offer this operation so this value can only ever be
// non-null for a frame-wide builder which is not currently evaluated for
// complexity.
AccumulateComplexity(Ceiling());
}
save_layer_count_++;
}
void DisplayListGLComplexityCalculator::GLHelper::drawLine(const SkPoint& p0,
const SkPoint& p1) {
if (IsComplex()) {
return;
}
// There is a relatively high fixed overhead cost for drawLine on OpenGL.
// Further, there is a strange bump where the cost of drawing a line of
// length ~500px is actually more costly than drawing a line of length
// ~1000px. The calculations here will be for a linear graph that
// approximate the overall trend.
float non_hairline_penalty = 1.0f;
unsigned int aa_penalty = 1;
// The non-hairline penalty is insignificant when AA is on.
if (!IsHairline() && !IsAntiAliased()) {
non_hairline_penalty = 1.15f;
}
if (IsAntiAliased()) {
aa_penalty = 2;
}
// Use an approximation for the distance to avoid floating point or
// sqrt() calls.
SkScalar distance = abs(p0.x() - p1.x()) + abs(p0.y() - p1.y());
// The baseline complexity is for a hairline stroke with no AA.
// m = 1/40
// c = 13
unsigned int complexity =
((distance + 520) / 2) * non_hairline_penalty * aa_penalty;
AccumulateComplexity(complexity);
}
void DisplayListGLComplexityCalculator::GLHelper::drawDashedLine(
const DlPoint& p0,
const DlPoint& p1,
DlScalar on_length,
DlScalar off_length) {
// Dashing is slightly more complex than a regular drawLine, but this
// op is so rare it is not worth measuring the difference.
drawLine(ToSkPoint(p0), ToSkPoint(p1));
}
void DisplayListGLComplexityCalculator::GLHelper::drawRect(const SkRect& rect) {
if (IsComplex()) {
return;
}
unsigned int complexity;
// If stroked, cost scales linearly with the rectangle width/height.
// If filled, it scales with the area.
//
// Hairline stroke vs non hairline has no significant penalty.
//
// There is also a kStrokeAndFill_Style that Skia exposes, but we do not
// currently use it anywhere in Flutter.
if (DrawStyle() == DlDrawStyle::kFill) {
// No real difference for AA with filled styles
unsigned int area = rect.width() * rect.height();
// m = 1/3500
// c = 0
complexity = area * 2 / 175;
} else {
// Take the average of the width and height.
unsigned int length = (rect.width() + rect.height()) / 2;
if (IsAntiAliased()) {
// m = 1/30
// c = 0
complexity = length * 4 / 3;
} else {
// If AA is disabled, the data shows that at larger sizes the overall
// cost comes down, peaking at around 1000px. As we don't anticipate
// rasterising rects with AA disabled to be all that frequent, just treat
// it as a straight line that peaks at 1000px, beyond which it stays
// constant. The rationale here is that it makes more sense to
// overestimate than to start decreasing the cost as the length goes up.
//
// This should be a reasonable approximation as it doesn't decrease by
// much from 1000px to 2000px.
//
// m = 1/20
// c = 0
complexity = std::min(length, 1000u) * 2;
}
}
AccumulateComplexity(complexity);
}
void DisplayListGLComplexityCalculator::GLHelper::drawOval(
const SkRect& bounds) {
if (IsComplex()) {
return;
}
// DrawOval scales very roughly linearly with the bounding box width/height
// (not area) for stroked styles without AA.
//
// Filled styles and stroked styles with AA scale linearly with the bounding
// box area.
unsigned int area = bounds.width() * bounds.height();
unsigned int complexity;
// There is also a kStrokeAndFill_Style that Skia exposes, but we do not
// currently use it anywhere in Flutter.
if (DrawStyle() == DlDrawStyle::kFill) {
// With filled styles, there is no significant AA penalty.
// m = 1/6000
// c = 0
complexity = area / 30;
} else {
if (IsAntiAliased()) {
// m = 1/4000
// c = 0
complexity = area / 20;
} else {
// Take the average of the width and height.
unsigned int length = (bounds.width() + bounds.height()) / 2;
// m = 1/75
// c = 0
complexity = length * 8 / 3;
}
}
AccumulateComplexity(complexity);
}
void DisplayListGLComplexityCalculator::GLHelper::drawCircle(
const SkPoint& center,
SkScalar radius) {
if (IsComplex()) {
return;
}
unsigned int complexity;
// There is also a kStrokeAndFill_Style that Skia exposes, but we do not
// currently use it anywhere in Flutter.
if (DrawStyle() == DlDrawStyle::kFill) {
// We can ignore pi here
unsigned int area = radius * radius;
// m = 1/525
// c = 50
complexity = (area + 26250) * 8 / 105;
// Penalty of around 8% when AA is disabled.
if (!IsAntiAliased()) {
complexity *= 1.08f;
}
} else {
// Hairline vs non-hairline has no significant performance difference.
if (IsAntiAliased()) {
// m = 1/3
// c = 10
complexity = (radius + 30) * 40 / 3;
} else {
// m = 1/10
// c = 20
complexity = (radius + 200) * 4;
}
}
AccumulateComplexity(complexity);
}
void DisplayListGLComplexityCalculator::GLHelper::drawRRect(
const SkRRect& rrect) {
if (IsComplex()) {
return;
}
// Drawing RRects is split into three performance tiers:
//
// 1) All stroked styles without AA *except* simple/symmetric RRects.
// 2) All filled styles and symmetric stroked styles w/AA.
// 3) Remaining stroked styles with AA.
//
// 1) and 3) scale linearly with length, 2) scales with area.
unsigned int complexity;
// These values were worked out by creating a straight line graph (y=mx+c)
// approximately matching the measured data, normalising the data so that
// 0.0005ms resulted in a score of 100 then simplifying down the formula.
if (DrawStyle() == DlDrawStyle::kFill ||
((rrect.getType() == SkRRect::Type::kSimple_Type) && IsAntiAliased())) {
unsigned int area = rrect.width() * rrect.height();
// m = 1/3200
// c = 0.5
complexity = (area + 1600) / 80;
} else {
// Take the average of the width and height.
unsigned int length = (rrect.width() + rrect.height()) / 2;
// There is some difference between hairline and non-hairline performance
// but the spread is relatively inconsistent and it's pretty much a wash.
if (IsAntiAliased()) {
// m = 1/25
// c = 1
complexity = (length + 25) * 8 / 5;
} else {
// m = 1/50
// c = 0.75
complexity = ((length * 2) + 75) * 2 / 5;
}
}
AccumulateComplexity(complexity);
}
void DisplayListGLComplexityCalculator::GLHelper::drawDRRect(
const SkRRect& outer,
const SkRRect& inner) {
if (IsComplex()) {
return;
}
// There are roughly four classes here:
// a) Filled style.
// b) Complex RRect type with AA enabled and filled style.
// c) Stroked style with AA enabled.
// d) Stroked style with AA disabled.
//
// a) and b) scale linearly with the area, c) and d) scale linearly with
// a single dimension (length). In all cases, the dimensions refer to
// the outer RRect.
unsigned int complexity;
// These values were worked out by creating a straight line graph (y=mx+c)
// approximately matching the measured data, normalising the data so that
// 0.0005ms resulted in a score of 100 then simplifying down the formula.
//
// There is also a kStrokeAndFill_Style that Skia exposes, but we do not
// currently use it anywhere in Flutter.
if (DrawStyle() == DlDrawStyle::kFill) {
unsigned int area = outer.width() * outer.height();
if (outer.getType() == SkRRect::Type::kComplex_Type) {
// m = 1/500
// c = 0.5
complexity = (area + 250) / 5;
} else {
// m = 1/1600
// c = 2
complexity = (area + 3200) / 16;
}
} else {
unsigned int length = (outer.width() + outer.height()) / 2;
if (IsAntiAliased()) {
// m = 1/15
// c = 1
complexity = (length + 15) * 20 / 3;
} else {
// m = 1/27
// c = 0.5
complexity = ((length * 2) + 27) * 50 / 27;
}
}
AccumulateComplexity(complexity);
}
void DisplayListGLComplexityCalculator::GLHelper::drawPath(const SkPath& path) {
if (IsComplex()) {
return;
}
// There is negligible effect on the performance for hairline vs. non-hairline
// stroke widths.
//
// The data for filled styles is currently suspicious, so for now we are going
// to assign scores based on stroked styles.
unsigned int line_verb_cost, quad_verb_cost, conic_verb_cost, cubic_verb_cost;
unsigned int complexity;
if (IsAntiAliased()) {
// There seems to be a fixed cost of around 1ms for calling drawPath with
// AA.
complexity = 200000;
line_verb_cost = 235;
quad_verb_cost = 365;
conic_verb_cost = 365;
cubic_verb_cost = 725;
} else {
// There seems to be a fixed cost of around 0.25ms for calling drawPath.
// without AA
complexity = 50000;
line_verb_cost = 135;
quad_verb_cost = 150;
conic_verb_cost = 200;
cubic_verb_cost = 235;
}
complexity += CalculatePathComplexity(path, line_verb_cost, quad_verb_cost,
conic_verb_cost, cubic_verb_cost);
AccumulateComplexity(complexity);
}
void DisplayListGLComplexityCalculator::GLHelper::drawArc(
const SkRect& oval_bounds,
SkScalar start_degrees,
SkScalar sweep_degrees,
bool use_center) {
if (IsComplex()) {
return;
}
// Hairline vs non-hairline makes no difference to the performance.
// Stroked styles without AA scale linearly with the log of the diameter.
// Stroked styles with AA scale linearly with the area.
// Filled styles scale lienarly with the area.
unsigned int area = oval_bounds.width() * oval_bounds.height();
unsigned int complexity;
// These values were worked out by creating a straight line graph (y=mx+c)
// approximately matching the measured data, normalising the data so that
// 0.0005ms resulted in a score of 100 then simplifying down the formula.
//
// There is also a kStrokeAndFill_Style that Skia exposes, but we do not
// currently use it anywhere in Flutter.
if (DrawStyle() == DlDrawStyle::kStroke) {
if (IsAntiAliased()) {
// m = 1/3800
// c = 12
complexity = (area + 45600) / 171;
} else {
unsigned int diameter = (oval_bounds.width() + oval_bounds.height()) / 2;
// m = 15
// c = -100
// This should never go negative though, so use std::max to ensure
// c is never larger than 15*log_diameter.
//
// Pre-multiply by 15 here so we get a little bit more precision.
unsigned int log_diameter = 15 * log(diameter);
complexity = (log_diameter - std::max(log_diameter, 100u)) * 200 / 9;
}
} else {
if (IsAntiAliased()) {
// m = 1/1000
// c = 10
complexity = (area + 10000) / 45;
} else {
// m = 1/6500
// c = 12
complexity = (area + 52000) * 2 / 585;
}
}
AccumulateComplexity(complexity);
}
void DisplayListGLComplexityCalculator::GLHelper::drawPoints(
DlCanvas::PointMode mode,
uint32_t count,
const SkPoint points[]) {
if (IsComplex()) {
return;
}
unsigned int complexity;
if (IsAntiAliased()) {
if (mode == DlCanvas::PointMode::kPoints) {
if (IsHairline()) {
// This is a special case, it triggers an extremely fast path.
// m = 1/4500
// c = 0
complexity = count * 400 / 9;
} else {
// m = 1/500
// c = 0
complexity = count * 400;
}
} else if (mode == DlCanvas::PointMode::kLines) {
if (IsHairline()) {
// m = 1/750
// c = 0
complexity = count * 800 / 3;
} else {
// m = 1/500
// c = 0
complexity = count * 400;
}
} else {
if (IsHairline()) {
// m = 1/350
// c = 0
complexity = count * 4000 / 7;
} else {
// m = 1/250
// c = 0
complexity = count * 800;
}
}
} else {
if (mode == DlCanvas::PointMode::kPoints) {
// Hairline vs non hairline makes no difference for points without AA.
// m = 1/18000
// c = 0.25
complexity = (count + 4500) * 100 / 9;
} else if (mode == DlCanvas::PointMode::kLines) {
if (IsHairline()) {
// m = 1/8500
// c = 0.25
complexity = (count + 2125) * 400 / 17;
} else {
// m = 1/9000
// c = 0.25
complexity = (count + 2250) * 200 / 9;
}
} else {
// Polygon only really diverges for hairline vs non hairline at large
// point counts, and only by a few %.
// m = 1/7500
// c = 0.25
complexity = (count + 1875) * 80 / 3;
}
}
AccumulateComplexity(complexity);
}
void DisplayListGLComplexityCalculator::GLHelper::drawVertices(
const DlVertices* vertices,
DlBlendMode mode) {
// There is currently no way for us to get the VertexMode from the SkVertices
// object, but for future reference:
//
// TriangleStrip is roughly 25% more expensive than TriangleFan.
// TriangleFan is roughly 5% more expensive than Triangles.
// For the baseline, it's hard to identify the trend. It might be O(n^1/2)
// For now, treat it as linear as an approximation.
//
// m = 1/1600
// c = 1
unsigned int complexity = (vertices->vertex_count() + 1600) * 250 / 2;
AccumulateComplexity(complexity);
}
void DisplayListGLComplexityCalculator::GLHelper::drawImage(
const sk_sp<DlImage> image,
const SkPoint point,
DlImageSampling sampling,
bool render_with_attributes) {
if (IsComplex()) {
return;
}
// AA vs non-AA has a cost but it's dwarfed by the overall cost of the
// drawImage call.
//
// The main difference is if the image is backed by a texture already or not
// If we don't need to upload, then the cost scales linearly with the
// length of the image. If it needs uploading, the cost scales linearly
// with the square of the area (!!!).
SkISize dimensions = image->dimensions();
unsigned int length = (dimensions.width() + dimensions.height()) / 2;
unsigned int area = dimensions.width() * dimensions.height();
// m = 1/13
// c = 0
unsigned int complexity = length * 400 / 13;
if (!image->isTextureBacked()) {
// We can't square the area here as we'll overflow, so let's approximate
// by taking the calculated complexity score and applying a multiplier to
// it.
//
// (complexity * area / 60000) + 4000 gives a reasonable approximation with
// AA (complexity * area / 19000) gives a reasonable approximation without
// AA.
float multiplier;
if (IsAntiAliased()) {
multiplier = area / 60000.0f;
complexity = complexity * multiplier + 4000;
} else {
multiplier = area / 19000.0f;
complexity = complexity * multiplier;
}
}
AccumulateComplexity(complexity);
}
void DisplayListGLComplexityCalculator::GLHelper::ImageRect(
const SkISize& size,
bool texture_backed,
bool render_with_attributes,
bool enforce_src_edges) {
if (IsComplex()) {
return;
}
// Two main groups here - texture-backed and non-texture-backed images.
//
// Within each group, they all perform within a few % of each other *except*
// when we have a strict constraint and anti-aliasing enabled.
// These values were worked out by creating a straight line graph (y=mx+c)
// approximately matching the measured data, normalising the data so that
// 0.0005ms resulted in a score of 100 then simplifying down the formula.
unsigned int complexity;
if (!texture_backed || (texture_backed && render_with_attributes &&
enforce_src_edges && IsAntiAliased())) {
unsigned int area = size.width() * size.height();
// m = 1/4000
// c = 5
complexity = (area + 20000) / 10;
} else {
unsigned int length = (size.width() + size.height()) / 2;
// There's a little bit of spread here but the numbers are pretty large
// anyway.
//
// m = 1/22
// c = 0
complexity = length * 200 / 11;
}
AccumulateComplexity(complexity);
}
void DisplayListGLComplexityCalculator::GLHelper::drawImageNine(
const sk_sp<DlImage> image,
const SkIRect& center,
const SkRect& dst,
DlFilterMode filter,
bool render_with_attributes) {
if (IsComplex()) {
return;
}
SkISize dimensions = image->dimensions();
unsigned int area = dimensions.width() * dimensions.height();
// m = 1/3600
// c = 3
unsigned int complexity = (area + 10800) / 9;
// Uploading incurs about a 40% performance penalty.
if (!image->isTextureBacked()) {
complexity *= 1.4f;
}
AccumulateComplexity(complexity);
}
void DisplayListGLComplexityCalculator::GLHelper::drawDisplayList(
const sk_sp<DisplayList> display_list,
SkScalar opacity) {
if (IsComplex()) {
return;
}
GLHelper helper(Ceiling() - CurrentComplexityScore());
if (opacity < SK_Scalar1 && !display_list->can_apply_group_opacity()) {
auto bounds = display_list->bounds();
helper.saveLayer(bounds, SaveLayerOptions::kWithAttributes, nullptr);
}
display_list->Dispatch(helper);
AccumulateComplexity(helper.ComplexityScore());
}
void DisplayListGLComplexityCalculator::GLHelper::drawTextBlob(
const sk_sp<SkTextBlob> blob,
SkScalar x,
SkScalar y) {
if (IsComplex()) {
return;
}
// DrawTextBlob has a high fixed cost, but if we call it multiple times
// per frame, that fixed cost is greatly reduced per subsequent call. This
// is likely because there is batching being done in SkCanvas.
// Increment draw_text_blob_count_ and calculate the cost at the end.
draw_text_blob_count_++;
}
void DisplayListGLComplexityCalculator::GLHelper::drawTextFrame(
const std::shared_ptr<impeller::TextFrame>& text_frame,
SkScalar x,
SkScalar y) {}
void DisplayListGLComplexityCalculator::GLHelper::drawShadow(
const SkPath& path,
const DlColor color,
const SkScalar elevation,
bool transparent_occluder,
SkScalar dpr) {
if (IsComplex()) {
return;
}
// Elevation has no significant effect on the timings. Whether the shadow
// is cast by a transparent occluder or not has a small impact of around 5%.
//
// The path verbs do have an effect but only if the verb type is cubic; line,
// quad and conic all perform similarly.
float occluder_penalty = 1.0f;
if (transparent_occluder) {
occluder_penalty = 1.20f;
}
// The benchmark uses a test path of around 10 path elements. This is likely
// to be similar to what we see in real world usage, but we should benchmark
// different path lengths to see how much impact there is from varying the
// path length.
//
// For now, we will assume that there is no fixed overhead and that the time
// spent rendering the shadow for a path is split evenly amongst all the verbs
// enumerated.
unsigned int line_verb_cost = 17000; // 0.085ms
unsigned int quad_verb_cost = 20000; // 0.1ms
unsigned int conic_verb_cost = 20000; // 0.1ms
unsigned int cubic_verb_cost = 120000; // 0.6ms
unsigned int complexity = CalculatePathComplexity(
path, line_verb_cost, quad_verb_cost, conic_verb_cost, cubic_verb_cost);
AccumulateComplexity(complexity * occluder_penalty);
}
} // namespace flutter