Add more shape drawing to new reorderer/renderer

bug:22480459

Add support for outsetting final bounds based on stroke.

Change-Id: I659318ccec51882bba1906ce3c7042288ce35c30
This commit is contained in:
Chris Craik
2015-12-07 17:08:25 -08:00
parent e0b2f2e270
commit 386aa03179
16 changed files with 584 additions and 135 deletions

View File

@@ -20,6 +20,7 @@
#include "Caches.h"
#include "Glop.h"
#include "GlopBuilder.h"
#include "PathTessellator.h"
#include "renderstate/OffscreenBufferPool.h"
#include "renderstate/RenderState.h"
#include "utils/GLUtils.h"
@@ -27,6 +28,7 @@
#include <algorithm>
#include <math.h>
#include <SkPaintDefaults.h>
namespace android {
namespace uirenderer {
@@ -206,6 +208,90 @@ void BakedOpDispatcher::onEndLayerOp(BakedOpRenderer&, const EndLayerOp&, const
LOG_ALWAYS_FATAL("unsupported operation");
}
namespace VertexBufferRenderFlags {
enum {
Offset = 0x1,
ShadowInterp = 0x2,
};
}
static void renderVertexBuffer(BakedOpRenderer& renderer, const BakedOpState& state,
const VertexBuffer& vertexBuffer, float translateX, float translateY,
const SkPaint& paint, int vertexBufferRenderFlags) {
if (CC_LIKELY(vertexBuffer.getVertexCount())) {
bool shadowInterp = vertexBufferRenderFlags & VertexBufferRenderFlags::ShadowInterp;
const int transformFlags = TransformFlags::OffsetByFudgeFactor;
Glop glop;
GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
.setRoundRectClipState(state.roundRectClipState)
.setMeshVertexBuffer(vertexBuffer, shadowInterp)
.setFillPaint(paint, state.alpha)
.setTransform(state.computedState.transform, transformFlags)
.setModelViewOffsetRect(translateX, translateY, vertexBuffer.getBounds())
.build();
renderer.renderGlop(state, glop);
}
}
static void renderConvexPath(BakedOpRenderer& renderer, const BakedOpState& state,
const SkPath& path, const SkPaint& paint) {
VertexBuffer vertexBuffer;
// TODO: try clipping large paths to viewport
PathTessellator::tessellatePath(path, &paint, state.computedState.transform, vertexBuffer);
renderVertexBuffer(renderer, state, vertexBuffer, 0.0f, 0.0f, paint, 0);
}
static void renderPathTexture(BakedOpRenderer& renderer, const BakedOpState& state,
PathTexture& texture, const RecordedOp& op) {
Rect dest(texture.width, texture.height);
dest.translate(texture.left + op.unmappedBounds.left - texture.offset,
texture.top + op.unmappedBounds.top - texture.offset);
Glop glop;
GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
.setRoundRectClipState(state.roundRectClipState)
.setMeshTexturedUnitQuad(nullptr)
.setFillPathTexturePaint(texture, *(op.paint), state.alpha)
.setTransform(state.computedState.transform, TransformFlags::None)
.setModelViewMapUnitToRect(dest)
.build();
renderer.renderGlop(state, glop);
}
SkRect getBoundsOfFill(const RecordedOp& op) {
SkRect bounds = op.unmappedBounds.toSkRect();
if (op.paint->getStyle() == SkPaint::kStrokeAndFill_Style) {
float outsetDistance = op.paint->getStrokeWidth() / 2;
bounds.outset(outsetDistance, outsetDistance);
}
return bounds;
}
void BakedOpDispatcher::onArcOp(BakedOpRenderer& renderer, const ArcOp& op, const BakedOpState& state) {
// TODO: support fills (accounting for concavity if useCenter && sweepAngle > 180)
if (op.paint->getStyle() != SkPaint::kStroke_Style
|| op.paint->getPathEffect() != nullptr
|| op.useCenter) {
PathTexture* texture = renderer.caches().pathCache.getArc(
op.unmappedBounds.getWidth(), op.unmappedBounds.getHeight(),
op.startAngle, op.sweepAngle, op.useCenter, op.paint);
const AutoTexture holder(texture);
if (CC_LIKELY(holder.texture)) {
renderPathTexture(renderer, state, *texture, op);
}
} else {
SkRect rect = getBoundsOfFill(op);
SkPath path;
if (op.useCenter) {
path.moveTo(rect.centerX(), rect.centerY());
}
path.arcTo(rect, op.startAngle, op.sweepAngle, !op.useCenter);
if (op.useCenter) {
path.close();
}
renderConvexPath(renderer, state, path, *(op.paint));
}
}
void BakedOpDispatcher::onBitmapOp(BakedOpRenderer& renderer, const BitmapOp& op, const BakedOpState& state) {
Texture* texture = renderer.getTexture(op.bitmap);
if (!texture) return;
@@ -225,43 +311,101 @@ void BakedOpDispatcher::onBitmapOp(BakedOpRenderer& renderer, const BitmapOp& op
}
void BakedOpDispatcher::onLinesOp(BakedOpRenderer& renderer, const LinesOp& op, const BakedOpState& state) {
LOG_ALWAYS_FATAL("todo");
VertexBuffer buffer;
PathTessellator::tessellateLines(op.points, op.floatCount, op.paint,
state.computedState.transform, buffer);
int displayFlags = op.paint->isAntiAlias() ? 0 : VertexBufferRenderFlags::Offset;
renderVertexBuffer(renderer, state, buffer, 0, 0, *(op.paint), displayFlags);
}
void BakedOpDispatcher::onOvalOp(BakedOpRenderer& renderer, const OvalOp& op, const BakedOpState& state) {
if (op.paint->getPathEffect() != nullptr) {
PathTexture* texture = renderer.caches().pathCache.getOval(
op.unmappedBounds.getWidth(), op.unmappedBounds.getHeight(), op.paint);
const AutoTexture holder(texture);
if (CC_LIKELY(holder.texture)) {
renderPathTexture(renderer, state, *texture, op);
}
} else {
SkPath path;
SkRect rect = getBoundsOfFill(op);
path.addOval(rect);
renderConvexPath(renderer, state, path, *(op.paint));
}
}
void BakedOpDispatcher::onPathOp(BakedOpRenderer& renderer, const PathOp& op, const BakedOpState& state) {
PathTexture* texture = renderer.caches().pathCache.get(op.path, op.paint);
const AutoTexture holder(texture);
if (CC_LIKELY(holder.texture)) {
renderPathTexture(renderer, state, *texture, op);
}
}
void BakedOpDispatcher::onPointsOp(BakedOpRenderer& renderer, const PointsOp& op, const BakedOpState& state) {
VertexBuffer buffer;
PathTessellator::tessellatePoints(op.points, op.floatCount, op.paint,
state.computedState.transform, buffer);
int displayFlags = op.paint->isAntiAlias() ? 0 : VertexBufferRenderFlags::Offset;
renderVertexBuffer(renderer, state, buffer, 0, 0, *(op.paint), displayFlags);
}
// See SkPaintDefaults.h
#define SkPaintDefaults_MiterLimit SkIntToScalar(4)
void BakedOpDispatcher::onRectOp(BakedOpRenderer& renderer, const RectOp& op, const BakedOpState& state) {
Glop glop;
GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
.setRoundRectClipState(state.roundRectClipState)
.setMeshUnitQuad()
.setFillPaint(*op.paint, state.alpha)
.setTransform(state.computedState.transform, TransformFlags::None)
.setModelViewMapUnitToRect(op.unmappedBounds)
.build();
renderer.renderGlop(state, glop);
if (op.paint->getStyle() != SkPaint::kFill_Style) {
// only fill + default miter is supported by drawConvexPath, since others must handle joins
static_assert(SkPaintDefaults_MiterLimit == 4.0f, "Miter limit has changed");
if (CC_UNLIKELY(op.paint->getPathEffect() != nullptr
|| op.paint->getStrokeJoin() != SkPaint::kMiter_Join
|| op.paint->getStrokeMiter() != SkPaintDefaults_MiterLimit)) {
PathTexture* texture = renderer.caches().pathCache.getRect(
op.unmappedBounds.getWidth(), op.unmappedBounds.getHeight(), op.paint);
const AutoTexture holder(texture);
if (CC_LIKELY(holder.texture)) {
renderPathTexture(renderer, state, *texture, op);
}
} else {
SkPath path;
path.addRect(getBoundsOfFill(op));
renderConvexPath(renderer, state, path, *(op.paint));
}
} else {
if (op.paint->isAntiAlias() && !state.computedState.transform.isSimple()) {
SkPath path;
path.addRect(op.unmappedBounds.toSkRect());
renderConvexPath(renderer, state, path, *(op.paint));
} else {
// render simple unit quad, no tessellation required
Glop glop;
GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
.setRoundRectClipState(state.roundRectClipState)
.setMeshUnitQuad()
.setFillPaint(*op.paint, state.alpha)
.setTransform(state.computedState.transform, TransformFlags::None)
.setModelViewMapUnitToRect(op.unmappedBounds)
.build();
renderer.renderGlop(state, glop);
}
}
}
namespace VertexBufferRenderFlags {
enum {
Offset = 0x1,
ShadowInterp = 0x2,
};
}
static void renderVertexBuffer(BakedOpRenderer& renderer, const BakedOpState& state,
const VertexBuffer& vertexBuffer, float translateX, float translateY,
SkPaint& paint, int vertexBufferRenderFlags) {
if (CC_LIKELY(vertexBuffer.getVertexCount())) {
bool shadowInterp = vertexBufferRenderFlags & VertexBufferRenderFlags::ShadowInterp;
const int transformFlags = TransformFlags::OffsetByFudgeFactor;
Glop glop;
GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
.setRoundRectClipState(state.roundRectClipState)
.setMeshVertexBuffer(vertexBuffer, shadowInterp)
.setFillPaint(paint, state.alpha)
.setTransform(state.computedState.transform, transformFlags)
.setModelViewOffsetRect(translateX, translateY, vertexBuffer.getBounds())
.build();
renderer.renderGlop(state, glop);
void BakedOpDispatcher::onRoundRectOp(BakedOpRenderer& renderer, const RoundRectOp& op, const BakedOpState& state) {
if (op.paint->getPathEffect() != nullptr) {
PathTexture* texture = renderer.caches().pathCache.getRoundRect(
op.unmappedBounds.getWidth(), op.unmappedBounds.getHeight(),
op.rx, op.ry, op.paint);
const AutoTexture holder(texture);
if (CC_LIKELY(holder.texture)) {
renderPathTexture(renderer, state, *texture, op);
}
} else {
const VertexBuffer* buffer = renderer.caches().tessellationCache.getRoundRect(
state.computedState.transform, *(op.paint),
op.unmappedBounds.getWidth(), op.unmappedBounds.getHeight(), op.rx, op.ry);
renderVertexBuffer(renderer, state, *buffer,
op.unmappedBounds.left, op.unmappedBounds.top, *(op.paint), 0);
}
}
@@ -323,8 +467,6 @@ void BakedOpDispatcher::onTextOp(BakedOpRenderer& renderer, const TextOp& op, co
void BakedOpDispatcher::onLayerOp(BakedOpRenderer& renderer, const LayerOp& op, const BakedOpState& state) {
OffscreenBuffer* buffer = *op.layerHandle;
// TODO: extend this to handle HW layers & paint properties which
// reside in node.properties().layerProperties()
float layerAlpha = op.alpha * state.alpha;
Glop glop;
GlopBuilder(renderer.renderState(), renderer.caches(), &glop)

View File

@@ -26,6 +26,10 @@ namespace uirenderer {
/**
* Provides all "onBitmapOp(...)" style static methods for every op type, which convert the
* RecordedOps and their state to Glops, and renders them with the provided BakedOpRenderer.
*
* onXXXOp methods must either render directly with the renderer, or call a static renderYYY
* method to render content. There should never be draw content rejection in BakedOpDispatcher -
* it must happen at a higher level (except in error-ish cases, like texture-too-big).
*/
class BakedOpDispatcher {
public:

View File

@@ -53,7 +53,7 @@ struct MergedBakedOpList {
class ResolvedRenderState {
public:
// TODO: remove the mapRects/matrix multiply when snapshot & recorded transforms are translates
ResolvedRenderState(const Snapshot& snapshot, const RecordedOp& recordedOp) {
ResolvedRenderState(const Snapshot& snapshot, const RecordedOp& recordedOp, bool expandForStroke) {
/* TODO: benchmark a fast path for translate-only matrices, such as:
if (CC_LIKELY(snapshot.transform->getType() == Matrix4::kTypeTranslate
&& recordedOp.localMatrix.getType() == Matrix4::kTypeTranslate)) {
@@ -83,7 +83,17 @@ public:
// resolvedClippedBounds = intersect(resolvedMatrix * opBounds, resolvedClipRect)
clippedBounds = recordedOp.unmappedBounds;
if (CC_UNLIKELY(expandForStroke)) {
// account for non-hairline stroke
clippedBounds.outset(recordedOp.paint->getStrokeWidth() * 0.5f);
}
transform.mapRect(clippedBounds);
if (CC_UNLIKELY(expandForStroke
&& (!transform.isPureTranslate() || recordedOp.paint->getStrokeWidth() < 1.0f))) {
// account for hairline stroke when stroke may be < 1 scaled pixel
// Non translate || strokeWidth < 1 is conservative, but will cover all cases
clippedBounds.outset(0.5f);
}
if (clipRect.left > clippedBounds.left) clipSideFlags |= OpClipSideFlags::Left;
if (clipRect.top > clippedBounds.top) clipSideFlags |= OpClipSideFlags::Top;
@@ -129,13 +139,36 @@ class BakedOpState {
public:
static BakedOpState* tryConstruct(LinearAllocator& allocator,
const Snapshot& snapshot, const RecordedOp& recordedOp) {
BakedOpState* bakedOp = new (allocator) BakedOpState(snapshot, recordedOp);
if (bakedOp->computedState.clippedBounds.isEmpty()) {
BakedOpState* bakedState = new (allocator) BakedOpState(snapshot, recordedOp, false);
if (bakedState->computedState.clippedBounds.isEmpty()) {
// bounds are empty, so op is rejected
allocator.rewindIfLastAlloc(bakedOp);
allocator.rewindIfLastAlloc(bakedState);
return nullptr;
}
return bakedOp;
return bakedState;
}
enum class StrokeBehavior {
// stroking is forced, regardless of style on paint
Forced,
// stroking is defined by style on paint
StyleDefined,
};
static BakedOpState* tryStrokeableOpConstruct(LinearAllocator& allocator,
const Snapshot& snapshot, const RecordedOp& recordedOp, StrokeBehavior strokeBehavior) {
bool expandForStroke = (strokeBehavior == StrokeBehavior::StyleDefined)
? (recordedOp.paint && recordedOp.paint->getStyle() != SkPaint::kFill_Style)
: true;
BakedOpState* bakedState = new (allocator) BakedOpState(
snapshot, recordedOp, expandForStroke);
if (bakedState->computedState.clippedBounds.isEmpty()) {
// bounds are empty, so op is rejected
allocator.rewindIfLastAlloc(bakedState);
return nullptr;
}
return bakedState;
}
static BakedOpState* tryShadowOpConstruct(LinearAllocator& allocator,
@@ -160,8 +193,8 @@ public:
const RecordedOp* op;
private:
BakedOpState(const Snapshot& snapshot, const RecordedOp& recordedOp)
: computedState(snapshot, recordedOp)
BakedOpState(const Snapshot& snapshot, const RecordedOp& recordedOp, bool expandForStroke)
: computedState(snapshot, recordedOp, expandForStroke)
, alpha(snapshot.alpha)
, roundRectClipState(snapshot.roundRectClipState)
, projectionPathMask(snapshot.projectionPathMask)

View File

@@ -113,10 +113,10 @@ public:
// Geometry
virtual void drawPoint(float x, float y, const SkPaint& paint) = 0;
virtual void drawPoints(const float* points, int count, const SkPaint& paint) = 0;
virtual void drawPoints(const float* points, int floatCount, const SkPaint& paint) = 0;
virtual void drawLine(float startX, float startY, float stopX, float stopY,
const SkPaint& paint) = 0;
virtual void drawLines(const float* points, int count, const SkPaint& paint) = 0;
virtual void drawLines(const float* points, int floatCount, const SkPaint& paint) = 0;
virtual void drawRect(float left, float top, float right, float bottom,
const SkPaint& paint) = 0;
virtual void drawRegion(const SkRegion& region, const SkPaint& paint) = 0;

View File

@@ -708,12 +708,36 @@ void OpReorderer::onRenderNodeOp(const RenderNodeOp& op) {
}
}
static batchid_t tessellatedBatchId(const SkPaint& paint) {
/**
* Defers an unmergeable, strokeable op, accounting correctly
* for paint's style on the bounds being computed.
*/
void OpReorderer::onStrokeableOp(const RecordedOp& op, batchid_t batchId,
BakedOpState::StrokeBehavior strokeBehavior) {
// Note: here we account for stroke when baking the op
BakedOpState* bakedState = BakedOpState::tryStrokeableOpConstruct(
mAllocator, *mCanvasState.currentSnapshot(), op, strokeBehavior);
if (!bakedState) return; // quick rejected
currentLayer().deferUnmergeableOp(mAllocator, bakedState, batchId);
}
/**
* Returns batch id for tessellatable shapes, based on paint. Checks to see if path effect/AA will
* be used, since they trigger significantly different rendering paths.
*
* Note: not used for lines/points, since they don't currently support path effects.
*/
static batchid_t tessBatchId(const RecordedOp& op) {
const SkPaint& paint = *(op.paint);
return paint.getPathEffect()
? OpBatchType::AlphaMaskTexture
: (paint.isAntiAlias() ? OpBatchType::AlphaVertices : OpBatchType::Vertices);
}
void OpReorderer::onArcOp(const ArcOp& op) {
onStrokeableOp(op, tessBatchId(op));
}
void OpReorderer::onBitmapOp(const BitmapOp& op) {
BakedOpState* bakedState = tryBakeOpState(op);
if (!bakedState) return; // quick rejected
@@ -734,15 +758,29 @@ void OpReorderer::onBitmapOp(const BitmapOp& op) {
}
void OpReorderer::onLinesOp(const LinesOp& op) {
BakedOpState* bakedState = tryBakeOpState(op);
if (!bakedState) return; // quick rejected
currentLayer().deferUnmergeableOp(mAllocator, bakedState, tessellatedBatchId(*op.paint));
batchid_t batch = op.paint->isAntiAlias() ? OpBatchType::AlphaVertices : OpBatchType::Vertices;
onStrokeableOp(op, batch, BakedOpState::StrokeBehavior::Forced);
}
void OpReorderer::onOvalOp(const OvalOp& op) {
onStrokeableOp(op, tessBatchId(op));
}
void OpReorderer::onPathOp(const PathOp& op) {
onStrokeableOp(op, OpBatchType::Bitmap);
}
void OpReorderer::onPointsOp(const PointsOp& op) {
batchid_t batch = op.paint->isAntiAlias() ? OpBatchType::AlphaVertices : OpBatchType::Vertices;
onStrokeableOp(op, batch, BakedOpState::StrokeBehavior::Forced);
}
void OpReorderer::onRectOp(const RectOp& op) {
BakedOpState* bakedState = tryBakeOpState(op);
if (!bakedState) return; // quick rejected
currentLayer().deferUnmergeableOp(mAllocator, bakedState, tessellatedBatchId(*op.paint));
onStrokeableOp(op, tessBatchId(op));
}
void OpReorderer::onRoundRectOp(const RoundRectOp& op) {
onStrokeableOp(op, tessBatchId(op));
}
void OpReorderer::onSimpleRectsOp(const SimpleRectsOp& op) {

View File

@@ -245,6 +245,10 @@ private:
mFrameAllocatedPaths.emplace_back(new SkPath);
return mFrameAllocatedPaths.back().get();
}
void onStrokeableOp(const RecordedOp& op, batchid_t batchId,
BakedOpState::StrokeBehavior strokeBehavior = BakedOpState::StrokeBehavior::StyleDefined);
/**
* Declares all OpReorderer::onXXXXOp() methods for every RecordedOp type.
*

View File

@@ -40,6 +40,7 @@
#include <SkCanvas.h>
#include <SkColor.h>
#include <SkPaintDefaults.h>
#include <SkPathOps.h>
#include <SkShader.h>
#include <SkTypeface.h>
@@ -1908,9 +1909,6 @@ void OpenGLRenderer::drawArc(float left, float top, float right, float bottom,
drawConvexPath(path, p);
}
// See SkPaintDefaults.h
#define SkPaintDefaults_MiterLimit SkIntToScalar(4)
void OpenGLRenderer::drawRect(float left, float top, float right, float bottom,
const SkPaint* p) {
if (mState.currentlyIgnored()
@@ -1921,6 +1919,7 @@ void OpenGLRenderer::drawRect(float left, float top, float right, float bottom,
if (p->getStyle() != SkPaint::kFill_Style) {
// only fill style is supported by drawConvexPath, since others have to handle joins
static_assert(SkPaintDefaults_MiterLimit == 4.0f, "Miter limit has changed");
if (p->getPathEffect() != nullptr || p->getStrokeJoin() != SkPaint::kMiter_Join ||
p->getStrokeMiter() != SkPaintDefaults_MiterLimit) {
mCaches.textureState().activateTexture(0);

View File

@@ -43,10 +43,15 @@ struct Vertex;
* This serves as the authoritative list of ops, used for generating ID enum, and ID based LUTs.
*/
#define MAP_OPS_BASED_ON_MERGEABILITY(U_OP_FN, M_OP_FN) \
U_OP_FN(ArcOp) \
M_OP_FN(BitmapOp) \
U_OP_FN(LinesOp) \
U_OP_FN(OvalOp) \
U_OP_FN(PathOp) \
U_OP_FN(PointsOp) \
U_OP_FN(RectOp) \
U_OP_FN(RenderNodeOp) \
U_OP_FN(RoundRectOp) \
U_OP_FN(ShadowOp) \
U_OP_FN(SimpleRectsOp) \
M_OP_FN(TextOp) \
@@ -74,7 +79,7 @@ namespace RecordedOpId {
Count,
};
}
static_assert(RecordedOpId::BitmapOp == 0,
static_assert(RecordedOpId::ArcOp == 0,
"First index must be zero for LUTs to work");
#define BASE_PARAMS const Rect& unmappedBounds, const Matrix4& localMatrix, const Rect& localClipRect, const SkPaint* paint
@@ -86,7 +91,7 @@ struct RecordedOp {
/* ID from RecordedOpId - generally used for jumping into function tables */
const int opId;
/* bounds in *local* space, without accounting for DisplayList transformation */
/* bounds in *local* space, without accounting for DisplayList transformation, or stroke */
const Rect unmappedBounds;
/* transform in recording space (vs DisplayList origin) */
@@ -128,6 +133,17 @@ struct RenderNodeOp : RecordedOp {
// Standard Ops
////////////////////////////////////////////////////////////////////////////////////////////////////
struct ArcOp : RecordedOp {
ArcOp(BASE_PARAMS, float startAngle, float sweepAngle, bool useCenter)
: SUPER(ArcOp)
, startAngle(startAngle)
, sweepAngle(sweepAngle)
, useCenter(useCenter) {}
const float startAngle;
const float sweepAngle;
const bool useCenter;
};
struct BitmapOp : RecordedOp {
BitmapOp(BASE_PARAMS, const SkBitmap* bitmap)
: SUPER(BitmapOp)
@@ -145,11 +161,41 @@ struct LinesOp : RecordedOp {
const int floatCount;
};
struct OvalOp : RecordedOp {
OvalOp(BASE_PARAMS)
: SUPER(OvalOp) {}
};
struct PathOp : RecordedOp {
PathOp(BASE_PARAMS, const SkPath* path)
: SUPER(PathOp)
, path(path) {}
const SkPath* path;
};
struct PointsOp : RecordedOp {
PointsOp(BASE_PARAMS, const float* points, const int floatCount)
: SUPER(PointsOp)
, points(points)
, floatCount(floatCount) {}
const float* points;
const int floatCount;
};
struct RectOp : RecordedOp {
RectOp(BASE_PARAMS)
: SUPER(RectOp) {}
};
struct RoundRectOp : RecordedOp {
RoundRectOp(BASE_PARAMS, float rx, float ry)
: SUPER(RoundRectOp)
, rx(rx)
, ry(ry) {}
const float rx;
const float ry;
};
/**
* Real-time, dynamic-lit shadow.
*

View File

@@ -237,26 +237,32 @@ void RecordingCanvas::drawPaint(const SkPaint& paint) {
refPaint(&paint)));
}
static Rect calcBoundsOfPoints(const float* points, int floatCount) {
Rect unmappedBounds(points[0], points[1], points[0], points[1]);
for (int i = 2; i < floatCount; i += 2) {
unmappedBounds.expandToCover(points[i], points[i + 1]);
}
return unmappedBounds;
}
// Geometry
void RecordingCanvas::drawPoints(const float* points, int count, const SkPaint& paint) {
LOG_ALWAYS_FATAL("TODO!");
void RecordingCanvas::drawPoints(const float* points, int floatCount, const SkPaint& paint) {
if (floatCount < 2) return;
floatCount &= ~0x1; // round down to nearest two
addOp(new (alloc()) PointsOp(
calcBoundsOfPoints(points, floatCount),
*mState.currentSnapshot()->transform,
mState.getRenderTargetClipBounds(),
refPaint(&paint), refBuffer<float>(points, floatCount), floatCount));
}
void RecordingCanvas::drawLines(const float* points, int floatCount, const SkPaint& paint) {
if (floatCount < 4) return;
floatCount &= ~0x3; // round down to nearest four
Rect unmappedBounds(points[0], points[1], points[0], points[1]);
for (int i = 2; i < floatCount; i += 2) {
unmappedBounds.expandToCover(points[i], points[i + 1]);
}
// since anything AA stroke with less than 1.0 pixel width is drawn with an alpha-reduced
// 1.0 stroke, treat 1.0 as minimum.
unmappedBounds.outset(std::max(paint.getStrokeWidth(), 1.0f) * 0.5f);
addOp(new (alloc()) LinesOp(
unmappedBounds,
calcBoundsOfPoints(points, floatCount),
*mState.currentSnapshot()->transform,
mState.getRenderTargetClipBounds(),
refPaint(&paint), refBuffer<float>(points, floatCount), floatCount));
@@ -330,20 +336,42 @@ void RecordingCanvas::drawRegion(const SkRegion& region, const SkPaint& paint) {
}
void RecordingCanvas::drawRoundRect(float left, float top, float right, float bottom,
float rx, float ry, const SkPaint& paint) {
LOG_ALWAYS_FATAL("TODO!");
addOp(new (alloc()) RoundRectOp(
Rect(left, top, right, bottom),
*(mState.currentSnapshot()->transform),
mState.getRenderTargetClipBounds(),
refPaint(&paint), rx, ry));
}
void RecordingCanvas::drawCircle(float x, float y, float radius, const SkPaint& paint) {
LOG_ALWAYS_FATAL("TODO!");
if (radius <= 0) return;
drawOval(x - radius, y - radius, x + radius, y + radius, paint);
}
void RecordingCanvas::drawOval(float left, float top, float right, float bottom, const SkPaint& paint) {
LOG_ALWAYS_FATAL("TODO!");
addOp(new (alloc()) OvalOp(
Rect(left, top, right, bottom),
*(mState.currentSnapshot()->transform),
mState.getRenderTargetClipBounds(),
refPaint(&paint)));
}
void RecordingCanvas::drawArc(float left, float top, float right, float bottom,
float startAngle, float sweepAngle, bool useCenter, const SkPaint& paint) {
LOG_ALWAYS_FATAL("TODO!");
float startAngle, float sweepAngle, bool useCenter, const SkPaint& paint) {
addOp(new (alloc()) ArcOp(
Rect(left, top, right, bottom),
*(mState.currentSnapshot()->transform),
mState.getRenderTargetClipBounds(),
refPaint(&paint),
startAngle, sweepAngle, useCenter));
}
void RecordingCanvas::drawPath(const SkPath& path, const SkPaint& paint) {
LOG_ALWAYS_FATAL("TODO!");
addOp(new (alloc()) PathOp(
Rect(path.getBounds()),
*(mState.currentSnapshot()->transform),
mState.getRenderTargetClipBounds(),
refPaint(&paint), refPath(&path)));
}
// Bitmap-based
@@ -375,6 +403,7 @@ void RecordingCanvas::drawBitmap(const SkBitmap& bitmap, const SkMatrix& matrix,
restore();
}
}
void RecordingCanvas::drawBitmap(const SkBitmap& bitmap, float srcLeft, float srcTop,
float srcRight, float srcBottom, float dstLeft, float dstTop,
float dstRight, float dstBottom, const SkPaint* paint) {
@@ -392,10 +421,12 @@ void RecordingCanvas::drawBitmap(const SkBitmap& bitmap, float srcLeft, float sr
LOG_ALWAYS_FATAL("TODO!");
}
}
void RecordingCanvas::drawBitmapMesh(const SkBitmap& bitmap, int meshWidth, int meshHeight,
const float* vertices, const int* colors, const SkPaint* paint) {
LOG_ALWAYS_FATAL("TODO!");
}
void RecordingCanvas::drawNinePatch(const SkBitmap& bitmap, const android::Res_png_9patch& chunk,
float dstLeft, float dstTop, float dstRight, float dstBottom,
const SkPaint* paint) {

View File

@@ -140,13 +140,13 @@ public:
float points[2] = { x, y };
drawPoints(points, 2, paint);
}
virtual void drawPoints(const float* points, int count, const SkPaint& paint) override;
virtual void drawPoints(const float* points, int floatCount, const SkPaint& paint) override;
virtual void drawLine(float startX, float startY, float stopX, float stopY,
const SkPaint& paint) override {
float points[4] = { startX, startY, stopX, stopY };
drawLines(points, 4, paint);
}
virtual void drawLines(const float* points, int count, const SkPaint& paint) override;
virtual void drawLines(const float* points, int floatCount, const SkPaint& paint) override;
virtual void drawRect(float left, float top, float right, float bottom, const SkPaint& paint) override;
virtual void drawRegion(const SkRegion& region, const SkPaint& paint) override;
virtual void drawRoundRect(float left, float top, float right, float bottom,

View File

@@ -120,16 +120,16 @@ private:
class AutoTexture {
public:
AutoTexture(const Texture* texture): mTexture(texture) { }
AutoTexture(const Texture* texture)
: texture(texture) {}
~AutoTexture() {
if (mTexture && mTexture->cleanup) {
mTexture->deleteTexture();
delete mTexture;
if (texture && texture->cleanup) {
texture->deleteTexture();
delete texture;
}
}
private:
const Texture* mTexture;
const Texture *const texture;
}; // class AutoTexture
}; // namespace uirenderer

View File

@@ -23,41 +23,130 @@
namespace android {
namespace uirenderer {
TEST(ResolvedRenderState, resolution) {
Matrix4 identity;
identity.loadIdentity();
TEST(ResolvedRenderState, construct) {
Matrix4 translate10x20;
translate10x20.loadTranslate(10, 20, 0);
SkPaint paint;
RectOp recordedOp(Rect(30, 40, 100, 200), translate10x20, Rect(0, 0, 100, 200), &paint);
RectOp recordedOp(Rect(30, 40, 100, 200), translate10x20, Rect(100, 200), &paint);
{
// recorded with transform, no parent transform
auto parentSnapshot = TestUtils::makeSnapshot(identity, Rect(0, 0, 100, 200));
ResolvedRenderState state(*parentSnapshot, recordedOp);
auto parentSnapshot = TestUtils::makeSnapshot(Matrix4::identity(), Rect(100, 200));
ResolvedRenderState state(*parentSnapshot, recordedOp, false);
EXPECT_MATRIX_APPROX_EQ(state.transform, translate10x20);
EXPECT_EQ(state.clipRect, Rect(0, 0, 100, 200));
EXPECT_EQ(state.clippedBounds, Rect(40, 60, 100, 200)); // translated and also clipped
EXPECT_EQ(Rect(0, 0, 100, 200), state.clipRect);
EXPECT_EQ(Rect(40, 60, 100, 200), state.clippedBounds); // translated and also clipped
EXPECT_EQ(OpClipSideFlags::Right | OpClipSideFlags::Bottom, state.clipSideFlags);
}
{
// recorded with transform and parent transform
auto parentSnapshot = TestUtils::makeSnapshot(translate10x20, Rect(0, 0, 100, 200));
ResolvedRenderState state(*parentSnapshot, recordedOp);
auto parentSnapshot = TestUtils::makeSnapshot(translate10x20, Rect(100, 200));
ResolvedRenderState state(*parentSnapshot, recordedOp, false);
Matrix4 expectedTranslate;
expectedTranslate.loadTranslate(20, 40, 0);
EXPECT_MATRIX_APPROX_EQ(state.transform, expectedTranslate);
EXPECT_MATRIX_APPROX_EQ(expectedTranslate, state.transform);
// intersection of parent & transformed child clip
EXPECT_EQ(state.clipRect, Rect(10, 20, 100, 200));
EXPECT_EQ(Rect(10, 20, 100, 200), state.clipRect);
// translated and also clipped
EXPECT_EQ(state.clippedBounds, Rect(50, 80, 100, 200));
EXPECT_EQ(Rect(50, 80, 100, 200), state.clippedBounds);
EXPECT_EQ(OpClipSideFlags::Right | OpClipSideFlags::Bottom, state.clipSideFlags);
}
}
TEST(BakedOpState, constructAndReject) {
const float HAIRLINE = 0.0f;
// Note: bounds will be conservative, but not precise for non-hairline
// - use approx bounds checks for these
const float SEMI_HAIRLINE = 0.3f;
struct StrokeTestCase {
float scale;
float strokeWidth;
const std::function<void(const ResolvedRenderState&)> validator;
};
const static StrokeTestCase sStrokeTestCases[] = {
{
1, HAIRLINE, [](const ResolvedRenderState& state) {
EXPECT_EQ(Rect(49.5f, 49.5f, 150.5f, 150.5f), state.clippedBounds);
}
},
{
1, SEMI_HAIRLINE, [](const ResolvedRenderState& state) {
EXPECT_TRUE(state.clippedBounds.contains(49.5f, 49.5f, 150.5f, 150.5f));
EXPECT_TRUE(Rect(49, 49, 151, 151).contains(state.clippedBounds));
}
},
{
1, 20, [](const ResolvedRenderState& state) {
EXPECT_EQ(Rect(40, 40, 160, 160), state.clippedBounds);
}
},
// 3x3 scale:
{
3, HAIRLINE, [](const ResolvedRenderState& state) {
EXPECT_EQ(Rect(149.5f, 149.5f, 200, 200), state.clippedBounds);
EXPECT_EQ(OpClipSideFlags::Right | OpClipSideFlags::Bottom, state.clipSideFlags);
}
},
{
3, SEMI_HAIRLINE, [](const ResolvedRenderState& state) {
EXPECT_TRUE(state.clippedBounds.contains(149.5f, 149.5f, 200, 200));
EXPECT_TRUE(Rect(149, 149, 200, 200).contains(state.clippedBounds));
}
},
{
3, 20, [](const ResolvedRenderState& state) {
EXPECT_TRUE(state.clippedBounds.contains(120, 120, 200, 200));
EXPECT_TRUE(Rect(119, 119, 200, 200).contains(state.clippedBounds));
}
},
// 0.5f x 0.5f scale
{
0.5f, HAIRLINE, [](const ResolvedRenderState& state) {
EXPECT_EQ(Rect(24.5f, 24.5f, 75.5f, 75.5f), state.clippedBounds);
}
},
{
0.5f, SEMI_HAIRLINE, [](const ResolvedRenderState& state) {
EXPECT_TRUE(state.clippedBounds.contains(24.5f, 24.5f, 75.5f, 75.5f));
EXPECT_TRUE(Rect(24, 24, 76, 76).contains(state.clippedBounds));
}
},
{
0.5f, 20, [](const ResolvedRenderState& state) {
EXPECT_TRUE(state.clippedBounds.contains(19.5f, 19.5f, 80.5f, 80.5f));
EXPECT_TRUE(Rect(19, 19, 81, 81).contains(state.clippedBounds));
}
}
};
TEST(ResolvedRenderState, construct_expandForStroke) {
// Loop over table of test cases and verify different combinations of stroke width and transform
for (auto&& testCase : sStrokeTestCases) {
SkPaint strokedPaint;
strokedPaint.setAntiAlias(true);
strokedPaint.setStyle(SkPaint::kStroke_Style);
strokedPaint.setStrokeWidth(testCase.strokeWidth);
RectOp recordedOp(Rect(50, 50, 150, 150),
Matrix4::identity(), Rect(200, 200), &strokedPaint);
Matrix4 snapshotMatrix;
snapshotMatrix.loadScale(testCase.scale, testCase.scale, 1);
auto parentSnapshot = TestUtils::makeSnapshot(snapshotMatrix, Rect(200, 200));
ResolvedRenderState state(*parentSnapshot, recordedOp, true);
testCase.validator(state);
}
}
TEST(BakedOpState, tryConstruct) {
LinearAllocator allocator;
Matrix4 translate100x0;
@@ -65,41 +154,85 @@ TEST(BakedOpState, constructAndReject) {
SkPaint paint;
{
RectOp rejectOp(Rect(30, 40, 100, 200), translate100x0, Rect(0, 0, 100, 200), &paint);
auto snapshot = TestUtils::makeSnapshot(Matrix4::identity(), Rect(0, 0, 100, 200));
BakedOpState* bakedOp = BakedOpState::tryConstruct(allocator, *snapshot, rejectOp);
RectOp rejectOp(Rect(30, 40, 100, 200), translate100x0, Rect(100, 200), &paint);
auto snapshot = TestUtils::makeSnapshot(Matrix4::identity(), Rect(100, 200));
BakedOpState* bakedState = BakedOpState::tryConstruct(allocator, *snapshot, rejectOp);
EXPECT_EQ(bakedOp, nullptr); // rejected by clip, so not constructed
EXPECT_LE(allocator.usedSize(), 8u); // no significant allocation space used for rejected op
EXPECT_EQ(nullptr, bakedState); // rejected by clip, so not constructed
EXPECT_GT(8u, allocator.usedSize()); // no significant allocation space used for rejected op
}
{
RectOp successOp(Rect(30, 40, 100, 200), Matrix4::identity(), Rect(0, 0, 100, 200), &paint);
auto snapshot = TestUtils::makeSnapshot(Matrix4::identity(), Rect(0, 0, 100, 200));
BakedOpState* bakedOp = BakedOpState::tryConstruct(allocator, *snapshot, successOp);
RectOp successOp(Rect(30, 40, 100, 200), Matrix4::identity(), Rect(100, 200), &paint);
auto snapshot = TestUtils::makeSnapshot(Matrix4::identity(), Rect(100, 200));
BakedOpState* bakedState = BakedOpState::tryConstruct(allocator, *snapshot, successOp);
EXPECT_NE(bakedOp, nullptr); // NOT rejected by clip, so will be constructed
EXPECT_GT(allocator.usedSize(), 64u); // relatively large alloc for non-rejected op
EXPECT_NE(nullptr, bakedState); // NOT rejected by clip, so will be constructed
EXPECT_LE(64u, allocator.usedSize()); // relatively large alloc for non-rejected op
}
}
TEST(BakedOpState, oplessConstructAndReject) {
TEST(BakedOpState, tryShadowOpConstruct) {
LinearAllocator allocator;
{
auto snapshot = TestUtils::makeSnapshot(Matrix4::identity(), Rect(0, 0, 0, 0)); // empty
BakedOpState* bakedOp = BakedOpState::tryShadowOpConstruct(allocator, *snapshot, (ShadowOp*)0x1234);
auto snapshot = TestUtils::makeSnapshot(Matrix4::identity(), Rect()); // Note: empty clip
BakedOpState* bakedState = BakedOpState::tryShadowOpConstruct(allocator, *snapshot, (ShadowOp*)0x1234);
EXPECT_EQ(bakedOp, nullptr); // rejected by clip, so not constructed
EXPECT_LE(allocator.usedSize(), 8u); // no significant allocation space used for rejected op
EXPECT_EQ(nullptr, bakedState); // rejected by clip, so not constructed
EXPECT_GT(8u, allocator.usedSize()); // no significant allocation space used for rejected op
}
{
auto snapshot = TestUtils::makeSnapshot(Matrix4::identity(), Rect(0, 0, 100, 200));
BakedOpState* bakedOp = BakedOpState::tryShadowOpConstruct(allocator, *snapshot, (ShadowOp*)0x1234);
auto snapshot = TestUtils::makeSnapshot(Matrix4::identity(), Rect(100, 200));
BakedOpState* bakedState = BakedOpState::tryShadowOpConstruct(allocator, *snapshot, (ShadowOp*)0x1234);
EXPECT_NE(bakedOp, nullptr); // NOT rejected by clip, so will be constructed
EXPECT_GT(allocator.usedSize(), 64u); // relatively large alloc for non-rejected op
EXPECT_EQ((ShadowOp*)0x1234, bakedOp->op);
ASSERT_NE(nullptr, bakedState); // NOT rejected by clip, so will be constructed
EXPECT_LE(64u, allocator.usedSize()); // relatively large alloc for non-rejected op
}
}
TEST(BakedOpState, tryStrokeableOpConstruct) {
LinearAllocator allocator;
{
// check regular rejection
SkPaint paint;
paint.setStyle(SkPaint::kStrokeAndFill_Style);
paint.setStrokeWidth(0.0f);
RectOp rejectOp(Rect(0, 0, 100, 200), Matrix4::identity(), Rect(100, 200), &paint);
auto snapshot = TestUtils::makeSnapshot(Matrix4::identity(), Rect()); // Note: empty clip
auto bakedState = BakedOpState::tryStrokeableOpConstruct(allocator, *snapshot, rejectOp,
BakedOpState::StrokeBehavior::StyleDefined);
EXPECT_EQ(nullptr, bakedState);
EXPECT_GT(8u, allocator.usedSize()); // no significant allocation space used for rejected op
}
{
// check simple unscaled expansion
SkPaint paint;
paint.setStyle(SkPaint::kStrokeAndFill_Style);
paint.setStrokeWidth(10.0f);
RectOp rejectOp(Rect(50, 50, 150, 150), Matrix4::identity(), Rect(200, 200), &paint);
auto snapshot = TestUtils::makeSnapshot(Matrix4::identity(), Rect(200, 200));
auto bakedState = BakedOpState::tryStrokeableOpConstruct(allocator, *snapshot, rejectOp,
BakedOpState::StrokeBehavior::StyleDefined);
ASSERT_NE(nullptr, bakedState);
EXPECT_EQ(Rect(45, 45, 155, 155), bakedState->computedState.clippedBounds);
EXPECT_EQ(0, bakedState->computedState.clipSideFlags);
}
{
// check simple unscaled expansion, and fill style with stroke forced
SkPaint paint;
paint.setStyle(SkPaint::kFill_Style);
paint.setStrokeWidth(10.0f);
RectOp rejectOp(Rect(50, 50, 150, 150), Matrix4::identity(), Rect(200, 200), &paint);
auto snapshot = TestUtils::makeSnapshot(Matrix4::identity(), Rect(200, 200));
auto bakedState = BakedOpState::tryStrokeableOpConstruct(allocator, *snapshot, rejectOp,
BakedOpState::StrokeBehavior::Forced);
ASSERT_NE(nullptr, bakedState);
EXPECT_EQ(Rect(45, 45, 155, 155), bakedState->computedState.clippedBounds);
EXPECT_EQ(0, bakedState->computedState.clipSideFlags);
}
}
}
} // namespace uirenderer
} // namespace android

View File

@@ -92,12 +92,10 @@ TEST(ClipArea, basics) {
TEST(ClipArea, paths) {
ClipArea area(createClipArea());
Matrix4 transform;
transform.loadIdentity();
SkPath path;
SkScalar r = 100;
path.addCircle(r, r, r);
area.clipPathWithTransform(path, &transform, SkRegion::kIntersect_Op);
area.clipPathWithTransform(path, &Matrix4::identity(), SkRegion::kIntersect_Op);
EXPECT_FALSE(area.isEmpty());
EXPECT_FALSE(area.isSimple());
EXPECT_FALSE(area.isRectangleList());
@@ -116,11 +114,10 @@ TEST(ClipArea, replaceNegative) {
ClipArea area(createClipArea());
area.setClip(0, 0, 100, 100);
Matrix4 transform;
transform.loadIdentity();
Rect expected(-50, -50, 50, 50);
area.clipRectWithTransform(expected, &transform, SkRegion::kReplace_Op);
area.clipRectWithTransform(expected, &Matrix4::identity(), SkRegion::kReplace_Op);
EXPECT_EQ(expected, area.getClipRect());
}
}
}

View File

@@ -31,13 +31,11 @@ using namespace android::uirenderer;
// as the output.
TEST(DamageAccumulator, identity) {
DamageAccumulator da;
Matrix4 identity;
SkRect curDirty;
identity.loadIdentity();
da.pushTransform(&identity);
da.pushTransform(&Matrix4::identity());
da.dirty(50, 50, 100, 100);
{
da.pushTransform(&identity);
da.pushTransform(&Matrix4::identity());
da.peekAtDirty(&curDirty);
ASSERT_EQ(SkRect(), curDirty);
da.popTransform();
@@ -68,15 +66,13 @@ TEST(DamageAccumulator, translate) {
// Test that dirty rectangles are being unioned across "siblings
TEST(DamageAccumulator, union) {
DamageAccumulator da;
Matrix4 identity;
SkRect curDirty;
identity.loadIdentity();
da.pushTransform(&identity);
da.pushTransform(&Matrix4::identity());
{
da.pushTransform(&identity);
da.pushTransform(&Matrix4::identity());
da.dirty(50, 50, 100, 100);
da.popTransform();
da.pushTransform(&identity);
da.pushTransform(&Matrix4::identity());
da.dirty(150, 50, 200, 125);
da.popTransform();
}

View File

@@ -144,6 +144,32 @@ TEST(OpReorderer, simple) {
EXPECT_EQ(4, renderer.getIndex()); // 2 ops + start + end
}
TEST(OpReorderer, simpleStroke) {
class SimpleStrokeTestRenderer : public TestRendererBase {
public:
void onPointsOp(const PointsOp& op, const BakedOpState& state) override {
EXPECT_EQ(0, mIndex++);
// even though initial bounds are empty...
EXPECT_TRUE(op.unmappedBounds.isEmpty())
<< "initial bounds should be empty, since they're unstroked";
EXPECT_EQ(Rect(45, 45, 55, 55), state.computedState.clippedBounds)
<< "final bounds should account for stroke";
}
};
auto node = TestUtils::createNode(0, 0, 100, 200,
[](RenderProperties& props, RecordingCanvas& canvas) {
SkPaint strokedPaint;
strokedPaint.setStrokeWidth(10);
canvas.drawPoint(50, 50, strokedPaint);
});
OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(100, 200), 100, 200,
createSyncedNodeList(node), sLightCenter);
SimpleStrokeTestRenderer renderer;
reorderer.replayBakedOps<TestDispatcher>(renderer);
EXPECT_EQ(1, renderer.getIndex());
}
TEST(OpReorderer, simpleRejection) {
auto node = TestUtils::createNode(0, 0, 200, 200,
[](RenderProperties& props, RecordingCanvas& canvas) {

View File

@@ -44,7 +44,7 @@ TEST(RecordingCanvas, emptyPlayback) {
TEST(RecordingCanvas, drawLines) {
auto dl = TestUtils::createDisplayList<RecordingCanvas>(100, 200, [](RecordingCanvas& canvas) {
SkPaint paint;
paint.setStrokeWidth(20);
paint.setStrokeWidth(20); // doesn't affect recorded bounds - would be resolved at bake time
float points[] = { 0, 0, 20, 10, 30, 40, 90 }; // NB: only 1 valid line
canvas.drawLines(&points[0], 7, paint);
});
@@ -54,8 +54,8 @@ TEST(RecordingCanvas, drawLines) {
ASSERT_EQ(RecordedOpId::LinesOp, op->opId);
EXPECT_EQ(4, ((LinesOp*)op)->floatCount)
<< "float count must be rounded down to closest multiple of 4";
EXPECT_EQ(Rect(-10, -10, 30, 20), op->unmappedBounds)
<< "unmapped bounds must be size of line, outset by 1/2 stroke width";
EXPECT_EQ(Rect(0, 0, 20, 10), op->unmappedBounds)
<< "unmapped bounds must be size of line, and not outset for stroke width";
}
TEST(RecordingCanvas, drawRect) {