Merge changes I4f72448f,I74b7233c into nyc-dev

* changes:
  Fix ripple positioning within scrolled node
  Clip projected ripples to outlines
This commit is contained in:
Chris Craik
2016-03-02 18:52:33 +00:00
committed by Android (Google) Code Review
10 changed files with 251 additions and 50 deletions

View File

@@ -30,6 +30,7 @@
#include <algorithm>
#include <math.h>
#include <SkPaintDefaults.h>
#include <SkPathOps.h>
namespace android {
namespace uirenderer {
@@ -527,6 +528,12 @@ void BakedOpDispatcher::onOvalOp(BakedOpRenderer& renderer, const OvalOp& op, co
SkPath path;
SkRect rect = getBoundsOfFill(op);
path.addOval(rect);
if (state.computedState.localProjectionPathMask != nullptr) {
// Mask the ripple path by the local space projection mask in local space.
// Note that this can create CCW paths.
Op(path, *state.computedState.localProjectionPathMask, kIntersect_SkPathOp, &path);
}
renderConvexPath(renderer, state, path, *(op.paint));
}
}

View File

@@ -63,9 +63,22 @@ ResolvedRenderState::ResolvedRenderState(LinearAllocator& allocator, Snapshot& s
clipState = nullptr;
clippedBounds.setEmpty();
} else {
// Not rejected! compute true clippedBounds and clipSideFlags
// Not rejected! compute true clippedBounds, clipSideFlags, and path mask
clipSideFlags = computeClipSideFlags(clipRect, clippedBounds);
clippedBounds.doIntersect(clipRect);
if (CC_UNLIKELY(snapshot.projectionPathMask)) {
// map projection path mask from render target space into op space,
// so intersection with op geometry is possible
Matrix4 inverseTransform;
inverseTransform.loadInverse(transform);
SkMatrix skInverseTransform;
inverseTransform.copyTo(skInverseTransform);
auto localMask = allocator.create<SkPath>();
snapshot.projectionPathMask->transform(skInverseTransform, localMask);
localProjectionPathMask = localMask;
}
}
}
@@ -73,13 +86,15 @@ ResolvedRenderState::ResolvedRenderState(LinearAllocator& allocator, Snapshot& s
: transform(*snapshot.transform)
, clipState(snapshot.mutateClipArea().serializeClip(allocator))
, clippedBounds(clipState->rect)
, clipSideFlags(OpClipSideFlags::Full) {}
, clipSideFlags(OpClipSideFlags::Full)
, localProjectionPathMask(nullptr) {}
ResolvedRenderState::ResolvedRenderState(const ClipRect* clipRect, const Rect& dstRect)
: transform(Matrix4::identity())
, clipState(clipRect)
, clippedBounds(dstRect)
, clipSideFlags(computeClipSideFlags(clipRect->rect, dstRect)) {
, clipSideFlags(computeClipSideFlags(clipRect->rect, dstRect))
, localProjectionPathMask(nullptr) {
clippedBounds.doIntersect(clipRect->rect);
}

View File

@@ -88,6 +88,7 @@ public:
const ClipBase* clipState = nullptr;
Rect clippedBounds;
int clipSideFlags = 0;
const SkPath* localProjectionPathMask = nullptr;
};
/**
@@ -154,7 +155,6 @@ public:
// simple state (straight pointer/value storage):
const float alpha;
const RoundRectClipState* roundRectClipState;
const ProjectionPathMask* projectionPathMask;
const RecordedOp* op;
private:
@@ -165,21 +165,18 @@ private:
: computedState(allocator, snapshot, recordedOp, expandForStroke)
, alpha(snapshot.alpha)
, roundRectClipState(snapshot.roundRectClipState)
, projectionPathMask(snapshot.projectionPathMask)
, op(&recordedOp) {}
BakedOpState(LinearAllocator& allocator, Snapshot& snapshot, const ShadowOp* shadowOpPtr)
: computedState(allocator, snapshot)
, alpha(snapshot.alpha)
, roundRectClipState(snapshot.roundRectClipState)
, projectionPathMask(snapshot.projectionPathMask)
, op(shadowOpPtr) {}
BakedOpState(const ClipRect* clipRect, const Rect& dstRect, const RecordedOp& recordedOp)
: computedState(clipRect, dstRect)
, alpha(1.0f)
, roundRectClipState(nullptr)
, projectionPathMask(nullptr)
, op(&recordedOp) {}
};

View File

@@ -389,34 +389,38 @@ void FrameBuilder::deferShadow(const RenderNodeOp& casterNodeOp) {
}
void FrameBuilder::deferProjectedChildren(const RenderNode& renderNode) {
const SkPath* projectionReceiverOutline = renderNode.properties().getOutline().getPath();
int count = mCanvasState.save(SaveFlags::MatrixClip);
const SkPath* projectionReceiverOutline = renderNode.properties().getOutline().getPath();
// can't be null, since DL=null node rejection happens before deferNodePropsAndOps
const DisplayList& displayList = *(renderNode.getDisplayList());
const RecordedOp* op = (displayList.getOps()[displayList.projectionReceiveIndex]);
const RenderNodeOp* backgroundOp = static_cast<const RenderNodeOp*>(op);
const RenderProperties& backgroundProps = backgroundOp->renderNode->properties();
// Transform renderer to match background we're projecting onto
// (by offsetting canvas by translationX/Y of background rendernode, since only those are set)
mCanvasState.translate(backgroundProps.getTranslationX(), backgroundProps.getTranslationY());
// If the projection receiver has an outline, we mask projected content to it
// (which we know, apriori, are all tessellated paths)
mCanvasState.setProjectionPathMask(mAllocator, projectionReceiverOutline);
// draw projected nodes
for (size_t i = 0; i < renderNode.mProjectedNodes.size(); i++) {
RenderNodeOp* childOp = renderNode.mProjectedNodes[i];
int restoreTo = mCanvasState.save(SaveFlags::Matrix);
mCanvasState.concatMatrix(childOp->transformFromCompositingAncestor);
deferRenderNodeOpImpl(*childOp);
mCanvasState.restoreToCount(restoreTo);
SkPath transformedMaskPath; // on stack, since BakedOpState makes a deep copy
if (projectionReceiverOutline) {
// transform the mask for this projector into render target space
// TODO: consider combining both transforms by stashing transform instead of applying
SkMatrix skCurrentTransform;
mCanvasState.currentTransform()->copyTo(skCurrentTransform);
projectionReceiverOutline->transform(
skCurrentTransform,
&transformedMaskPath);
mCanvasState.setProjectionPathMask(mAllocator, &transformedMaskPath);
}
for (size_t i = 0; i < renderNode.mProjectedNodes.size(); i++) {
RenderNodeOp* childOp = renderNode.mProjectedNodes[i];
RenderNode& childNode = *childOp->renderNode;
// Draw child if it has content, but ignore state in childOp - matrix already applied to
// transformFromCompositingAncestor, and record-time clip is ignored when projecting
if (!childNode.nothingToDraw()) {
int restoreTo = mCanvasState.save(SaveFlags::MatrixClip);
// Apply transform between ancestor and projected descendant
mCanvasState.concatMatrix(childOp->transformFromCompositingAncestor);
deferNodePropsAndOps(childNode);
mCanvasState.restoreToCount(restoreTo);
}
}
mCanvasState.restoreToCount(count);
}

View File

@@ -140,7 +140,10 @@ public:
// Identical round rect clip state means both ops will clip in the same way, or not at all.
// As the state objects are const, we can compare their pointers to determine mergeability
if (lhs->roundRectClipState != rhs->roundRectClipState) return false;
if (lhs->projectionPathMask != rhs->projectionPathMask) return false;
// Local masks prevent merge, since they're potentially in different coordinate spaces
if (lhs->computedState.localProjectionPathMask
|| rhs->computedState.localProjectionPathMask) return false;
/* Clipping compatibility check
*

View File

@@ -1148,7 +1148,9 @@ bool OpenGLRenderer::storeDisplayState(DeferredDisplayState& state, int stateDef
// always store/restore, since these are just pointers
state.mRoundRectClipState = currentSnapshot()->roundRectClipState;
#if !HWUI_NEW_OPS
state.mProjectionPathMask = currentSnapshot()->projectionPathMask;
#endif
return false;
}
@@ -1156,7 +1158,9 @@ void OpenGLRenderer::restoreDisplayState(const DeferredDisplayState& state, bool
setGlobalMatrix(state.mMatrix);
writableSnapshot()->alpha = state.mAlpha;
writableSnapshot()->roundRectClipState = state.mRoundRectClipState;
#if !HWUI_NEW_OPS
writableSnapshot()->projectionPathMask = state.mProjectionPathMask;
#endif
if (state.mClipValid && !skipClipRestore) {
writableSnapshot()->setClip(state.mClip.left, state.mClip.top,
@@ -1833,6 +1837,7 @@ void OpenGLRenderer::drawCircle(float x, float y, float radius, const SkPaint* p
path.addCircle(x, y, radius);
}
#if !HWUI_NEW_OPS
if (CC_UNLIKELY(currentSnapshot()->projectionPathMask != nullptr)) {
// mask ripples with projection mask
SkPath maskPath = *(currentSnapshot()->projectionPathMask->projectionMask);
@@ -1852,6 +1857,7 @@ void OpenGLRenderer::drawCircle(float x, float y, float radius, const SkPaint* p
// in local space. Note that this can create CCW paths.
Op(path, maskPath, kIntersect_SkPathOp, &path);
}
#endif
drawConvexPath(path, p);
}

View File

@@ -146,6 +146,9 @@ void Snapshot::resetTransform(float x, float y, float z) {
}
void Snapshot::buildScreenSpaceTransform(Matrix4* outTransform) const {
#if HWUI_NEW_OPS
LOG_ALWAYS_FATAL("not supported - not needed by new ops");
#else
// build (reverse ordered) list of the stack of snapshots, terminated with a NULL
Vector<const Snapshot*> snapshotList;
snapshotList.push(nullptr);
@@ -171,6 +174,7 @@ void Snapshot::buildScreenSpaceTransform(Matrix4* outTransform) const {
outTransform->multiply(*(current->transform));
}
}
#endif
}
///////////////////////////////////////////////////////////////////////////////
@@ -223,15 +227,19 @@ void Snapshot::setClippingRoundRect(LinearAllocator& allocator, const Rect& boun
}
void Snapshot::setProjectionPathMask(LinearAllocator& allocator, const SkPath* path) {
#if HWUI_NEW_OPS
// TODO: remove allocator param for HWUI_NEW_OPS
projectionPathMask = path;
#else
if (path) {
ProjectionPathMask* mask = new (allocator) ProjectionPathMask;
mask->projectionMask = path;
buildScreenSpaceTransform(&(mask->projectionMaskTransform));
projectionPathMask = mask;
} else {
projectionPathMask = nullptr;
}
#endif
}
///////////////////////////////////////////////////////////////////////////////

View File

@@ -63,6 +63,7 @@ public:
float radius;
};
// TODO: remove for HWUI_NEW_OPS
class ProjectionPathMask {
public:
static void* operator new(size_t size) = delete;
@@ -219,6 +220,7 @@ public:
* Fills outTransform with the current, total transform to screen space,
* across layer boundaries.
*/
// TODO: remove for HWUI_NEW_OPS
void buildScreenSpaceTransform(Matrix4* outTransform) const;
/**
@@ -294,9 +296,13 @@ public:
const RoundRectClipState* roundRectClipState;
/**
* Current projection masking path - used exclusively to mask tessellated circles.
* Current projection masking path - used exclusively to mask projected, tessellated circles.
*/
#if HWUI_NEW_OPS
const SkPath* projectionPathMask;
#else
const ProjectionPathMask* projectionPathMask;
#endif
void dump() const;

View File

@@ -990,21 +990,26 @@ TEST(FrameBuilder, projectionReorder) {
EXPECT_EQ(Rect(100, 100), op.unmappedBounds);
EXPECT_EQ(SK_ColorWHITE, op.paint->getColor());
expectedMatrix.loadIdentity();
EXPECT_EQ(nullptr, state.computedState.localProjectionPathMask);
break;
case 1:
EXPECT_EQ(Rect(-10, -10, 60, 60), op.unmappedBounds);
EXPECT_EQ(SK_ColorDKGRAY, op.paint->getColor());
expectedMatrix.loadTranslate(50, 50, 0); // TODO: should scroll be respected here?
expectedMatrix.loadTranslate(50 - scrollX, 50 - scrollY, 0);
ASSERT_NE(nullptr, state.computedState.localProjectionPathMask);
EXPECT_EQ(Rect(-35, -30, 45, 50),
Rect(state.computedState.localProjectionPathMask->getBounds()));
break;
case 2:
EXPECT_EQ(Rect(100, 50), op.unmappedBounds);
EXPECT_EQ(SK_ColorBLUE, op.paint->getColor());
expectedMatrix.loadTranslate(-scrollX, 50 - scrollY, 0);
EXPECT_EQ(nullptr, state.computedState.localProjectionPathMask);
break;
default:
ADD_FAILURE();
}
EXPECT_MATRIX_APPROX_EQ(expectedMatrix, state.computedState.transform);
EXPECT_EQ(expectedMatrix, state.computedState.transform);
}
};
@@ -1045,6 +1050,9 @@ TEST(FrameBuilder, projectionReorder) {
});
auto parent = TestUtils::createNode(0, 0, 100, 100,
[&receiverBackground, &child](RenderProperties& properties, RecordingCanvas& canvas) {
// Set a rect outline for the projecting ripple to be masked against.
properties.mutableOutline().setRoundRect(10, 10, 90, 90, 5, 1.0f);
canvas.save(SaveFlags::MatrixClip);
canvas.translate(-scrollX, -scrollY); // Apply scroll (note: bg undoes this internally)
canvas.drawRenderNode(receiverBackground.get());
@@ -1059,6 +1067,145 @@ TEST(FrameBuilder, projectionReorder) {
EXPECT_EQ(3, renderer.getIndex());
}
RENDERTHREAD_TEST(FrameBuilder, projectionHwLayer) {
static const int scrollX = 5;
static const int scrollY = 10;
class ProjectionHwLayerTestRenderer : public TestRendererBase {
public:
void startRepaintLayer(OffscreenBuffer* offscreenBuffer, const Rect& repaintRect) override {
EXPECT_EQ(0, mIndex++);
}
void onArcOp(const ArcOp& op, const BakedOpState& state) override {
EXPECT_EQ(1, mIndex++);
ASSERT_EQ(nullptr, state.computedState.localProjectionPathMask);
}
void endLayer() override {
EXPECT_EQ(2, mIndex++);
}
void onRectOp(const RectOp& op, const BakedOpState& state) override {
EXPECT_EQ(3, mIndex++);
ASSERT_EQ(nullptr, state.computedState.localProjectionPathMask);
}
void onOvalOp(const OvalOp& op, const BakedOpState& state) override {
EXPECT_EQ(4, mIndex++);
ASSERT_NE(nullptr, state.computedState.localProjectionPathMask);
Matrix4 expected;
expected.loadTranslate(100 - scrollX, 100 - scrollY, 0);
EXPECT_EQ(expected, state.computedState.transform);
EXPECT_EQ(Rect(-85, -80, 295, 300),
Rect(state.computedState.localProjectionPathMask->getBounds()));
}
void onLayerOp(const LayerOp& op, const BakedOpState& state) override {
EXPECT_EQ(5, mIndex++);
ASSERT_EQ(nullptr, state.computedState.localProjectionPathMask);
}
};
auto receiverBackground = TestUtils::createNode(0, 0, 400, 400,
[](RenderProperties& properties, RecordingCanvas& canvas) {
properties.setProjectionReceiver(true);
// scroll doesn't apply to background, so undone via translationX/Y
// NOTE: translationX/Y only! no other transform properties may be set for a proj receiver!
properties.setTranslationX(scrollX);
properties.setTranslationY(scrollY);
canvas.drawRect(0, 0, 400, 400, SkPaint());
});
auto projectingRipple = TestUtils::createNode(0, 0, 200, 200,
[](RenderProperties& properties, RecordingCanvas& canvas) {
properties.setProjectBackwards(true);
properties.setClipToBounds(false);
canvas.drawOval(100, 100, 300, 300, SkPaint()); // drawn mostly out of layer bounds
});
auto child = TestUtils::createNode(100, 100, 300, 300,
[&projectingRipple](RenderProperties& properties, RecordingCanvas& canvas) {
properties.mutateLayerProperties().setType(LayerType::RenderLayer);
canvas.drawRenderNode(projectingRipple.get());
canvas.drawArc(0, 0, 200, 200, 0.0f, 280.0f, true, SkPaint());
});
auto parent = TestUtils::createNode(0, 0, 400, 400,
[&receiverBackground, &child](RenderProperties& properties, RecordingCanvas& canvas) {
// Set a rect outline for the projecting ripple to be masked against.
properties.mutableOutline().setRoundRect(10, 10, 390, 390, 0, 1.0f);
canvas.translate(-scrollX, -scrollY); // Apply scroll (note: bg undoes this internally)
canvas.drawRenderNode(receiverBackground.get());
canvas.drawRenderNode(child.get());
});
OffscreenBuffer** layerHandle = child->getLayerHandle();
// create RenderNode's layer here in same way prepareTree would, setting windowTransform
OffscreenBuffer layer(renderThread.renderState(), Caches::getInstance(), 200, 200);
Matrix4 windowTransform;
windowTransform.loadTranslate(100, 100, 0); // total transform of layer's origin
layer.setWindowTransform(windowTransform);
*layerHandle = &layer;
auto syncedList = TestUtils::createSyncedNodeList(parent);
LayerUpdateQueue layerUpdateQueue; // Note: enqueue damage post-sync, so bounds are valid
layerUpdateQueue.enqueueLayerWithDamage(child.get(), Rect(200, 200));
FrameBuilder frameBuilder(layerUpdateQueue, SkRect::MakeWH(400, 400), 400, 400,
syncedList, sLightGeometry, nullptr);
ProjectionHwLayerTestRenderer renderer;
frameBuilder.replayBakedOps<TestDispatcher>(renderer);
EXPECT_EQ(6, renderer.getIndex());
// clean up layer pointer, so we can safely destruct RenderNode
*layerHandle = nullptr;
}
RENDERTHREAD_TEST(FrameBuilder, projectionChildScroll) {
static const int scrollX = 500000;
static const int scrollY = 0;
class ProjectionChildScrollTestRenderer : public TestRendererBase {
public:
void onRectOp(const RectOp& op, const BakedOpState& state) override {
EXPECT_EQ(0, mIndex++);
EXPECT_TRUE(state.computedState.transform.isIdentity());
}
void onOvalOp(const OvalOp& op, const BakedOpState& state) override {
EXPECT_EQ(1, mIndex++);
ASSERT_NE(nullptr, state.computedState.clipState);
ASSERT_EQ(ClipMode::Rectangle, state.computedState.clipState->mode);
ASSERT_EQ(Rect(400, 400), state.computedState.clipState->rect);
EXPECT_TRUE(state.computedState.transform.isIdentity());
}
};
auto receiverBackground = TestUtils::createNode(0, 0, 400, 400,
[](RenderProperties& properties, RecordingCanvas& canvas) {
properties.setProjectionReceiver(true);
canvas.drawRect(0, 0, 400, 400, SkPaint());
});
auto projectingRipple = TestUtils::createNode(0, 0, 200, 200,
[](RenderProperties& properties, RecordingCanvas& canvas) {
// scroll doesn't apply to background, so undone via translationX/Y
// NOTE: translationX/Y only! no other transform properties may be set for a proj receiver!
properties.setTranslationX(scrollX);
properties.setTranslationY(scrollY);
properties.setProjectBackwards(true);
properties.setClipToBounds(false);
canvas.drawOval(0, 0, 200, 200, SkPaint());
});
auto child = TestUtils::createNode(0, 0, 400, 400,
[&projectingRipple](RenderProperties& properties, RecordingCanvas& canvas) {
// Record time clip will be ignored by projectee
canvas.clipRect(100, 100, 300, 300, SkRegion::kIntersect_Op);
canvas.translate(-scrollX, -scrollY); // Apply scroll (note: bg undoes this internally)
canvas.drawRenderNode(projectingRipple.get());
});
auto parent = TestUtils::createNode(0, 0, 400, 400,
[&receiverBackground, &child](RenderProperties& properties, RecordingCanvas& canvas) {
canvas.drawRenderNode(receiverBackground.get());
canvas.drawRenderNode(child.get());
});
FrameBuilder frameBuilder(sEmptyLayerUpdateQueue, SkRect::MakeWH(400, 400), 400, 400,
TestUtils::createSyncedNodeList(parent), sLightGeometry, nullptr);
ProjectionChildScrollTestRenderer renderer;
frameBuilder.replayBakedOps<TestDispatcher>(renderer);
EXPECT_EQ(2, renderer.getIndex());
}
// creates a 100x100 shadow casting node with provided translationZ
static sp<RenderNode> createWhiteRectShadowCaster(float translationZ) {
return TestUtils::createNode(0, 0, 100, 100,

View File

@@ -3,24 +3,32 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
<ScrollView
android:orientation="vertical"
android:translationX="50dp"
android:translationY="50dp"
android:elevation="30dp"
android:layout_width="200dp"
android:layout_height="200dp"
android:background="@drawable/round_rect_background">
<View
android:id="@+id/clickable1"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="?android:attr/selectableItemBackgroundBorderless"/>
<View
android:id="@+id/clickable2"
android:translationX="50dp"
android:translationY="10dp"
android:layout_width="150dp"
android:layout_height="100dp"
android:background="?android:attr/selectableItemBackgroundBorderless"/>
</FrameLayout>
<FrameLayout
android:layout_width="200dp"
android:layout_height="wrap_content">
<View
android:layout_width="200dp"
android:layout_height="2000dp"/>
<View
android:id="@+id/clickable1"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="?android:attr/selectableItemBackgroundBorderless"/>
<View
android:id="@+id/clickable2"
android:translationX="50dp"
android:translationY="10dp"
android:layout_width="150dp"
android:layout_height="100dp"
android:background="?android:attr/selectableItemBackgroundBorderless"/>
</FrameLayout>
</ScrollView>
</LinearLayout>