/*
* 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 "ClipArea.h"
#include "utils/LinearAllocator.h"
#include <SkPath.h>
#include <limits>
#include <type_traits>
namespace android {
namespace uirenderer {
static void handlePoint(Rect& transformedBounds, const Matrix4& transform, float x, float y) {
Vertex v = {x, y};
transform.mapPoint(v.x, v.y);
transformedBounds.expandToCover(v.x, v.y);
}
Rect transformAndCalculateBounds(const Rect& r, const Matrix4& transform) {
const float kMinFloat = std::numeric_limits<float>::lowest();
const float kMaxFloat = std::numeric_limits<float>::max();
Rect transformedBounds = { kMaxFloat, kMaxFloat, kMinFloat, kMinFloat };
handlePoint(transformedBounds, transform, r.left, r.top);
handlePoint(transformedBounds, transform, r.right, r.top);
handlePoint(transformedBounds, transform, r.left, r.bottom);
handlePoint(transformedBounds, transform, r.right, r.bottom);
return transformedBounds;
}
void ClipBase::dump() const {
ALOGD("mode %d" RECT_STRING, mode, RECT_ARGS(rect));
}
/*
* TransformedRectangle
*/
TransformedRectangle::TransformedRectangle() {
}
TransformedRectangle::TransformedRectangle(const Rect& bounds,
const Matrix4& transform)
: mBounds(bounds)
, mTransform(transform) {
}
bool TransformedRectangle::canSimplyIntersectWith(
const TransformedRectangle& other) const {
return mTransform == other.mTransform;
}
void TransformedRectangle::intersectWith(const TransformedRectangle& other) {
mBounds.doIntersect(other.mBounds);
}
bool TransformedRectangle::isEmpty() const {
return mBounds.isEmpty();
}
/*
* RectangleList
*/
RectangleList::RectangleList()
: mTransformedRectanglesCount(0) {
}
bool RectangleList::isEmpty() const {
if (mTransformedRectanglesCount < 1) {
return true;
}
for (int i = 0; i < mTransformedRectanglesCount; i++) {
if (mTransformedRectangles[i].isEmpty()) {
return true;
}
}
return false;
}
int RectangleList::getTransformedRectanglesCount() const {
return mTransformedRectanglesCount;
}
const TransformedRectangle& RectangleList::getTransformedRectangle(int i) const {
return mTransformedRectangles[i];
}
void RectangleList::setEmpty() {
mTransformedRectanglesCount = 0;
}
void RectangleList::set(const Rect& bounds, const Matrix4& transform) {
mTransformedRectanglesCount = 1;
mTransformedRectangles[0] = TransformedRectangle(bounds, transform);
}
bool RectangleList::intersectWith(const Rect& bounds,
const Matrix4& transform) {
TransformedRectangle newRectangle(bounds, transform);
// Try to find a rectangle with a compatible transformation
int index = 0;
for (; index < mTransformedRectanglesCount; index++) {
TransformedRectangle& tr(mTransformedRectangles[index]);
if (tr.canSimplyIntersectWith(newRectangle)) {
tr.intersectWith(newRectangle);
return true;
}
}
// Add it to the list if there is room
if (index < kMaxTransformedRectangles) {
mTransformedRectangles[index] = newRectangle;
mTransformedRectanglesCount += 1;
return true;
}
// This rectangle list is full
return false;
}
Rect RectangleList::calculateBounds() const {
Rect bounds;
for (int index = 0; index < mTransformedRectanglesCount; index++) {
const TransformedRectangle& tr(mTransformedRectangles[index]);
if (index == 0) {
bounds = tr.transformedBounds();
} else {
bounds.doIntersect(tr.transformedBounds());
}
}
return bounds;
}
static SkPath pathFromTransformedRectangle(const Rect& bounds,
const Matrix4& transform) {
SkPath rectPath;
SkPath rectPathTransformed;
rectPath.addRect(bounds.left, bounds.top, bounds.right, bounds.bottom);
SkMatrix skTransform;
transform.copyTo(skTransform);
rectPath.transform(skTransform, &rectPathTransformed);
return rectPathTransformed;
}
SkRegion RectangleList::convertToRegion(const SkRegion& clip) const {
SkRegion rectangleListAsRegion;
for (int index = 0; index < mTransformedRectanglesCount; index++) {
const TransformedRectangle& tr(mTransformedRectangles[index]);
SkPath rectPathTransformed = pathFromTransformedRectangle(
tr.getBounds(), tr.getTransform());
if (index == 0) {
rectangleListAsRegion.setPath(rectPathTransformed, clip);
} else {
SkRegion rectRegion;
rectRegion.setPath(rectPathTransformed, clip);
rectangleListAsRegion.op(rectRegion, SkRegion::kIntersect_Op);
}
}
return rectangleListAsRegion;
}
void RectangleList::transform(const Matrix4& transform) {
for (int index = 0; index < mTransformedRectanglesCount; index++) {
mTransformedRectangles[index].transform(transform);
}
}
/*
* ClipArea
*/
ClipArea::ClipArea()
: mMode(ClipMode::Rectangle) {
}
/*
* Interface
*/
void ClipArea::setViewportDimensions(int width, int height) {
mPostViewportClipObserved = false;
mViewportBounds.set(0, 0, width, height);
mClipRect = mViewportBounds;
}
void ClipArea::setEmpty() {
onClipUpdated();
mMode = ClipMode::Rectangle;
mClipRect.setEmpty();
mClipRegion.setEmpty();
mRectangleList.setEmpty();
}
void ClipArea::setClip(float left, float top, float right, float bottom) {
onClipUpdated();
mMode = ClipMode::Rectangle;
mClipRect.set(left, top, right, bottom);
mClipRegion.setEmpty();
}
void ClipArea::clipRectWithTransform(const Rect& r, const mat4* transform,
SkRegion::Op op) {
if (op == SkRegion::kReplace_Op) mReplaceOpObserved = true;
if (!mPostViewportClipObserved && op == SkRegion::kIntersect_Op) op = SkRegion::kReplace_Op;
onClipUpdated();
switch (mMode) {
case ClipMode::Rectangle:
rectangleModeClipRectWithTransform(r, transform, op);
break;
case ClipMode::RectangleList:
rectangleListModeClipRectWithTransform(r, transform, op);
break;
case ClipMode::Region:
regionModeClipRectWithTransform(r, transform, op);
break;
}
}
void ClipArea::clipRegion(const SkRegion& region, SkRegion::Op op) {
if (op == SkRegion::kReplace_Op) mReplaceOpObserved = true;
if (!mPostViewportClipObserved && op == SkRegion::kIntersect_Op) op = SkRegion::kReplace_Op;
onClipUpdated();
enterRegionMode();
mClipRegion.op(region, op);
onClipRegionUpdated();
}
void ClipArea::clipPathWithTransform(const SkPath& path, const mat4* transform,
SkRegion::Op op) {
if (op == SkRegion::kReplace_Op) mReplaceOpObserved = true;
if (!mPostViewportClipObserved && op == SkRegion::kIntersect_Op) op = SkRegion::kReplace_Op;
onClipUpdated();
SkMatrix skTransform;
transform->copyTo(skTransform);
SkPath transformed;
path.transform(skTransform, &transformed);
SkRegion region;
regionFromPath(transformed, region);
enterRegionMode();
mClipRegion.op(region, op);
onClipRegionUpdated();
}
/*
* Rectangle mode
*/
void ClipArea::enterRectangleMode() {
// Entering rectangle mode discards any
// existing clipping information from the other modes.
// The only way this occurs is by a clip setting operation.
mMode = ClipMode::Rectangle;
}
void ClipArea::rectangleModeClipRectWithTransform(const Rect& r,
const mat4* transform, SkRegion::Op op) {
if (op == SkRegion::kReplace_Op && transform->rectToRect()) {
mClipRect = r;
transform->mapRect(mClipRect);
return;
} else if (op != SkRegion::kIntersect_Op) {
enterRegionMode();
regionModeClipRectWithTransform(r, transform, op);
return;
}
if (transform->rectToRect()) {
Rect transformed(r);
transform->mapRect(transformed);
mClipRect.doIntersect(transformed);
return;
}
enterRectangleListMode();
rectangleListModeClipRectWithTransform(r, transform, op);
}
/*
* RectangleList mode implementation
*/
void ClipArea::enterRectangleListMode() {
// Is is only legal to enter rectangle list mode from
// rectangle mode, since rectangle list mode cannot represent
// all clip areas that can be represented by a region.
ALOG_ASSERT(mMode == ClipMode::Rectangle);
mMode = ClipMode::RectangleList;
mRectangleList.set(mClipRect, Matrix4::identity());
}
void ClipArea::rectangleListModeClipRectWithTransform(const Rect& r,
const mat4* transform, SkRegion::Op op) {
if (op != SkRegion::kIntersect_Op
|| !mRectangleList.intersectWith(r, *transform)) {
enterRegionMode();
regionModeClipRectWithTransform(r, transform, op);
}
}
/*
* Region mode implementation
*/
void ClipArea::enterRegionMode() {
ClipMode oldMode = mMode;
mMode = ClipMode::Region;
if (oldMode != ClipMode::Region) {
if (oldMode == ClipMode::Rectangle) {
mClipRegion.setRect(mClipRect.toSkIRect());
} else {
mClipRegion = mRectangleList.convertToRegion(createViewportRegion());
onClipRegionUpdated();
}
}
}
void ClipArea::regionModeClipRectWithTransform(const Rect& r,
const mat4* transform, SkRegion::Op op) {
SkPath transformedRect = pathFromTransformedRectangle(r, *transform);
SkRegion transformedRectRegion;
regionFromPath(transformedRect, transformedRectRegion);
mClipRegion.op(transformedRectRegion, op);
onClipRegionUpdated();
}
void ClipArea::onClipRegionUpdated() {
if (!mClipRegion.isEmpty()) {
mClipRect.set(mClipRegion.getBounds());
if (mClipRegion.isRect()) {
mClipRegion.setEmpty();
enterRectangleMode();
}
} else {
mClipRect.setEmpty();
}
}
/**
* Clip serialization
*/
const ClipBase* ClipArea::serializeClip(LinearAllocator& allocator) {
if (!mPostViewportClipObserved) {
// Only initial clip-to-viewport observed, so no serialization of clip necessary
return nullptr;
}
static_assert(std::is_trivially_destructible<Rect>::value,
"expect Rect to be trivially destructible");
static_assert(std::is_trivially_destructible<RectangleList>::value,
"expect RectangleList to be trivially destructible");
if (mLastSerialization == nullptr) {
ClipBase* serialization = nullptr;
switch (mMode) {
case ClipMode::Rectangle:
serialization = allocator.create<ClipRect>(mClipRect);
break;
case ClipMode::RectangleList:
serialization = allocator.create<ClipRectList>(mRectangleList);
serialization->rect = mRectangleList.calculateBounds();
break;
case ClipMode::Region:
serialization = allocator.create<ClipRegion>(mClipRegion);
serialization->rect.set(mClipRegion.getBounds());
break;
}
serialization->intersectWithRoot = mReplaceOpObserved;
// TODO: this is only done for draw time, should eventually avoid for record time
serialization->rect.snapToPixelBoundaries();
mLastSerialization = serialization;
}
return mLastSerialization;
}
inline static const RectangleList& getRectList(const ClipBase* scb) {
return reinterpret_cast<const ClipRectList*>(scb)->rectList;
}
inline static const SkRegion& getRegion(const ClipBase* scb) {
return reinterpret_cast<const ClipRegion*>(scb)->region;
}
// Conservative check for too many rectangles to fit in rectangle list.
// For simplicity, doesn't account for rect merging
static bool cannotFitInRectangleList(const ClipArea& clipArea, const ClipBase* scb) {
int currentRectCount = clipArea.isRectangleList()
? clipArea.getRectangleList().getTransformedRectanglesCount()
: 1;
int recordedRectCount = (scb->mode == ClipMode::RectangleList)
? getRectList(scb).getTransformedRectanglesCount()
: 1;
return currentRectCount + recordedRectCount > RectangleList::kMaxTransformedRectangles;
}
static const ClipRect sEmptyClipRect(Rect(0, 0));
const ClipBase* ClipArea::serializeIntersectedClip(LinearAllocator& allocator,
const ClipBase* recordedClip, const Matrix4& recordedClipTransform) {
// if no recordedClip passed, just serialize current state
if (!recordedClip) return serializeClip(allocator);
// if either is empty, clip is empty
if (CC_UNLIKELY(recordedClip->rect.isEmpty())|| mClipRect.isEmpty()) return &sEmptyClipRect;
if (!mLastResolutionResult
|| recordedClip != mLastResolutionClip
|| recordedClipTransform != mLastResolutionTransform) {
mLastResolutionClip = recordedClip;
mLastResolutionTransform = recordedClipTransform;
if (CC_LIKELY(mMode == ClipMode::Rectangle
&& recordedClip->mode == ClipMode::Rectangle
&& recordedClipTransform.rectToRect())) {
// common case - result is a single rectangle
auto rectClip = allocator.create<ClipRect>(recordedClip->rect);
recordedClipTransform.mapRect(rectClip->rect);
rectClip->rect.doIntersect(mClipRect);
rectClip->rect.snapToPixelBoundaries();
mLastResolutionResult = rectClip;
} else if (CC_UNLIKELY(mMode == ClipMode::Region
|| recordedClip->mode == ClipMode::Region
|| cannotFitInRectangleList(*this, recordedClip))) {
// region case
SkRegion other;
switch (recordedClip->mode) {
case ClipMode::Rectangle:
if (CC_LIKELY(recordedClipTransform.rectToRect())) {
// simple transform, skip creating SkPath
Rect resultClip(recordedClip->rect);
recordedClipTransform.mapRect(resultClip);
other.setRect(resultClip.toSkIRect());
} else {
SkPath transformedRect = pathFromTransformedRectangle(recordedClip->rect,
recordedClipTransform);
other.setPath(transformedRect, createViewportRegion());
}
break;
case ClipMode::RectangleList: {
RectangleList transformedList(getRectList(recordedClip));
transformedList.transform(recordedClipTransform);
other = transformedList.convertToRegion(createViewportRegion());
break;
}
case ClipMode::Region:
other = getRegion(recordedClip);
applyTransformToRegion(recordedClipTransform, &other);
}
ClipRegion* regionClip = allocator.create<ClipRegion>();
switch (mMode) {
case ClipMode::Rectangle:
regionClip->region.op(mClipRect.toSkIRect(), other, SkRegion::kIntersect_Op);
break;
case ClipMode::RectangleList:
regionClip->region.op(mRectangleList.convertToRegion(createViewportRegion()),
other, SkRegion::kIntersect_Op);
break;
case ClipMode::Region:
regionClip->region.op(mClipRegion, other, SkRegion::kIntersect_Op);
break;
}
// Don't need to snap, since region's in int bounds
regionClip->rect.set(regionClip->region.getBounds());
mLastResolutionResult = regionClip;
} else {
auto rectListClip = allocator.create<ClipRectList>(mRectangleList);
auto&& rectList = rectListClip->rectList;
if (mMode == ClipMode::Rectangle) {
rectList.set(mClipRect, Matrix4::identity());
}
if (recordedClip->mode == ClipMode::Rectangle) {
rectList.intersectWith(recordedClip->rect, recordedClipTransform);
} else {
const RectangleList& other = getRectList(recordedClip);
for (int i = 0; i < other.getTransformedRectanglesCount(); i++) {
auto&& tr = other.getTransformedRectangle(i);
Matrix4 totalTransform(recordedClipTransform);
totalTransform.multiply(tr.getTransform());
rectList.intersectWith(tr.getBounds(), totalTransform);
}
}
rectListClip->rect = rectList.calculateBounds();
rectListClip->rect.snapToPixelBoundaries();
mLastResolutionResult = rectListClip;
}
}
return mLastResolutionResult;
}
void ClipArea::applyClip(const ClipBase* clip, const Matrix4& transform) {
if (!clip) return; // nothing to do
if (CC_LIKELY(clip->mode == ClipMode::Rectangle)) {
clipRectWithTransform(clip->rect, &transform, SkRegion::kIntersect_Op);
} else if (CC_LIKELY(clip->mode == ClipMode::RectangleList)) {
auto&& rectList = getRectList(clip);
for (int i = 0; i < rectList.getTransformedRectanglesCount(); i++) {
auto&& tr = rectList.getTransformedRectangle(i);
Matrix4 totalTransform(transform);
totalTransform.multiply(tr.getTransform());
clipRectWithTransform(tr.getBounds(), &totalTransform, SkRegion::kIntersect_Op);
}
} else {
SkRegion region(getRegion(clip));
applyTransformToRegion(transform, ®ion);
clipRegion(region, SkRegion::kIntersect_Op);
}
}
void ClipArea::applyTransformToRegion(const Matrix4& transform, SkRegion* region) {
if (transform.rectToRect() && !transform.isPureTranslate()) {
// handle matrices with scale manually by mapping each rect
SkRegion other;
SkRegion::Iterator it(*region);
while (!it.done()) {
Rect rect(it.rect());
transform.mapRect(rect);
rect.snapGeometryToPixelBoundaries(true);
other.op(rect.left, rect.top, rect.right, rect.bottom, SkRegion::kUnion_Op);
it.next();
}
region->swap(other);
} else {
// TODO: handle non-translate transforms properly!
region->translate(transform.getTranslateX(), transform.getTranslateY());
}
}
} /* namespace uirenderer */
} /* namespace android */