android_frameworks_base/libs/hwui/VectorDrawable.cpp
John Reck 83161dcd6a Delete VectorDrawableAtlas
Poking around in a few apps it doesn't appear that
the VectorDrawableAtlas is achieving sufficient
utilization to justify its existence. The potential for
draw call merging doesn't seem warranted for the
RAM cost of the atlas.

Bug: 137853925
Test: builds
Change-Id: Id2419bc6dccb6316636d50c568f8fac75a2d563f
2019-10-04 14:49:20 -07:00

636 lines
22 KiB
C++

/*
* Copyright (C) 2015 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.
*/
#include "VectorDrawable.h"
#include <math.h>
#include <string.h>
#include <utils/Log.h>
#include "PathParser.h"
#include "SkColorFilter.h"
#include "SkImageInfo.h"
#include "SkShader.h"
#include "hwui/Paint.h"
#ifdef __ANDROID__
#include "renderthread/RenderThread.h"
#endif
#include "utils/Macros.h"
#include "utils/TraceUtils.h"
#include "utils/VectorDrawableUtils.h"
namespace android {
namespace uirenderer {
namespace VectorDrawable {
const int Tree::MAX_CACHED_BITMAP_SIZE = 2048;
void Path::dump() {
ALOGD("Path: %s has %zu points", mName.c_str(), mProperties.getData().points.size());
}
// Called from UI thread during the initial setup/theme change.
Path::Path(const char* pathStr, size_t strLength) {
PathParser::ParseResult result;
Data data;
PathParser::getPathDataFromAsciiString(&data, &result, pathStr, strLength);
mStagingProperties.setData(data);
}
Path::Path(const Path& path) : Node(path) {
mStagingProperties.syncProperties(path.mStagingProperties);
}
const SkPath& Path::getUpdatedPath(bool useStagingData, SkPath* tempStagingPath) {
if (useStagingData) {
tempStagingPath->reset();
VectorDrawableUtils::verbsToPath(tempStagingPath, mStagingProperties.getData());
return *tempStagingPath;
} else {
if (mSkPathDirty) {
mSkPath.reset();
VectorDrawableUtils::verbsToPath(&mSkPath, mProperties.getData());
mSkPathDirty = false;
}
return mSkPath;
}
}
void Path::syncProperties() {
if (mStagingPropertiesDirty) {
mProperties.syncProperties(mStagingProperties);
} else {
mStagingProperties.syncProperties(mProperties);
}
mStagingPropertiesDirty = false;
}
FullPath::FullPath(const FullPath& path) : Path(path) {
mStagingProperties.syncProperties(path.mStagingProperties);
}
static void applyTrim(SkPath* outPath, const SkPath& inPath, float trimPathStart, float trimPathEnd,
float trimPathOffset) {
if (trimPathStart == 0.0f && trimPathEnd == 1.0f) {
*outPath = inPath;
return;
}
outPath->reset();
if (trimPathStart == trimPathEnd) {
// Trimmed path should be empty.
return;
}
SkPathMeasure measure(inPath, false);
float len = SkScalarToFloat(measure.getLength());
float start = len * fmod((trimPathStart + trimPathOffset), 1.0f);
float end = len * fmod((trimPathEnd + trimPathOffset), 1.0f);
if (start > end) {
measure.getSegment(start, len, outPath, true);
if (end > 0) {
measure.getSegment(0, end, outPath, true);
}
} else {
measure.getSegment(start, end, outPath, true);
}
}
const SkPath& FullPath::getUpdatedPath(bool useStagingData, SkPath* tempStagingPath) {
if (!useStagingData && !mSkPathDirty && !mProperties.mTrimDirty) {
return mTrimmedSkPath;
}
Path::getUpdatedPath(useStagingData, tempStagingPath);
SkPath* outPath;
if (useStagingData) {
SkPath inPath = *tempStagingPath;
applyTrim(tempStagingPath, inPath, mStagingProperties.getTrimPathStart(),
mStagingProperties.getTrimPathEnd(), mStagingProperties.getTrimPathOffset());
outPath = tempStagingPath;
} else {
if (mProperties.getTrimPathStart() != 0.0f || mProperties.getTrimPathEnd() != 1.0f) {
mProperties.mTrimDirty = false;
applyTrim(&mTrimmedSkPath, mSkPath, mProperties.getTrimPathStart(),
mProperties.getTrimPathEnd(), mProperties.getTrimPathOffset());
outPath = &mTrimmedSkPath;
} else {
outPath = &mSkPath;
}
}
const FullPathProperties& properties = useStagingData ? mStagingProperties : mProperties;
bool setFillPath = properties.getFillGradient() != nullptr ||
properties.getFillColor() != SK_ColorTRANSPARENT;
if (setFillPath) {
SkPath::FillType ft = static_cast<SkPath::FillType>(properties.getFillType());
outPath->setFillType(ft);
}
return *outPath;
}
void FullPath::dump() {
Path::dump();
ALOGD("stroke width, color, alpha: %f, %d, %f, fill color, alpha: %d, %f",
mProperties.getStrokeWidth(), mProperties.getStrokeColor(), mProperties.getStrokeAlpha(),
mProperties.getFillColor(), mProperties.getFillAlpha());
}
inline SkColor applyAlpha(SkColor color, float alpha) {
int alphaBytes = SkColorGetA(color);
return SkColorSetA(color, alphaBytes * alpha);
}
void FullPath::draw(SkCanvas* outCanvas, bool useStagingData) {
const FullPathProperties& properties = useStagingData ? mStagingProperties : mProperties;
SkPath tempStagingPath;
const SkPath& renderPath = getUpdatedPath(useStagingData, &tempStagingPath);
// Draw path's fill, if fill color or gradient is valid
bool needsFill = false;
SkPaint paint;
if (properties.getFillGradient() != nullptr) {
paint.setColor(applyAlpha(SK_ColorBLACK, properties.getFillAlpha()));
paint.setShader(sk_sp<SkShader>(SkSafeRef(properties.getFillGradient())));
needsFill = true;
} else if (properties.getFillColor() != SK_ColorTRANSPARENT) {
paint.setColor(applyAlpha(properties.getFillColor(), properties.getFillAlpha()));
needsFill = true;
}
if (needsFill) {
paint.setStyle(SkPaint::Style::kFill_Style);
paint.setAntiAlias(mAntiAlias);
outCanvas->drawPath(renderPath, paint);
}
// Draw path's stroke, if stroke color or Gradient is valid
bool needsStroke = false;
if (properties.getStrokeGradient() != nullptr) {
paint.setColor(applyAlpha(SK_ColorBLACK, properties.getStrokeAlpha()));
paint.setShader(sk_sp<SkShader>(SkSafeRef(properties.getStrokeGradient())));
needsStroke = true;
} else if (properties.getStrokeColor() != SK_ColorTRANSPARENT) {
paint.setColor(applyAlpha(properties.getStrokeColor(), properties.getStrokeAlpha()));
needsStroke = true;
}
if (needsStroke) {
paint.setStyle(SkPaint::Style::kStroke_Style);
paint.setAntiAlias(mAntiAlias);
paint.setStrokeJoin(SkPaint::Join(properties.getStrokeLineJoin()));
paint.setStrokeCap(SkPaint::Cap(properties.getStrokeLineCap()));
paint.setStrokeMiter(properties.getStrokeMiterLimit());
paint.setStrokeWidth(properties.getStrokeWidth());
outCanvas->drawPath(renderPath, paint);
}
}
void FullPath::syncProperties() {
Path::syncProperties();
if (mStagingPropertiesDirty) {
mProperties.syncProperties(mStagingProperties);
} else {
// Update staging property with property values from animation.
mStagingProperties.syncProperties(mProperties);
}
mStagingPropertiesDirty = false;
}
REQUIRE_COMPATIBLE_LAYOUT(FullPath::FullPathProperties::PrimitiveFields);
static_assert(sizeof(float) == sizeof(int32_t), "float is not the same size as int32_t");
static_assert(sizeof(SkColor) == sizeof(int32_t), "SkColor is not the same size as int32_t");
bool FullPath::FullPathProperties::copyProperties(int8_t* outProperties, int length) const {
int propertyDataSize = sizeof(FullPathProperties::PrimitiveFields);
if (length != propertyDataSize) {
LOG_ALWAYS_FATAL("Properties needs exactly %d bytes, a byte array of size %d is provided",
propertyDataSize, length);
return false;
}
PrimitiveFields* out = reinterpret_cast<PrimitiveFields*>(outProperties);
*out = mPrimitiveFields;
return true;
}
void FullPath::FullPathProperties::setColorPropertyValue(int propertyId, int32_t value) {
Property currentProperty = static_cast<Property>(propertyId);
if (currentProperty == Property::strokeColor) {
setStrokeColor(value);
} else if (currentProperty == Property::fillColor) {
setFillColor(value);
} else {
LOG_ALWAYS_FATAL(
"Error setting color property on FullPath: No valid property"
" with id: %d",
propertyId);
}
}
void FullPath::FullPathProperties::setPropertyValue(int propertyId, float value) {
Property property = static_cast<Property>(propertyId);
switch (property) {
case Property::strokeWidth:
setStrokeWidth(value);
break;
case Property::strokeAlpha:
setStrokeAlpha(value);
break;
case Property::fillAlpha:
setFillAlpha(value);
break;
case Property::trimPathStart:
setTrimPathStart(value);
break;
case Property::trimPathEnd:
setTrimPathEnd(value);
break;
case Property::trimPathOffset:
setTrimPathOffset(value);
break;
default:
LOG_ALWAYS_FATAL("Invalid property id: %d for animation", propertyId);
break;
}
}
void ClipPath::draw(SkCanvas* outCanvas, bool useStagingData) {
SkPath tempStagingPath;
outCanvas->clipPath(getUpdatedPath(useStagingData, &tempStagingPath));
}
Group::Group(const Group& group) : Node(group) {
mStagingProperties.syncProperties(group.mStagingProperties);
}
void Group::draw(SkCanvas* outCanvas, bool useStagingData) {
// Save the current clip and matrix information, which is local to this group.
SkAutoCanvasRestore saver(outCanvas, true);
// apply the current group's matrix to the canvas
SkMatrix stackedMatrix;
const GroupProperties& prop = useStagingData ? mStagingProperties : mProperties;
getLocalMatrix(&stackedMatrix, prop);
outCanvas->concat(stackedMatrix);
// Draw the group tree in the same order as the XML file.
for (auto& child : mChildren) {
child->draw(outCanvas, useStagingData);
}
// Restore the previous clip and matrix information.
}
void Group::dump() {
ALOGD("Group %s has %zu children: ", mName.c_str(), mChildren.size());
ALOGD("Group translateX, Y : %f, %f, scaleX, Y: %f, %f", mProperties.getTranslateX(),
mProperties.getTranslateY(), mProperties.getScaleX(), mProperties.getScaleY());
for (size_t i = 0; i < mChildren.size(); i++) {
mChildren[i]->dump();
}
}
void Group::syncProperties() {
// Copy over the dirty staging properties
if (mStagingPropertiesDirty) {
mProperties.syncProperties(mStagingProperties);
} else {
mStagingProperties.syncProperties(mProperties);
}
mStagingPropertiesDirty = false;
for (auto& child : mChildren) {
child->syncProperties();
}
}
void Group::getLocalMatrix(SkMatrix* outMatrix, const GroupProperties& properties) {
outMatrix->reset();
// TODO: use rotate(mRotate, mPivotX, mPivotY) and scale with pivot point, instead of
// translating to pivot for rotating and scaling, then translating back.
outMatrix->postTranslate(-properties.getPivotX(), -properties.getPivotY());
outMatrix->postScale(properties.getScaleX(), properties.getScaleY());
outMatrix->postRotate(properties.getRotation(), 0, 0);
outMatrix->postTranslate(properties.getTranslateX() + properties.getPivotX(),
properties.getTranslateY() + properties.getPivotY());
}
void Group::addChild(Node* child) {
mChildren.emplace_back(child);
if (mPropertyChangedListener != nullptr) {
child->setPropertyChangedListener(mPropertyChangedListener);
}
}
bool Group::GroupProperties::copyProperties(float* outProperties, int length) const {
int propertyCount = static_cast<int>(Property::count);
if (length != propertyCount) {
LOG_ALWAYS_FATAL("Properties needs exactly %d bytes, a byte array of size %d is provided",
propertyCount, length);
return false;
}
PrimitiveFields* out = reinterpret_cast<PrimitiveFields*>(outProperties);
*out = mPrimitiveFields;
return true;
}
// TODO: Consider animating the properties as float pointers
// Called on render thread
float Group::GroupProperties::getPropertyValue(int propertyId) const {
Property currentProperty = static_cast<Property>(propertyId);
switch (currentProperty) {
case Property::rotate:
return getRotation();
case Property::pivotX:
return getPivotX();
case Property::pivotY:
return getPivotY();
case Property::scaleX:
return getScaleX();
case Property::scaleY:
return getScaleY();
case Property::translateX:
return getTranslateX();
case Property::translateY:
return getTranslateY();
default:
LOG_ALWAYS_FATAL("Invalid property index: %d", propertyId);
return 0;
}
}
// Called on render thread
void Group::GroupProperties::setPropertyValue(int propertyId, float value) {
Property currentProperty = static_cast<Property>(propertyId);
switch (currentProperty) {
case Property::rotate:
setRotation(value);
break;
case Property::pivotX:
setPivotX(value);
break;
case Property::pivotY:
setPivotY(value);
break;
case Property::scaleX:
setScaleX(value);
break;
case Property::scaleY:
setScaleY(value);
break;
case Property::translateX:
setTranslateX(value);
break;
case Property::translateY:
setTranslateY(value);
break;
default:
LOG_ALWAYS_FATAL("Invalid property index: %d", propertyId);
}
}
bool Group::isValidProperty(int propertyId) {
return GroupProperties::isValidProperty(propertyId);
}
bool Group::GroupProperties::isValidProperty(int propertyId) {
return propertyId >= 0 && propertyId < static_cast<int>(Property::count);
}
int Tree::draw(Canvas* outCanvas, SkColorFilter* colorFilter, const SkRect& bounds,
bool needsMirroring, bool canReuseCache) {
// The imageView can scale the canvas in different ways, in order to
// avoid blurry scaling, we have to draw into a bitmap with exact pixel
// size first. This bitmap size is determined by the bounds and the
// canvas scale.
SkMatrix canvasMatrix;
outCanvas->getMatrix(&canvasMatrix);
float canvasScaleX = 1.0f;
float canvasScaleY = 1.0f;
if (canvasMatrix.getSkewX() == 0 && canvasMatrix.getSkewY() == 0) {
// Only use the scale value when there's no skew or rotation in the canvas matrix.
// TODO: Add a cts test for drawing VD on a canvas with negative scaling factors.
canvasScaleX = fabs(canvasMatrix.getScaleX());
canvasScaleY = fabs(canvasMatrix.getScaleY());
}
int scaledWidth = (int)(bounds.width() * canvasScaleX);
int scaledHeight = (int)(bounds.height() * canvasScaleY);
scaledWidth = std::min(Tree::MAX_CACHED_BITMAP_SIZE, scaledWidth);
scaledHeight = std::min(Tree::MAX_CACHED_BITMAP_SIZE, scaledHeight);
if (scaledWidth <= 0 || scaledHeight <= 0) {
return 0;
}
mStagingProperties.setScaledSize(scaledWidth, scaledHeight);
int saveCount = outCanvas->save(SaveFlags::MatrixClip);
outCanvas->translate(bounds.fLeft, bounds.fTop);
// Handle RTL mirroring.
if (needsMirroring) {
outCanvas->translate(bounds.width(), 0);
outCanvas->scale(-1.0f, 1.0f);
}
mStagingProperties.setColorFilter(colorFilter);
// At this point, canvas has been translated to the right position.
// And we use this bound for the destination rect for the drawBitmap, so
// we offset to (0, 0);
SkRect tmpBounds = bounds;
tmpBounds.offsetTo(0, 0);
mStagingProperties.setBounds(tmpBounds);
outCanvas->drawVectorDrawable(this);
outCanvas->restoreToCount(saveCount);
return scaledWidth * scaledHeight;
}
void Tree::drawStaging(Canvas* outCanvas) {
bool redrawNeeded = allocateBitmapIfNeeded(mStagingCache, mStagingProperties.getScaledWidth(),
mStagingProperties.getScaledHeight());
// draw bitmap cache
if (redrawNeeded || mStagingCache.dirty) {
updateBitmapCache(*mStagingCache.bitmap, true);
mStagingCache.dirty = false;
}
SkPaint skp;
getPaintFor(&skp, mStagingProperties);
Paint paint;
paint.setFilterQuality(skp.getFilterQuality());
paint.setColorFilter(skp.refColorFilter());
paint.setAlpha(skp.getAlpha());
outCanvas->drawBitmap(*mStagingCache.bitmap, 0, 0, mStagingCache.bitmap->width(),
mStagingCache.bitmap->height(), mStagingProperties.getBounds().left(),
mStagingProperties.getBounds().top(),
mStagingProperties.getBounds().right(),
mStagingProperties.getBounds().bottom(), &paint);
}
void Tree::getPaintFor(SkPaint* outPaint, const TreeProperties& prop) const {
// HWUI always draws VD with bilinear filtering.
outPaint->setFilterQuality(kLow_SkFilterQuality);
if (prop.getColorFilter() != nullptr) {
outPaint->setColorFilter(sk_ref_sp(prop.getColorFilter()));
}
outPaint->setAlpha(prop.getRootAlpha() * 255);
}
Bitmap& Tree::getBitmapUpdateIfDirty() {
bool redrawNeeded = allocateBitmapIfNeeded(mCache, mProperties.getScaledWidth(),
mProperties.getScaledHeight());
if (redrawNeeded || mCache.dirty) {
updateBitmapCache(*mCache.bitmap, false);
mCache.dirty = false;
}
return *mCache.bitmap;
}
void Tree::draw(SkCanvas* canvas, const SkRect& bounds, const SkPaint& inPaint) {
if (canvas->quickReject(bounds)) {
// The RenderNode is on screen, but the AVD is not.
return;
}
// Update the paint for any animatable properties
SkPaint paint = inPaint;
paint.setAlpha(mProperties.getRootAlpha() * 255);
Bitmap& bitmap = getBitmapUpdateIfDirty();
SkBitmap skiaBitmap;
bitmap.getSkBitmap(&skiaBitmap);
int scaledWidth = SkScalarCeilToInt(mProperties.getScaledWidth());
int scaledHeight = SkScalarCeilToInt(mProperties.getScaledHeight());
canvas->drawBitmapRect(skiaBitmap, SkRect::MakeWH(scaledWidth, scaledHeight), bounds,
&paint, SkCanvas::kFast_SrcRectConstraint);
}
void Tree::updateBitmapCache(Bitmap& bitmap, bool useStagingData) {
SkBitmap outCache;
bitmap.getSkBitmap(&outCache);
int cacheWidth = outCache.width();
int cacheHeight = outCache.height();
ATRACE_FORMAT("VectorDrawable repaint %dx%d", cacheWidth, cacheHeight);
outCache.eraseColor(SK_ColorTRANSPARENT);
SkCanvas outCanvas(outCache);
float viewportWidth =
useStagingData ? mStagingProperties.getViewportWidth() : mProperties.getViewportWidth();
float viewportHeight = useStagingData ? mStagingProperties.getViewportHeight()
: mProperties.getViewportHeight();
float scaleX = cacheWidth / viewportWidth;
float scaleY = cacheHeight / viewportHeight;
outCanvas.scale(scaleX, scaleY);
mRootNode->draw(&outCanvas, useStagingData);
}
bool Tree::allocateBitmapIfNeeded(Cache& cache, int width, int height) {
if (!canReuseBitmap(cache.bitmap.get(), width, height)) {
SkImageInfo info = SkImageInfo::MakeN32(width, height, kPremul_SkAlphaType);
cache.bitmap = Bitmap::allocateHeapBitmap(info);
return true;
}
return false;
}
bool Tree::canReuseBitmap(Bitmap* bitmap, int width, int height) {
return bitmap && width <= bitmap->width() && height <= bitmap->height();
}
void Tree::onPropertyChanged(TreeProperties* prop) {
if (prop == &mStagingProperties) {
mStagingCache.dirty = true;
} else {
mCache.dirty = true;
}
}
class MinMaxAverage {
public:
void add(float sample) {
if (mCount == 0) {
mMin = sample;
mMax = sample;
} else {
mMin = std::min(mMin, sample);
mMax = std::max(mMax, sample);
}
mTotal += sample;
mCount++;
}
float average() { return mTotal / mCount; }
float min() { return mMin; }
float max() { return mMax; }
float delta() { return mMax - mMin; }
private:
float mMin = 0.0f;
float mMax = 0.0f;
float mTotal = 0.0f;
int mCount = 0;
};
BitmapPalette Tree::computePalette() {
// TODO Cache this and share the code with Bitmap.cpp
ATRACE_CALL();
// TODO: This calculation of converting to HSV & tracking min/max is probably overkill
// Experiment with something simpler since we just want to figure out if it's "color-ful"
// and then the average perceptual lightness.
MinMaxAverage hue, saturation, value;
int sampledCount = 0;
// Sample a grid of 100 pixels to get an overall estimation of the colors in play
mRootNode->forEachFillColor([&](SkColor color) {
if (SkColorGetA(color) < 75) {
return;
}
sampledCount++;
float hsv[3];
SkColorToHSV(color, hsv);
hue.add(hsv[0]);
saturation.add(hsv[1]);
value.add(hsv[2]);
});
if (sampledCount == 0) {
ALOGV("VectorDrawable is mostly translucent");
return BitmapPalette::Unknown;
}
ALOGV("samples = %d, hue [min = %f, max = %f, avg = %f]; saturation [min = %f, max = %f, avg = "
"%f]; value [min = %f, max = %f, avg = %f]",
sampledCount, hue.min(), hue.max(), hue.average(), saturation.min(), saturation.max(),
saturation.average(), value.min(), value.max(), value.average());
if (hue.delta() <= 20 && saturation.delta() <= .1f) {
if (value.average() >= .5f) {
return BitmapPalette::Light;
} else {
return BitmapPalette::Dark;
}
}
return BitmapPalette::Unknown;
}
} // namespace VectorDrawable
} // namespace uirenderer
} // namespace android