Add cap tessellation support

bug:7117155
bug:8114304

Currently used for lines (with and without AA) and arcs with useCenter=false

Also removes 0.375, 0.375 offset for AA lines

Change-Id: Ic8ace418739344db1e2814edf65253fe7448b0b0
This commit is contained in:
Chris Craik
2012-12-10 17:56:27 -08:00
parent c93c6aa555
commit 65cd612fac
11 changed files with 1097 additions and 1161 deletions

View File

@@ -21,10 +21,10 @@ ifeq ($(USE_OPENGL_RENDERER),true)
LayerRenderer.cpp \
Matrix.cpp \
OpenGLRenderer.cpp \
PathRenderer.cpp \
Patch.cpp \
PatchCache.cpp \
PathCache.cpp \
PathTessellator.cpp \
Program.cpp \
ProgramCache.cpp \
ResourceCache.cpp \

View File

@@ -66,7 +66,6 @@ static const TextureVertex gMeshVertices[] = {
static const GLsizei gMeshStride = sizeof(TextureVertex);
static const GLsizei gVertexStride = sizeof(Vertex);
static const GLsizei gAlphaVertexStride = sizeof(AlphaVertex);
static const GLsizei gAAVertexStride = sizeof(AAVertex);
static const GLsizei gMeshTextureOffset = 2 * sizeof(float);
static const GLsizei gVertexAlphaOffset = 2 * sizeof(float);
static const GLsizei gVertexAAWidthOffset = 2 * sizeof(float);

View File

@@ -33,7 +33,7 @@
#include "OpenGLRenderer.h"
#include "DisplayListRenderer.h"
#include "PathRenderer.h"
#include "PathTessellator.h"
#include "Properties.h"
#include "Vector.h"
@@ -1469,10 +1469,6 @@ void OpenGLRenderer::setupDrawAA() {
mDescription.isAA = true;
}
void OpenGLRenderer::setupDrawVertexShape() {
mDescription.isVertexShape = true;
}
void OpenGLRenderer::setupDrawPoint(float pointSize) {
mDescription.isPoint = true;
mDescription.pointSize = pointSize;
@@ -1688,41 +1684,6 @@ void OpenGLRenderer::setupDrawVertices(GLvoid* vertices) {
mCaches.unbindIndicesBuffer();
}
/**
* Sets up the shader to draw an AA line. We draw AA lines with quads, where there is an
* outer boundary that fades out to 0. The variables set in the shader define the proportion of
* the width and length of the primitive occupied by the AA region. The vtxWidth and vtxLength
* attributes (one per vertex) are values from zero to one that tells the fragment
* shader where the fragment is in relation to the line width/length overall; these values are
* then used to compute the proper color, based on whether the fragment lies in the fading AA
* region of the line.
* Note that we only pass down the width values in this setup function. The length coordinates
* are set up for each individual segment.
*/
void OpenGLRenderer::setupDrawAALine(GLvoid* vertices, GLvoid* widthCoords,
GLvoid* lengthCoords, float boundaryWidthProportion, int& widthSlot, int& lengthSlot) {
bool force = mCaches.unbindMeshBuffer();
mCaches.bindPositionVertexPointer(force, vertices, gAAVertexStride);
mCaches.resetTexCoordsVertexPointer();
mCaches.unbindIndicesBuffer();
widthSlot = mCaches.currentProgram->getAttrib("vtxWidth");
glEnableVertexAttribArray(widthSlot);
glVertexAttribPointer(widthSlot, 1, GL_FLOAT, GL_FALSE, gAAVertexStride, widthCoords);
lengthSlot = mCaches.currentProgram->getAttrib("vtxLength");
glEnableVertexAttribArray(lengthSlot);
glVertexAttribPointer(lengthSlot, 1, GL_FLOAT, GL_FALSE, gAAVertexStride, lengthCoords);
int boundaryWidthSlot = mCaches.currentProgram->getUniform("boundaryWidth");
glUniform1f(boundaryWidthSlot, boundaryWidthProportion);
}
void OpenGLRenderer::finishDrawAALine(const int widthSlot, const int lengthSlot) {
glDisableVertexAttribArray(widthSlot);
glDisableVertexAttribArray(lengthSlot);
}
void OpenGLRenderer::finishDrawTexture() {
}
@@ -2083,39 +2044,26 @@ status_t OpenGLRenderer::drawPatch(SkBitmap* bitmap, const int32_t* xDivs, const
return DrawGlInfo::kStatusDrew;
}
/**
* Renders a convex path via tessellation. For AA paths, this function uses a similar approach to
* that of AA lines in the drawLines() function. We expand the convex path by a half pixel in
* screen space in all directions. However, instead of using a fragment shader to compute the
* translucency of the color from its position, we simply use a varying parameter to define how far
* a given pixel is from the edge. For non-AA paths, the expansion and alpha varying are not used.
*
* Doesn't yet support joins, caps, or path effects.
*/
void OpenGLRenderer::drawConvexPath(const SkPath& path, SkPaint* paint) {
status_t OpenGLRenderer::drawVertexBuffer(const VertexBuffer& vertexBuffer, SkPaint* paint,
bool useOffset) {
if (!vertexBuffer.getSize()) {
// no vertices to draw
return DrawGlInfo::kStatusDone;
}
int color = paint->getColor();
SkXfermode::Mode mode = getXfermode(paint->getXfermode());
bool isAA = paint->isAntiAlias();
VertexBuffer vertexBuffer;
// TODO: try clipping large paths to viewport
PathRenderer::convexPathVertices(path, paint, mSnapshot->transform, vertexBuffer);
if (!vertexBuffer.getSize()) {
// no vertices to draw
return;
}
setupDraw();
setupDrawNoTexture();
if (isAA) setupDrawAA();
setupDrawVertexShape();
setupDrawColor(color, ((color >> 24) & 0xFF) * mSnapshot->alpha);
setupDrawColorFilter();
setupDrawShader();
setupDrawBlending(isAA, mode);
setupDrawProgram();
setupDrawModelViewIdentity();
setupDrawModelViewIdentity(useOffset);
setupDrawColorUniforms();
setupDrawColorFilterUniforms();
setupDrawShaderIdentityUniforms();
@@ -2136,286 +2084,59 @@ void OpenGLRenderer::drawConvexPath(const SkPath& path, SkPaint* paint) {
glVertexAttribPointer(alphaSlot, 1, GL_FLOAT, GL_FALSE, gAlphaVertexStride, alphaCoords);
}
SkRect bounds = PathRenderer::computePathBounds(path, paint);
dirtyLayer(bounds.fLeft, bounds.fTop, bounds.fRight, bounds.fBottom, *mSnapshot->transform);
glDrawArrays(GL_TRIANGLE_STRIP, 0, vertexBuffer.getSize());
if (isAA) {
glDisableVertexAttribArray(alphaSlot);
}
return DrawGlInfo::kStatusDrew;
}
/**
* We draw lines as quads (tristrips). Using GL_LINES can be difficult because the rasterization
* rules for those lines produces some unexpected results, and may vary between hardware devices.
* The basics of lines-as-quads is easy; we simply find the normal to the line and position the
* corners of the quads on either side of each line endpoint, separated by the strokeWidth
* of the line. Hairlines are more involved because we need to account for transform scaling
* to end up with a one-pixel-wide line in screen space..
* Anti-aliased lines add another factor to the approach. We use a specialized fragment shader
* in combination with values that we calculate and pass down in this method. The basic approach
* is that the quad we create contains both the core line area plus a bounding area in which
* the translucent/AA pixels are drawn. The values we calculate tell the shader what
* proportion of the width and the length of a given segment is represented by the boundary
* region. The quad ends up being exactly .5 pixel larger in all directions than the non-AA quad.
* The bounding region is actually 1 pixel wide on all sides (half pixel on the outside, half pixel
* on the inside). This ends up giving the result we want, with pixels that are completely
* 'inside' the line area being filled opaquely and the other pixels being filled according to
* how far into the boundary region they are, which is determined by shader interpolation.
* Renders a convex path via tessellation. For AA paths, this function uses a similar approach to
* that of AA lines in the drawLines() function. We expand the convex path by a half pixel in
* screen space in all directions. However, instead of using a fragment shader to compute the
* translucency of the color from its position, we simply use a varying parameter to define how far
* a given pixel is from the edge. For non-AA paths, the expansion and alpha varying are not used.
*
* Doesn't yet support joins, caps, or path effects.
*/
status_t OpenGLRenderer::drawConvexPath(const SkPath& path, SkPaint* paint) {
VertexBuffer vertexBuffer;
// TODO: try clipping large paths to viewport
PathTessellator::tessellatePath(path, paint, mSnapshot->transform, vertexBuffer);
SkRect bounds = path.getBounds();
PathTessellator::expandBoundsForStroke(bounds, paint, false);
dirtyLayer(bounds.fLeft, bounds.fTop, bounds.fRight, bounds.fBottom, *mSnapshot->transform);
return drawVertexBuffer(vertexBuffer, paint);
}
/**
* We create tristrips for the lines much like shape stroke tessellation, using a per-vertex alpha
* and additional geometry for defining an alpha slope perimeter.
*
* Using GL_LINES can be difficult because the rasterization rules for those lines produces some
* unexpected results, and may vary between hardware devices. Previously we used a varying-base
* in-shader alpha region, but found it to be taxing on some GPUs.
*
* TODO: try using a fixed input buffer for non-capped lines as in text rendering. this may reduce
* memory transfer by removing need for degenerate vertices.
*/
status_t OpenGLRenderer::drawLines(float* points, int count, SkPaint* paint) {
if (mSnapshot->isIgnored()) return DrawGlInfo::kStatusDone;
if (mSnapshot->isIgnored() || count < 4) return DrawGlInfo::kStatusDone;
const bool isAA = paint->isAntiAlias();
// We use half the stroke width here because we're going to position the quad
// corner vertices half of the width away from the line endpoints
float halfStrokeWidth = paint->getStrokeWidth() * 0.5f;
// A stroke width of 0 has a special meaning in Skia:
// it draws a line 1 px wide regardless of current transform
bool isHairLine = paint->getStrokeWidth() == 0.0f;
count &= ~0x3; // round down to nearest four
float inverseScaleX = 1.0f;
float inverseScaleY = 1.0f;
bool scaled = false;
VertexBuffer buffer;
SkRect bounds;
PathTessellator::tessellateLines(points, count, paint, mSnapshot->transform, bounds, buffer);
dirtyLayer(bounds.fLeft, bounds.fTop, bounds.fRight, bounds.fBottom, *mSnapshot->transform);
int alpha;
SkXfermode::Mode mode;
int generatedVerticesCount = 0;
int verticesCount = count;
if (count > 4) {
// Polyline: account for extra vertices needed for continuous tri-strip
verticesCount += (count - 4);
}
if (isHairLine || isAA) {
// The quad that we use for AA and hairlines needs to account for scaling. For hairlines
// the line on the screen should always be one pixel wide regardless of scale. For
// AA lines, we only want one pixel of translucent boundary around the quad.
if (CC_UNLIKELY(!mSnapshot->transform->isPureTranslate())) {
Matrix4 *mat = mSnapshot->transform;
float m00 = mat->data[Matrix4::kScaleX];
float m01 = mat->data[Matrix4::kSkewY];
float m10 = mat->data[Matrix4::kSkewX];
float m11 = mat->data[Matrix4::kScaleY];
float scaleX = sqrtf(m00 * m00 + m01 * m01);
float scaleY = sqrtf(m10 * m10 + m11 * m11);
inverseScaleX = (scaleX != 0) ? (inverseScaleX / scaleX) : 0;
inverseScaleY = (scaleY != 0) ? (inverseScaleY / scaleY) : 0;
if (inverseScaleX != 1.0f || inverseScaleY != 1.0f) {
scaled = true;
}
}
}
getAlphaAndMode(paint, &alpha, &mode);
mCaches.enableScissor();
setupDraw();
setupDrawNoTexture();
if (isAA) {
setupDrawAA();
}
setupDrawColor(paint->getColor(), alpha);
setupDrawColorFilter();
setupDrawShader();
setupDrawBlending(isAA, mode);
setupDrawProgram();
setupDrawModelViewIdentity(true);
setupDrawColorUniforms();
setupDrawColorFilterUniforms();
setupDrawShaderIdentityUniforms();
if (isHairLine) {
// Set a real stroke width to be used in quad construction
halfStrokeWidth = isAA? 1 : .5;
} else if (isAA && !scaled) {
// Expand boundary to enable AA calculations on the quad border
halfStrokeWidth += .5f;
}
int widthSlot;
int lengthSlot;
Vertex lines[verticesCount];
Vertex* vertices = &lines[0];
AAVertex wLines[verticesCount];
AAVertex* aaVertices = &wLines[0];
if (CC_UNLIKELY(!isAA)) {
setupDrawVertices(vertices);
} else {
void* widthCoords = ((GLbyte*) aaVertices) + gVertexAAWidthOffset;
void* lengthCoords = ((GLbyte*) aaVertices) + gVertexAALengthOffset;
// innerProportion is the ratio of the inner (non-AA) part of the line to the total
// AA stroke width (the base stroke width expanded by a half pixel on either side).
// This value is used in the fragment shader to determine how to fill fragments.
// We will need to calculate the actual width proportion on each segment for
// scaled non-hairlines, since the boundary proportion may differ per-axis when scaled.
float boundaryWidthProportion = .5 - 1 / (2 * halfStrokeWidth);
setupDrawAALine((void*) aaVertices, widthCoords, lengthCoords,
boundaryWidthProportion, widthSlot, lengthSlot);
}
AAVertex* prevAAVertex = NULL;
Vertex* prevVertex = NULL;
int boundaryLengthSlot = -1;
int boundaryWidthSlot = -1;
for (int i = 0; i < count; i += 4) {
// a = start point, b = end point
vec2 a(points[i], points[i + 1]);
vec2 b(points[i + 2], points[i + 3]);
float length = 0;
float boundaryLengthProportion = 0;
float boundaryWidthProportion = 0;
// Find the normal to the line
vec2 n = (b - a).copyNormalized() * halfStrokeWidth;
float x = n.x;
n.x = -n.y;
n.y = x;
if (isHairLine) {
if (isAA) {
float wideningFactor;
if (fabs(n.x) >= fabs(n.y)) {
wideningFactor = fabs(1.0f / n.x);
} else {
wideningFactor = fabs(1.0f / n.y);
}
n *= wideningFactor;
}
if (scaled) {
n.x *= inverseScaleX;
n.y *= inverseScaleY;
}
} else if (scaled) {
// Extend n by .5 pixel on each side, post-transform
vec2 extendedN = n.copyNormalized();
extendedN /= 2;
extendedN.x *= inverseScaleX;
extendedN.y *= inverseScaleY;
float extendedNLength = extendedN.length();
// We need to set this value on the shader prior to drawing
boundaryWidthProportion = .5 - extendedNLength / (halfStrokeWidth + extendedNLength);
n += extendedN;
}
// aa lines expand the endpoint vertices to encompass the AA boundary
if (isAA) {
vec2 abVector = (b - a);
length = abVector.length();
abVector.normalize();
if (scaled) {
abVector.x *= inverseScaleX;
abVector.y *= inverseScaleY;
float abLength = abVector.length();
boundaryLengthProportion = .5 - abLength / (length + abLength);
} else {
boundaryLengthProportion = .5 - .5 / (length + 1);
}
abVector /= 2;
a -= abVector;
b += abVector;
}
// Four corners of the rectangle defining a thick line
vec2 p1 = a - n;
vec2 p2 = a + n;
vec2 p3 = b + n;
vec2 p4 = b - n;
const float left = fmin(p1.x, fmin(p2.x, fmin(p3.x, p4.x)));
const float right = fmax(p1.x, fmax(p2.x, fmax(p3.x, p4.x)));
const float top = fmin(p1.y, fmin(p2.y, fmin(p3.y, p4.y)));
const float bottom = fmax(p1.y, fmax(p2.y, fmax(p3.y, p4.y)));
if (!quickRejectNoScissor(left, top, right, bottom)) {
if (!isAA) {
if (prevVertex != NULL) {
// Issue two repeat vertices to create degenerate triangles to bridge
// between the previous line and the new one. This is necessary because
// we are creating a single triangle_strip which will contain
// potentially discontinuous line segments.
Vertex::set(vertices++, prevVertex->position[0], prevVertex->position[1]);
Vertex::set(vertices++, p1.x, p1.y);
generatedVerticesCount += 2;
}
Vertex::set(vertices++, p1.x, p1.y);
Vertex::set(vertices++, p2.x, p2.y);
Vertex::set(vertices++, p4.x, p4.y);
Vertex::set(vertices++, p3.x, p3.y);
prevVertex = vertices - 1;
generatedVerticesCount += 4;
} else {
if (!isHairLine && scaled) {
// Must set width proportions per-segment for scaled non-hairlines to use the
// correct AA boundary dimensions
if (boundaryWidthSlot < 0) {
boundaryWidthSlot =
mCaches.currentProgram->getUniform("boundaryWidth");
}
glUniform1f(boundaryWidthSlot, boundaryWidthProportion);
}
if (boundaryLengthSlot < 0) {
boundaryLengthSlot = mCaches.currentProgram->getUniform("boundaryLength");
}
glUniform1f(boundaryLengthSlot, boundaryLengthProportion);
if (prevAAVertex != NULL) {
// Issue two repeat vertices to create degenerate triangles to bridge
// between the previous line and the new one. This is necessary because
// we are creating a single triangle_strip which will contain
// potentially discontinuous line segments.
AAVertex::set(aaVertices++,prevAAVertex->position[0],
prevAAVertex->position[1], prevAAVertex->width, prevAAVertex->length);
AAVertex::set(aaVertices++, p4.x, p4.y, 1, 1);
generatedVerticesCount += 2;
}
AAVertex::set(aaVertices++, p4.x, p4.y, 1, 1);
AAVertex::set(aaVertices++, p1.x, p1.y, 1, 0);
AAVertex::set(aaVertices++, p3.x, p3.y, 0, 1);
AAVertex::set(aaVertices++, p2.x, p2.y, 0, 0);
prevAAVertex = aaVertices - 1;
generatedVerticesCount += 4;
}
dirtyLayer(a.x == b.x ? left - 1 : left, a.y == b.y ? top - 1 : top,
a.x == b.x ? right: right, a.y == b.y ? bottom: bottom,
*mSnapshot->transform);
}
}
if (generatedVerticesCount > 0) {
glDrawArrays(GL_TRIANGLE_STRIP, 0, generatedVerticesCount);
}
if (isAA) {
finishDrawAALine(widthSlot, lengthSlot);
}
return DrawGlInfo::kStatusDrew;
bool useOffset = !paint->isAntiAlias();
return drawVertexBuffer(buffer, paint, useOffset);
}
status_t OpenGLRenderer::drawPoints(float* points, int count, SkPaint* paint) {
@@ -2526,9 +2247,7 @@ status_t OpenGLRenderer::drawRoundRect(float left, float top, float right, float
ry += outset;
}
path.addRoundRect(rect, rx, ry);
drawConvexPath(path, p);
return DrawGlInfo::kStatusDrew;
return drawConvexPath(path, p);
}
status_t OpenGLRenderer::drawCircle(float x, float y, float radius, SkPaint* p) {
@@ -2548,9 +2267,7 @@ status_t OpenGLRenderer::drawCircle(float x, float y, float radius, SkPaint* p)
} else {
path.addCircle(x, y, radius);
}
drawConvexPath(path, p);
return DrawGlInfo::kStatusDrew;
return drawConvexPath(path, p);
}
status_t OpenGLRenderer::drawOval(float left, float top, float right, float bottom,
@@ -2571,9 +2288,7 @@ status_t OpenGLRenderer::drawOval(float left, float top, float right, float bott
rect.outset(p->getStrokeWidth() / 2, p->getStrokeWidth() / 2);
}
path.addOval(rect);
drawConvexPath(path, p);
return DrawGlInfo::kStatusDrew;
return drawConvexPath(path, p);
}
status_t OpenGLRenderer::drawArc(float left, float top, float right, float bottom,
@@ -2587,8 +2302,7 @@ status_t OpenGLRenderer::drawArc(float left, float top, float right, float botto
}
// TODO: support fills (accounting for concavity if useCenter && sweepAngle > 180)
if (p->getStyle() != SkPaint::kStroke_Style || p->getPathEffect() != 0 ||
p->getStrokeCap() != SkPaint::kButt_Cap || useCenter) {
if (p->getStyle() != SkPaint::kStroke_Style || p->getPathEffect() != 0 || useCenter) {
mCaches.activeTexture(0);
const PathTexture* texture = mCaches.arcShapeCache.getArc(right - left, bottom - top,
startAngle, sweepAngle, useCenter, p);
@@ -2608,9 +2322,7 @@ status_t OpenGLRenderer::drawArc(float left, float top, float right, float botto
if (useCenter) {
path.close();
}
drawConvexPath(path, p);
return DrawGlInfo::kStatusDrew;
return drawConvexPath(path, p);
}
// See SkPaintDefaults.h
@@ -2637,20 +2349,17 @@ status_t OpenGLRenderer::drawRect(float left, float top, float right, float bott
rect.outset(p->getStrokeWidth() / 2, p->getStrokeWidth() / 2);
}
path.addRect(rect);
drawConvexPath(path, p);
return DrawGlInfo::kStatusDrew;
return drawConvexPath(path, p);
}
if (p->isAntiAlias() && !mSnapshot->transform->isSimple()) {
SkPath path;
path.addRect(left, top, right, bottom);
drawConvexPath(path, p);
return drawConvexPath(path, p);
} else {
drawColorRect(left, top, right, bottom, p->getColor(), getXfermode(p->getXfermode()));
return DrawGlInfo::kStatusDrew;
}
return DrawGlInfo::kStatusDrew;
}
void OpenGLRenderer::drawTextShadow(SkPaint* paint, const char* text, int bytesCount, int count,

View File

@@ -53,6 +53,7 @@ namespace uirenderer {
///////////////////////////////////////////////////////////////////////////////
class DisplayList;
class VertexBuffer;
/**
* OpenGL renderer used to draw accelerated 2D graphics. The API is a
@@ -581,13 +582,23 @@ private:
*/
void drawAlphaBitmap(Texture* texture, float left, float top, SkPaint* paint);
/**
* Renders a strip of polygons with the specified paint, used for tessellated geometry.
*
* @param vertexBuffer The VertexBuffer to be drawn
* @param paint The paint to render with
* @param useOffset Offset the vertexBuffer (used in drawing non-AA lines)
*/
status_t drawVertexBuffer(const VertexBuffer& vertexBuffer, SkPaint* paint,
bool useOffset = false);
/**
* Renders the convex hull defined by the specified path as a strip of polygons.
*
* @param path The hull of the path to draw
* @param paint The paint to render with
*/
void drawConvexPath(const SkPath& path, SkPaint* paint);
status_t drawConvexPath(const SkPath& path, SkPaint* paint);
/**
* Draws a textured rectangle with the specified texture. The specified coordinates
@@ -754,7 +765,6 @@ private:
void setupDrawWithExternalTexture();
void setupDrawNoTexture();
void setupDrawAA();
void setupDrawVertexShape();
void setupDrawPoint(float pointSize);
void setupDrawColor(int color, int alpha);
void setupDrawColor(float r, float g, float b, float a);
@@ -788,9 +798,6 @@ private:
void setupDrawMesh(GLvoid* vertices, GLvoid* texCoords = NULL, GLuint vbo = 0);
void setupDrawMeshIndices(GLvoid* vertices, GLvoid* texCoords);
void setupDrawVertices(GLvoid* vertices);
void setupDrawAALine(GLvoid* vertices, GLvoid* distanceCoords, GLvoid* lengthCoords,
float strokeWidth, int& widthSlot, int& lengthSlot);
void finishDrawAALine(const int widthSlot, const int lengthSlot);
void finishDrawTexture();
void accountForClear(SkXfermode::Mode mode);

View File

@@ -1,720 +0,0 @@
/*
* Copyright (C) 2012 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.
*/
#define LOG_TAG "PathRenderer"
#define LOG_NDEBUG 1
#define ATRACE_TAG ATRACE_TAG_GRAPHICS
#define VERTEX_DEBUG 0
#include <SkPath.h>
#include <SkPaint.h>
#include <stdlib.h>
#include <stdint.h>
#include <sys/types.h>
#include <utils/Log.h>
#include <utils/Trace.h>
#include "PathRenderer.h"
#include "Matrix.h"
#include "Vector.h"
#include "Vertex.h"
namespace android {
namespace uirenderer {
#define THRESHOLD 0.5f
SkRect PathRenderer::computePathBounds(const SkPath& path, const SkPaint* paint) {
SkRect bounds = path.getBounds();
if (paint->getStyle() != SkPaint::kFill_Style) {
float outset = paint->getStrokeWidth() * 0.5f;
bounds.outset(outset, outset);
}
return bounds;
}
void computeInverseScales(const mat4 *transform, float &inverseScaleX, float& inverseScaleY) {
if (CC_UNLIKELY(!transform->isPureTranslate())) {
float m00 = transform->data[Matrix4::kScaleX];
float m01 = transform->data[Matrix4::kSkewY];
float m10 = transform->data[Matrix4::kSkewX];
float m11 = transform->data[Matrix4::kScaleY];
float scaleX = sqrt(m00 * m00 + m01 * m01);
float scaleY = sqrt(m10 * m10 + m11 * m11);
inverseScaleX = (scaleX != 0) ? (1.0f / scaleX) : 1.0f;
inverseScaleY = (scaleY != 0) ? (1.0f / scaleY) : 1.0f;
} else {
inverseScaleX = 1.0f;
inverseScaleY = 1.0f;
}
}
inline void copyVertex(Vertex* destPtr, const Vertex* srcPtr) {
Vertex::set(destPtr, srcPtr->position[0], srcPtr->position[1]);
}
inline void copyAlphaVertex(AlphaVertex* destPtr, const AlphaVertex* srcPtr) {
AlphaVertex::set(destPtr, srcPtr->position[0], srcPtr->position[1], srcPtr->alpha);
}
/**
* Produces a pseudo-normal for a vertex, given the normals of the two incoming lines. If the offset
* from each vertex in a perimeter is calculated, the resultant lines connecting the offset vertices
* will be offset by 1.0
*
* Note that we can't add and normalize the two vectors, that would result in a rectangle having an
* offset of (sqrt(2)/2, sqrt(2)/2) at each corner, instead of (1, 1)
*
* NOTE: assumes angles between normals 90 degrees or less
*/
inline vec2 totalOffsetFromNormals(const vec2& normalA, const vec2& normalB) {
return (normalA + normalB) / (1 + fabs(normalA.dot(normalB)));
}
inline void scaleOffsetForStrokeWidth(vec2& offset, float halfStrokeWidth,
float inverseScaleX, float inverseScaleY) {
if (halfStrokeWidth == 0.0f) {
// hairline - compensate for scale
offset.x *= 0.5f * inverseScaleX;
offset.y *= 0.5f * inverseScaleY;
} else {
offset *= halfStrokeWidth;
}
}
void getFillVerticesFromPerimeter(const Vector<Vertex>& perimeter, VertexBuffer& vertexBuffer) {
Vertex* buffer = vertexBuffer.alloc<Vertex>(perimeter.size());
int currentIndex = 0;
// zig zag between all previous points on the inside of the hull to create a
// triangle strip that fills the hull
int srcAindex = 0;
int srcBindex = perimeter.size() - 1;
while (srcAindex <= srcBindex) {
copyVertex(&buffer[currentIndex++], &perimeter[srcAindex]);
if (srcAindex == srcBindex) break;
copyVertex(&buffer[currentIndex++], &perimeter[srcBindex]);
srcAindex++;
srcBindex--;
}
}
void getStrokeVerticesFromPerimeter(const Vector<Vertex>& perimeter, float halfStrokeWidth,
VertexBuffer& vertexBuffer, float inverseScaleX, float inverseScaleY) {
Vertex* buffer = vertexBuffer.alloc<Vertex>(perimeter.size() * 2 + 2);
int currentIndex = 0;
const Vertex* last = &(perimeter[perimeter.size() - 1]);
const Vertex* current = &(perimeter[0]);
vec2 lastNormal(current->position[1] - last->position[1],
last->position[0] - current->position[0]);
lastNormal.normalize();
for (unsigned int i = 0; i < perimeter.size(); i++) {
const Vertex* next = &(perimeter[i + 1 >= perimeter.size() ? 0 : i + 1]);
vec2 nextNormal(next->position[1] - current->position[1],
current->position[0] - next->position[0]);
nextNormal.normalize();
vec2 totalOffset = totalOffsetFromNormals(lastNormal, nextNormal);
scaleOffsetForStrokeWidth(totalOffset, halfStrokeWidth, inverseScaleX, inverseScaleY);
Vertex::set(&buffer[currentIndex++],
current->position[0] + totalOffset.x,
current->position[1] + totalOffset.y);
Vertex::set(&buffer[currentIndex++],
current->position[0] - totalOffset.x,
current->position[1] - totalOffset.y);
last = current;
current = next;
lastNormal = nextNormal;
}
// wrap around to beginning
copyVertex(&buffer[currentIndex++], &buffer[0]);
copyVertex(&buffer[currentIndex++], &buffer[1]);
}
void getStrokeVerticesFromUnclosedVertices(const Vector<Vertex>& vertices, float halfStrokeWidth,
VertexBuffer& vertexBuffer, float inverseScaleX, float inverseScaleY) {
Vertex* buffer = vertexBuffer.alloc<Vertex>(vertices.size() * 2);
int currentIndex = 0;
const Vertex* current = &(vertices[0]);
vec2 lastNormal;
for (unsigned int i = 0; i < vertices.size() - 1; i++) {
const Vertex* next = &(vertices[i + 1]);
vec2 nextNormal(next->position[1] - current->position[1],
current->position[0] - next->position[0]);
nextNormal.normalize();
vec2 totalOffset;
if (i == 0) {
totalOffset = nextNormal;
} else {
totalOffset = totalOffsetFromNormals(lastNormal, nextNormal);
}
scaleOffsetForStrokeWidth(totalOffset, halfStrokeWidth, inverseScaleX, inverseScaleY);
Vertex::set(&buffer[currentIndex++],
current->position[0] + totalOffset.x,
current->position[1] + totalOffset.y);
Vertex::set(&buffer[currentIndex++],
current->position[0] - totalOffset.x,
current->position[1] - totalOffset.y);
current = next;
lastNormal = nextNormal;
}
vec2 totalOffset = lastNormal;
scaleOffsetForStrokeWidth(totalOffset, halfStrokeWidth, inverseScaleX, inverseScaleY);
Vertex::set(&buffer[currentIndex++],
current->position[0] + totalOffset.x,
current->position[1] + totalOffset.y);
Vertex::set(&buffer[currentIndex++],
current->position[0] - totalOffset.x,
current->position[1] - totalOffset.y);
#if VERTEX_DEBUG
for (unsigned int i = 0; i < vertexBuffer.getSize(); i++) {
ALOGD("point at %f %f", buffer[i].position[0], buffer[i].position[1]);
}
#endif
}
void getFillVerticesFromPerimeterAA(const Vector<Vertex>& perimeter, VertexBuffer& vertexBuffer,
float inverseScaleX, float inverseScaleY) {
AlphaVertex* buffer = vertexBuffer.alloc<AlphaVertex>(perimeter.size() * 3 + 2);
// generate alpha points - fill Alpha vertex gaps in between each point with
// alpha 0 vertex, offset by a scaled normal.
int currentIndex = 0;
const Vertex* last = &(perimeter[perimeter.size() - 1]);
const Vertex* current = &(perimeter[0]);
vec2 lastNormal(current->position[1] - last->position[1],
last->position[0] - current->position[0]);
lastNormal.normalize();
for (unsigned int i = 0; i < perimeter.size(); i++) {
const Vertex* next = &(perimeter[i + 1 >= perimeter.size() ? 0 : i + 1]);
vec2 nextNormal(next->position[1] - current->position[1],
current->position[0] - next->position[0]);
nextNormal.normalize();
// AA point offset from original point is that point's normal, such that each side is offset
// by .5 pixels
vec2 totalOffset = totalOffsetFromNormals(lastNormal, nextNormal);
totalOffset.x *= 0.5f * inverseScaleX;
totalOffset.y *= 0.5f * inverseScaleY;
AlphaVertex::set(&buffer[currentIndex++],
current->position[0] + totalOffset.x,
current->position[1] + totalOffset.y,
0.0f);
AlphaVertex::set(&buffer[currentIndex++],
current->position[0] - totalOffset.x,
current->position[1] - totalOffset.y,
1.0f);
last = current;
current = next;
lastNormal = nextNormal;
}
// wrap around to beginning
copyAlphaVertex(&buffer[currentIndex++], &buffer[0]);
copyAlphaVertex(&buffer[currentIndex++], &buffer[1]);
// zig zag between all previous points on the inside of the hull to create a
// triangle strip that fills the hull, repeating the first inner point to
// create degenerate tris to start inside path
int srcAindex = 0;
int srcBindex = perimeter.size() - 1;
while (srcAindex <= srcBindex) {
copyAlphaVertex(&buffer[currentIndex++], &buffer[srcAindex * 2 + 1]);
if (srcAindex == srcBindex) break;
copyAlphaVertex(&buffer[currentIndex++], &buffer[srcBindex * 2 + 1]);
srcAindex++;
srcBindex--;
}
#if VERTEX_DEBUG
for (unsigned int i = 0; i < vertexBuffer.getSize(); i++) {
ALOGD("point at %f %f, alpha %f", buffer[i].position[0], buffer[i].position[1], buffer[i].alpha);
}
#endif
}
void getStrokeVerticesFromUnclosedVerticesAA(const Vector<Vertex>& vertices, float halfStrokeWidth,
VertexBuffer& vertexBuffer, float inverseScaleX, float inverseScaleY) {
AlphaVertex* buffer = vertexBuffer.alloc<AlphaVertex>(6 * vertices.size() + 2);
// avoid lines smaller than hairline since they break triangle based sampling. instead reducing
// alpha value (TODO: support different X/Y scale)
float maxAlpha = 1.0f;
if (halfStrokeWidth != 0 && inverseScaleX == inverseScaleY &&
halfStrokeWidth * inverseScaleX < 0.5f) {
maxAlpha *= (2 * halfStrokeWidth) / inverseScaleX;
halfStrokeWidth = 0.0f;
}
// there is no outer/inner here, using them for consistency with below approach
int offset = 2 * (vertices.size() - 2);
int currentAAOuterIndex = 2;
int currentAAInnerIndex = 2 * offset + 5; // reversed
int currentStrokeIndex = currentAAInnerIndex + 7;
const Vertex* last = &(vertices[0]);
const Vertex* current = &(vertices[1]);
vec2 lastNormal(current->position[1] - last->position[1],
last->position[0] - current->position[0]);
lastNormal.normalize();
{
// start cap
vec2 totalOffset = lastNormal;
vec2 AAOffset = totalOffset;
AAOffset.x *= 0.5f * inverseScaleX;
AAOffset.y *= 0.5f * inverseScaleY;
vec2 innerOffset = totalOffset;
scaleOffsetForStrokeWidth(innerOffset, halfStrokeWidth, inverseScaleX, inverseScaleY);
vec2 outerOffset = innerOffset + AAOffset;
innerOffset -= AAOffset;
// TODO: support square cap by changing this offset to incorporate halfStrokeWidth
vec2 capAAOffset(AAOffset.y, -AAOffset.x);
AlphaVertex::set(&buffer[0],
last->position[0] + outerOffset.x + capAAOffset.x,
last->position[1] + outerOffset.y + capAAOffset.y,
0.0f);
AlphaVertex::set(&buffer[1],
last->position[0] + innerOffset.x - capAAOffset.x,
last->position[1] + innerOffset.y - capAAOffset.y,
maxAlpha);
AlphaVertex::set(&buffer[2 * offset + 6],
last->position[0] - outerOffset.x + capAAOffset.x,
last->position[1] - outerOffset.y + capAAOffset.y,
0.0f);
AlphaVertex::set(&buffer[2 * offset + 7],
last->position[0] - innerOffset.x - capAAOffset.x,
last->position[1] - innerOffset.y - capAAOffset.y,
maxAlpha);
copyAlphaVertex(&buffer[2 * offset + 8], &buffer[0]);
copyAlphaVertex(&buffer[2 * offset + 9], &buffer[1]);
copyAlphaVertex(&buffer[2 * offset + 10], &buffer[1]); // degenerate tris (the only two!)
copyAlphaVertex(&buffer[2 * offset + 11], &buffer[2 * offset + 7]);
}
for (unsigned int i = 1; i < vertices.size() - 1; i++) {
const Vertex* next = &(vertices[i + 1]);
vec2 nextNormal(next->position[1] - current->position[1],
current->position[0] - next->position[0]);
nextNormal.normalize();
vec2 totalOffset = totalOffsetFromNormals(lastNormal, nextNormal);
vec2 AAOffset = totalOffset;
AAOffset.x *= 0.5f * inverseScaleX;
AAOffset.y *= 0.5f * inverseScaleY;
vec2 innerOffset = totalOffset;
scaleOffsetForStrokeWidth(innerOffset, halfStrokeWidth, inverseScaleX, inverseScaleY);
vec2 outerOffset = innerOffset + AAOffset;
innerOffset -= AAOffset;
AlphaVertex::set(&buffer[currentAAOuterIndex++],
current->position[0] + outerOffset.x,
current->position[1] + outerOffset.y,
0.0f);
AlphaVertex::set(&buffer[currentAAOuterIndex++],
current->position[0] + innerOffset.x,
current->position[1] + innerOffset.y,
maxAlpha);
AlphaVertex::set(&buffer[currentStrokeIndex++],
current->position[0] + innerOffset.x,
current->position[1] + innerOffset.y,
maxAlpha);
AlphaVertex::set(&buffer[currentStrokeIndex++],
current->position[0] - innerOffset.x,
current->position[1] - innerOffset.y,
maxAlpha);
AlphaVertex::set(&buffer[currentAAInnerIndex--],
current->position[0] - innerOffset.x,
current->position[1] - innerOffset.y,
maxAlpha);
AlphaVertex::set(&buffer[currentAAInnerIndex--],
current->position[0] - outerOffset.x,
current->position[1] - outerOffset.y,
0.0f);
last = current;
current = next;
lastNormal = nextNormal;
}
{
// end cap
vec2 totalOffset = lastNormal;
vec2 AAOffset = totalOffset;
AAOffset.x *= 0.5f * inverseScaleX;
AAOffset.y *= 0.5f * inverseScaleY;
vec2 innerOffset = totalOffset;
scaleOffsetForStrokeWidth(innerOffset, halfStrokeWidth, inverseScaleX, inverseScaleY);
vec2 outerOffset = innerOffset + AAOffset;
innerOffset -= AAOffset;
// TODO: support square cap by changing this offset to incorporate halfStrokeWidth
vec2 capAAOffset(-AAOffset.y, AAOffset.x);
AlphaVertex::set(&buffer[offset + 2],
current->position[0] + outerOffset.x + capAAOffset.x,
current->position[1] + outerOffset.y + capAAOffset.y,
0.0f);
AlphaVertex::set(&buffer[offset + 3],
current->position[0] + innerOffset.x - capAAOffset.x,
current->position[1] + innerOffset.y - capAAOffset.y,
maxAlpha);
AlphaVertex::set(&buffer[offset + 4],
current->position[0] - outerOffset.x + capAAOffset.x,
current->position[1] - outerOffset.y + capAAOffset.y,
0.0f);
AlphaVertex::set(&buffer[offset + 5],
current->position[0] - innerOffset.x - capAAOffset.x,
current->position[1] - innerOffset.y - capAAOffset.y,
maxAlpha);
copyAlphaVertex(&buffer[vertexBuffer.getSize() - 2], &buffer[offset + 3]);
copyAlphaVertex(&buffer[vertexBuffer.getSize() - 1], &buffer[offset + 5]);
}
#if VERTEX_DEBUG
for (unsigned int i = 0; i < vertexBuffer.getSize(); i++) {
ALOGD("point at %f %f, alpha %f", buffer[i].position[0], buffer[i].position[1], buffer[i].alpha);
}
#endif
}
void getStrokeVerticesFromPerimeterAA(const Vector<Vertex>& perimeter, float halfStrokeWidth,
VertexBuffer& vertexBuffer, float inverseScaleX, float inverseScaleY) {
AlphaVertex* buffer = vertexBuffer.alloc<AlphaVertex>(6 * perimeter.size() + 8);
// avoid lines smaller than hairline since they break triangle based sampling. instead reducing
// alpha value (TODO: support different X/Y scale)
float maxAlpha = 1.0f;
if (halfStrokeWidth != 0 && inverseScaleX == inverseScaleY &&
halfStrokeWidth * inverseScaleX < 0.5f) {
maxAlpha *= (2 * halfStrokeWidth) / inverseScaleX;
halfStrokeWidth = 0.0f;
}
int offset = 2 * perimeter.size() + 3;
int currentAAOuterIndex = 0;
int currentStrokeIndex = offset;
int currentAAInnerIndex = offset * 2;
const Vertex* last = &(perimeter[perimeter.size() - 1]);
const Vertex* current = &(perimeter[0]);
vec2 lastNormal(current->position[1] - last->position[1],
last->position[0] - current->position[0]);
lastNormal.normalize();
for (unsigned int i = 0; i < perimeter.size(); i++) {
const Vertex* next = &(perimeter[i + 1 >= perimeter.size() ? 0 : i + 1]);
vec2 nextNormal(next->position[1] - current->position[1],
current->position[0] - next->position[0]);
nextNormal.normalize();
vec2 totalOffset = totalOffsetFromNormals(lastNormal, nextNormal);
vec2 AAOffset = totalOffset;
AAOffset.x *= 0.5f * inverseScaleX;
AAOffset.y *= 0.5f * inverseScaleY;
vec2 innerOffset = totalOffset;
scaleOffsetForStrokeWidth(innerOffset, halfStrokeWidth, inverseScaleX, inverseScaleY);
vec2 outerOffset = innerOffset + AAOffset;
innerOffset -= AAOffset;
AlphaVertex::set(&buffer[currentAAOuterIndex++],
current->position[0] + outerOffset.x,
current->position[1] + outerOffset.y,
0.0f);
AlphaVertex::set(&buffer[currentAAOuterIndex++],
current->position[0] + innerOffset.x,
current->position[1] + innerOffset.y,
maxAlpha);
AlphaVertex::set(&buffer[currentStrokeIndex++],
current->position[0] + innerOffset.x,
current->position[1] + innerOffset.y,
maxAlpha);
AlphaVertex::set(&buffer[currentStrokeIndex++],
current->position[0] - innerOffset.x,
current->position[1] - innerOffset.y,
maxAlpha);
AlphaVertex::set(&buffer[currentAAInnerIndex++],
current->position[0] - innerOffset.x,
current->position[1] - innerOffset.y,
maxAlpha);
AlphaVertex::set(&buffer[currentAAInnerIndex++],
current->position[0] - outerOffset.x,
current->position[1] - outerOffset.y,
0.0f);
last = current;
current = next;
lastNormal = nextNormal;
}
// wrap each strip around to beginning, creating degenerate tris to bridge strips
copyAlphaVertex(&buffer[currentAAOuterIndex++], &buffer[0]);
copyAlphaVertex(&buffer[currentAAOuterIndex++], &buffer[1]);
copyAlphaVertex(&buffer[currentAAOuterIndex++], &buffer[1]);
copyAlphaVertex(&buffer[currentStrokeIndex++], &buffer[offset]);
copyAlphaVertex(&buffer[currentStrokeIndex++], &buffer[offset + 1]);
copyAlphaVertex(&buffer[currentStrokeIndex++], &buffer[offset + 1]);
copyAlphaVertex(&buffer[currentAAInnerIndex++], &buffer[2 * offset]);
copyAlphaVertex(&buffer[currentAAInnerIndex++], &buffer[2 * offset + 1]);
// don't need to create last degenerate tri
#if VERTEX_DEBUG
for (unsigned int i = 0; i < vertexBuffer.getSize(); i++) {
ALOGD("point at %f %f, alpha %f", buffer[i].position[0], buffer[i].position[1], buffer[i].alpha);
}
#endif
}
void PathRenderer::convexPathVertices(const SkPath &path, const SkPaint* paint,
const mat4 *transform, VertexBuffer& vertexBuffer) {
ATRACE_CALL();
SkPaint::Style style = paint->getStyle();
bool isAA = paint->isAntiAlias();
float inverseScaleX, inverseScaleY;
computeInverseScales(transform, inverseScaleX, inverseScaleY);
Vector<Vertex> tempVertices;
float threshInvScaleX = inverseScaleX;
float threshInvScaleY = inverseScaleY;
if (style == SkPaint::kStroke_Style) {
// alter the bezier recursion threshold values we calculate in order to compensate for
// expansion done after the path vertices are found
SkRect bounds = path.getBounds();
if (!bounds.isEmpty()) {
threshInvScaleX *= bounds.width() / (bounds.width() + paint->getStrokeWidth());
threshInvScaleY *= bounds.height() / (bounds.height() + paint->getStrokeWidth());
}
}
// force close if we're filling the path, since fill path expects closed perimeter.
bool forceClose = style != SkPaint::kStroke_Style;
bool wasClosed = convexPathPerimeterVertices(path, forceClose, threshInvScaleX * threshInvScaleX,
threshInvScaleY * threshInvScaleY, tempVertices);
if (!tempVertices.size()) {
// path was empty, return without allocating vertex buffer
return;
}
#if VERTEX_DEBUG
for (unsigned int i = 0; i < tempVertices.size(); i++) {
ALOGD("orig path: point at %f %f", tempVertices[i].position[0], tempVertices[i].position[1]);
}
#endif
if (style == SkPaint::kStroke_Style) {
float halfStrokeWidth = paint->getStrokeWidth() * 0.5f;
if (!isAA) {
if (wasClosed) {
getStrokeVerticesFromPerimeter(tempVertices, halfStrokeWidth, vertexBuffer,
inverseScaleX, inverseScaleY);
} else {
getStrokeVerticesFromUnclosedVertices(tempVertices, halfStrokeWidth, vertexBuffer,
inverseScaleX, inverseScaleY);
}
} else {
if (wasClosed) {
getStrokeVerticesFromPerimeterAA(tempVertices, halfStrokeWidth, vertexBuffer,
inverseScaleX, inverseScaleY);
} else {
getStrokeVerticesFromUnclosedVerticesAA(tempVertices, halfStrokeWidth, vertexBuffer,
inverseScaleX, inverseScaleY);
}
}
} else {
// For kStrokeAndFill style, the path should be adjusted externally, as it will be treated as a fill here.
if (!isAA) {
getFillVerticesFromPerimeter(tempVertices, vertexBuffer);
} else {
getFillVerticesFromPerimeterAA(tempVertices, vertexBuffer, inverseScaleX, inverseScaleY);
}
}
}
void pushToVector(Vector<Vertex>& vertices, float x, float y) {
// TODO: make this not yuck
vertices.push();
Vertex* newVertex = &(vertices.editArray()[vertices.size() - 1]);
Vertex::set(newVertex, x, y);
}
bool PathRenderer::convexPathPerimeterVertices(const SkPath& path, bool forceClose,
float sqrInvScaleX, float sqrInvScaleY, Vector<Vertex>& outputVertices) {
ATRACE_CALL();
// TODO: to support joins other than sharp miter, join vertices should be labelled in the
// perimeter, or resolved into more vertices. Reconsider forceClose-ing in that case.
SkPath::Iter iter(path, forceClose);
SkPoint pts[4];
SkPath::Verb v;
while (SkPath::kDone_Verb != (v = iter.next(pts))) {
switch (v) {
case SkPath::kMove_Verb:
pushToVector(outputVertices, pts[0].x(), pts[0].y());
ALOGV("Move to pos %f %f", pts[0].x(), pts[0].y());
break;
case SkPath::kClose_Verb:
ALOGV("Close at pos %f %f", pts[0].x(), pts[0].y());
break;
case SkPath::kLine_Verb:
ALOGV("kLine_Verb %f %f -> %f %f",
pts[0].x(), pts[0].y(),
pts[1].x(), pts[1].y());
pushToVector(outputVertices, pts[1].x(), pts[1].y());
break;
case SkPath::kQuad_Verb:
ALOGV("kQuad_Verb");
recursiveQuadraticBezierVertices(
pts[0].x(), pts[0].y(),
pts[2].x(), pts[2].y(),
pts[1].x(), pts[1].y(),
sqrInvScaleX, sqrInvScaleY, outputVertices);
break;
case SkPath::kCubic_Verb:
ALOGV("kCubic_Verb");
recursiveCubicBezierVertices(
pts[0].x(), pts[0].y(),
pts[1].x(), pts[1].y(),
pts[3].x(), pts[3].y(),
pts[2].x(), pts[2].y(),
sqrInvScaleX, sqrInvScaleY, outputVertices);
break;
default:
break;
}
}
int size = outputVertices.size();
if (size >= 2 && outputVertices[0].position[0] == outputVertices[size - 1].position[0] &&
outputVertices[0].position[1] == outputVertices[size - 1].position[1]) {
outputVertices.pop();
return true;
}
return false;
}
void PathRenderer::recursiveCubicBezierVertices(
float p1x, float p1y, float c1x, float c1y,
float p2x, float p2y, float c2x, float c2y,
float sqrInvScaleX, float sqrInvScaleY, Vector<Vertex>& outputVertices) {
float dx = p2x - p1x;
float dy = p2y - p1y;
float d1 = fabs((c1x - p2x) * dy - (c1y - p2y) * dx);
float d2 = fabs((c2x - p2x) * dy - (c2y - p2y) * dx);
float d = d1 + d2;
// multiplying by sqrInvScaleY/X equivalent to multiplying in dimensional scale factors
if (d * d < THRESHOLD * THRESHOLD * (dx * dx * sqrInvScaleY + dy * dy * sqrInvScaleX)) {
// below thresh, draw line by adding endpoint
pushToVector(outputVertices, p2x, p2y);
} else {
float p1c1x = (p1x + c1x) * 0.5f;
float p1c1y = (p1y + c1y) * 0.5f;
float p2c2x = (p2x + c2x) * 0.5f;
float p2c2y = (p2y + c2y) * 0.5f;
float c1c2x = (c1x + c2x) * 0.5f;
float c1c2y = (c1y + c2y) * 0.5f;
float p1c1c2x = (p1c1x + c1c2x) * 0.5f;
float p1c1c2y = (p1c1y + c1c2y) * 0.5f;
float p2c1c2x = (p2c2x + c1c2x) * 0.5f;
float p2c1c2y = (p2c2y + c1c2y) * 0.5f;
float mx = (p1c1c2x + p2c1c2x) * 0.5f;
float my = (p1c1c2y + p2c1c2y) * 0.5f;
recursiveCubicBezierVertices(
p1x, p1y, p1c1x, p1c1y,
mx, my, p1c1c2x, p1c1c2y,
sqrInvScaleX, sqrInvScaleY, outputVertices);
recursiveCubicBezierVertices(
mx, my, p2c1c2x, p2c1c2y,
p2x, p2y, p2c2x, p2c2y,
sqrInvScaleX, sqrInvScaleY, outputVertices);
}
}
void PathRenderer::recursiveQuadraticBezierVertices(
float ax, float ay,
float bx, float by,
float cx, float cy,
float sqrInvScaleX, float sqrInvScaleY, Vector<Vertex>& outputVertices) {
float dx = bx - ax;
float dy = by - ay;
float d = (cx - bx) * dy - (cy - by) * dx;
if (d * d < THRESHOLD * THRESHOLD * (dx * dx * sqrInvScaleY + dy * dy * sqrInvScaleX)) {
// below thresh, draw line by adding endpoint
pushToVector(outputVertices, bx, by);
} else {
float acx = (ax + cx) * 0.5f;
float bcx = (bx + cx) * 0.5f;
float acy = (ay + cy) * 0.5f;
float bcy = (by + cy) * 0.5f;
// midpoint
float mx = (acx + bcx) * 0.5f;
float my = (acy + bcy) * 0.5f;
recursiveQuadraticBezierVertices(ax, ay, mx, my, acx, acy,
sqrInvScaleX, sqrInvScaleY, outputVertices);
recursiveQuadraticBezierVertices(mx, my, bx, by, bcx, bcy,
sqrInvScaleX, sqrInvScaleY, outputVertices);
}
}
}; // namespace uirenderer
}; // namespace android

View File

@@ -0,0 +1,970 @@
/*
* Copyright (C) 2012 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.
*/
#define LOG_TAG "PathTessellator"
#define LOG_NDEBUG 1
#define ATRACE_TAG ATRACE_TAG_GRAPHICS
#define VERTEX_DEBUG 0
#if VERTEX_DEBUG
#define DEBUG_DUMP_ALPHA_BUFFER() \
for (unsigned int i = 0; i < vertexBuffer.getSize(); i++) { \
ALOGD("point %d at %f %f, alpha %f", \
i, buffer[i].position[0], buffer[i].position[1], buffer[i].alpha); \
}
#define DEBUG_DUMP_BUFFER() \
for (unsigned int i = 0; i < vertexBuffer.getSize(); i++) { \
ALOGD("point %d at %f %f", i, buffer[i].position[0], buffer[i].position[1]); \
}
#else
#define DEBUG_DUMP_ALPHA_BUFFER()
#define DEBUG_DUMP_BUFFER()
#endif
#include <SkPath.h>
#include <SkPaint.h>
#include <stdlib.h>
#include <stdint.h>
#include <sys/types.h>
#include <utils/Log.h>
#include <utils/Trace.h>
#include "PathTessellator.h"
#include "Matrix.h"
#include "Vector.h"
#include "Vertex.h"
namespace android {
namespace uirenderer {
#define THRESHOLD 0.5f
#define ROUND_CAP_THRESH 0.25f
#define PI 3.1415926535897932f
void PathTessellator::expandBoundsForStroke(SkRect& bounds, const SkPaint* paint,
bool forceExpand) {
if (forceExpand || paint->getStyle() != SkPaint::kFill_Style) {
float outset = paint->getStrokeWidth() * 0.5f;
bounds.outset(outset, outset);
}
}
inline void copyVertex(Vertex* destPtr, const Vertex* srcPtr) {
Vertex::set(destPtr, srcPtr->position[0], srcPtr->position[1]);
}
inline void copyAlphaVertex(AlphaVertex* destPtr, const AlphaVertex* srcPtr) {
AlphaVertex::set(destPtr, srcPtr->position[0], srcPtr->position[1], srcPtr->alpha);
}
/**
* Produces a pseudo-normal for a vertex, given the normals of the two incoming lines. If the offset
* from each vertex in a perimeter is calculated, the resultant lines connecting the offset vertices
* will be offset by 1.0
*
* Note that we can't add and normalize the two vectors, that would result in a rectangle having an
* offset of (sqrt(2)/2, sqrt(2)/2) at each corner, instead of (1, 1)
*
* NOTE: assumes angles between normals 90 degrees or less
*/
inline vec2 totalOffsetFromNormals(const vec2& normalA, const vec2& normalB) {
return (normalA + normalB) / (1 + fabs(normalA.dot(normalB)));
}
/**
* Structure used for storing useful information about the SkPaint and scale used for tessellating
*/
struct PaintInfo {
public:
PaintInfo(const SkPaint* paint, const mat4 *transform) :
style(paint->getStyle()), cap(paint->getStrokeCap()), isAA(paint->isAntiAlias()),
inverseScaleX(1.0f), inverseScaleY(1.0f),
halfStrokeWidth(paint->getStrokeWidth() * 0.5f), maxAlpha(1.0f) {
// compute inverse scales
if (CC_UNLIKELY(!transform->isPureTranslate())) {
float m00 = transform->data[Matrix4::kScaleX];
float m01 = transform->data[Matrix4::kSkewY];
float m10 = transform->data[Matrix4::kSkewX];
float m11 = transform->data[Matrix4::kScaleY];
float scaleX = sqrt(m00 * m00 + m01 * m01);
float scaleY = sqrt(m10 * m10 + m11 * m11);
inverseScaleX = (scaleX != 0) ? (1.0f / scaleX) : 1.0f;
inverseScaleY = (scaleY != 0) ? (1.0f / scaleY) : 1.0f;
}
if (isAA && halfStrokeWidth != 0 && inverseScaleX == inverseScaleY &&
halfStrokeWidth * inverseScaleX < 0.5f) {
maxAlpha *= (2 * halfStrokeWidth) / inverseScaleX;
halfStrokeWidth = 0.0f;
}
}
SkPaint::Style style;
SkPaint::Cap cap;
bool isAA;
float inverseScaleX;
float inverseScaleY;
float halfStrokeWidth;
float maxAlpha;
inline void scaleOffsetForStrokeWidth(vec2& offset) const {
if (halfStrokeWidth == 0.0f) {
// hairline - compensate for scale
offset.x *= 0.5f * inverseScaleX;
offset.y *= 0.5f * inverseScaleY;
} else {
offset *= halfStrokeWidth;
}
}
/**
* NOTE: the input will not always be a normal, especially for sharp edges - it should be the
* result of totalOffsetFromNormals (see documentation there)
*/
inline vec2 deriveAAOffset(const vec2& offset) const {
return vec2(offset.x * 0.5f * inverseScaleX,
offset.y * 0.5f * inverseScaleY);
}
/**
* Returns the number of cap divisions beyond the minimum 2 (kButt_Cap/kSquareCap will return 0)
* Should only be used when stroking and drawing caps
*/
inline int capExtraDivisions() const {
if (cap == SkPaint::kRound_Cap) {
if (halfStrokeWidth == 0.0f) return 2;
// ROUND_CAP_THRESH is the maximum error for polygonal approximation of the round cap
const float errConst = (-ROUND_CAP_THRESH / halfStrokeWidth + 1);
const float targetCosVal = 2 * errConst * errConst - 1;
int neededDivisions = (int)(ceilf(PI / acos(targetCosVal)/2)) * 2;
return neededDivisions;
}
return 0;
}
};
void getFillVerticesFromPerimeter(const Vector<Vertex>& perimeter, VertexBuffer& vertexBuffer) {
Vertex* buffer = vertexBuffer.alloc<Vertex>(perimeter.size());
int currentIndex = 0;
// zig zag between all previous points on the inside of the hull to create a
// triangle strip that fills the hull
int srcAindex = 0;
int srcBindex = perimeter.size() - 1;
while (srcAindex <= srcBindex) {
copyVertex(&buffer[currentIndex++], &perimeter[srcAindex]);
if (srcAindex == srcBindex) break;
copyVertex(&buffer[currentIndex++], &perimeter[srcBindex]);
srcAindex++;
srcBindex--;
}
}
/*
* Fills a vertexBuffer with non-alpha vertices, zig-zagging at each perimeter point to create a
* tri-strip as wide as the stroke.
*
* Uses an additional 2 vertices at the end to wrap around, closing the tri-strip
* (for a total of perimeter.size() * 2 + 2 vertices)
*/
void getStrokeVerticesFromPerimeter(const PaintInfo& paintInfo, const Vector<Vertex>& perimeter,
VertexBuffer& vertexBuffer) {
Vertex* buffer = vertexBuffer.alloc<Vertex>(perimeter.size() * 2 + 2);
int currentIndex = 0;
const Vertex* last = &(perimeter[perimeter.size() - 1]);
const Vertex* current = &(perimeter[0]);
vec2 lastNormal(current->position[1] - last->position[1],
last->position[0] - current->position[0]);
lastNormal.normalize();
for (unsigned int i = 0; i < perimeter.size(); i++) {
const Vertex* next = &(perimeter[i + 1 >= perimeter.size() ? 0 : i + 1]);
vec2 nextNormal(next->position[1] - current->position[1],
current->position[0] - next->position[0]);
nextNormal.normalize();
vec2 totalOffset = totalOffsetFromNormals(lastNormal, nextNormal);
paintInfo.scaleOffsetForStrokeWidth(totalOffset);
Vertex::set(&buffer[currentIndex++],
current->position[0] + totalOffset.x,
current->position[1] + totalOffset.y);
Vertex::set(&buffer[currentIndex++],
current->position[0] - totalOffset.x,
current->position[1] - totalOffset.y);
last = current;
current = next;
lastNormal = nextNormal;
}
// wrap around to beginning
copyVertex(&buffer[currentIndex++], &buffer[0]);
copyVertex(&buffer[currentIndex++], &buffer[1]);
DEBUG_DUMP_BUFFER();
}
/**
* Fills a vertexBuffer with non-alpha vertices similar to getStrokeVerticesFromPerimeter, except:
*
* 1 - Doesn't need to wrap around, since the input vertices are unclosed
*
* 2 - can zig-zag across 'extra' vertices at either end, to create round caps
*/
void getStrokeVerticesFromUnclosedVertices(const PaintInfo& paintInfo,
const Vector<Vertex>& vertices, VertexBuffer& vertexBuffer) {
const int extra = paintInfo.capExtraDivisions();
const int allocSize = (vertices.size() + extra) * 2;
Vertex* buffer = vertexBuffer.alloc<Vertex>(allocSize);
if (extra > 0) {
// tessellate both round caps
const int last = vertices.size() - 1;
float beginTheta = atan2(
- (vertices[0].position[0] - vertices[1].position[0]),
vertices[0].position[1] - vertices[1].position[1]);
float endTheta = atan2(
- (vertices[last].position[0] - vertices[last - 1].position[0]),
vertices[last].position[1] - vertices[last - 1].position[1]);
const float dTheta = PI / (extra + 1);
const float radialScale = 2.0f / (1 + cos(dTheta));
int capOffset;
for (int i = 0; i < extra; i++) {
if (i < extra / 2) {
capOffset = extra - 2 * i - 1;
} else {
capOffset = 2 * i - extra;
}
beginTheta += dTheta;
vec2 beginRadialOffset(cos(beginTheta), sin(beginTheta));
paintInfo.scaleOffsetForStrokeWidth(beginRadialOffset);
Vertex::set(&buffer[capOffset],
vertices[0].position[0] + beginRadialOffset.x,
vertices[0].position[1] + beginRadialOffset.y);
endTheta += dTheta;
vec2 endRadialOffset(cos(endTheta), sin(endTheta));
paintInfo.scaleOffsetForStrokeWidth(endRadialOffset);
Vertex::set(&buffer[allocSize - 1 - capOffset],
vertices[last].position[0] + endRadialOffset.x,
vertices[last].position[1] + endRadialOffset.y);
}
}
int currentIndex = extra;
const Vertex* current = &(vertices[0]);
vec2 lastNormal;
for (unsigned int i = 0; i < vertices.size() - 1; i++) {
const Vertex* next = &(vertices[i + 1]);
vec2 nextNormal(next->position[1] - current->position[1],
current->position[0] - next->position[0]);
nextNormal.normalize();
vec2 totalOffset;
if (i == 0) {
totalOffset = nextNormal;
} else {
totalOffset = totalOffsetFromNormals(lastNormal, nextNormal);
}
paintInfo.scaleOffsetForStrokeWidth(totalOffset);
Vertex::set(&buffer[currentIndex++],
current->position[0] + totalOffset.x,
current->position[1] + totalOffset.y);
Vertex::set(&buffer[currentIndex++],
current->position[0] - totalOffset.x,
current->position[1] - totalOffset.y);
current = next;
lastNormal = nextNormal;
}
vec2 totalOffset = lastNormal;
paintInfo.scaleOffsetForStrokeWidth(totalOffset);
Vertex::set(&buffer[currentIndex++],
current->position[0] + totalOffset.x,
current->position[1] + totalOffset.y);
Vertex::set(&buffer[currentIndex++],
current->position[0] - totalOffset.x,
current->position[1] - totalOffset.y);
DEBUG_DUMP_BUFFER();
}
/**
* Populates a vertexBuffer with AlphaVertices to create an anti-aliased fill shape tessellation
*
* 1 - create the AA perimeter of unit width, by zig-zagging at each point around the perimeter of
* the shape (using 2 * perimeter.size() vertices)
*
* 2 - wrap around to the beginning to complete the perimeter (2 vertices)
*
* 3 - zig zag back and forth inside the shape to fill it (using perimeter.size() vertices)
*/
void getFillVerticesFromPerimeterAA(const PaintInfo& paintInfo, const Vector<Vertex>& perimeter,
VertexBuffer& vertexBuffer) {
AlphaVertex* buffer = vertexBuffer.alloc<AlphaVertex>(perimeter.size() * 3 + 2);
// generate alpha points - fill Alpha vertex gaps in between each point with
// alpha 0 vertex, offset by a scaled normal.
int currentIndex = 0;
const Vertex* last = &(perimeter[perimeter.size() - 1]);
const Vertex* current = &(perimeter[0]);
vec2 lastNormal(current->position[1] - last->position[1],
last->position[0] - current->position[0]);
lastNormal.normalize();
for (unsigned int i = 0; i < perimeter.size(); i++) {
const Vertex* next = &(perimeter[i + 1 >= perimeter.size() ? 0 : i + 1]);
vec2 nextNormal(next->position[1] - current->position[1],
current->position[0] - next->position[0]);
nextNormal.normalize();
// AA point offset from original point is that point's normal, such that each side is offset
// by .5 pixels
vec2 totalOffset = paintInfo.deriveAAOffset(totalOffsetFromNormals(lastNormal, nextNormal));
AlphaVertex::set(&buffer[currentIndex++],
current->position[0] + totalOffset.x,
current->position[1] + totalOffset.y,
0.0f);
AlphaVertex::set(&buffer[currentIndex++],
current->position[0] - totalOffset.x,
current->position[1] - totalOffset.y,
1.0f);
last = current;
current = next;
lastNormal = nextNormal;
}
// wrap around to beginning
copyAlphaVertex(&buffer[currentIndex++], &buffer[0]);
copyAlphaVertex(&buffer[currentIndex++], &buffer[1]);
// zig zag between all previous points on the inside of the hull to create a
// triangle strip that fills the hull, repeating the first inner point to
// create degenerate tris to start inside path
int srcAindex = 0;
int srcBindex = perimeter.size() - 1;
while (srcAindex <= srcBindex) {
copyAlphaVertex(&buffer[currentIndex++], &buffer[srcAindex * 2 + 1]);
if (srcAindex == srcBindex) break;
copyAlphaVertex(&buffer[currentIndex++], &buffer[srcBindex * 2 + 1]);
srcAindex++;
srcBindex--;
}
DEBUG_DUMP_BUFFER();
}
/**
* Stores geometry for a single, AA-perimeter (potentially rounded) cap
*
* For explanation of constants and general methodoloyg, see comments for
* getStrokeVerticesFromUnclosedVerticesAA() below.
*/
inline void storeCapAA(const PaintInfo& paintInfo, const Vector<Vertex>& vertices,
AlphaVertex* buffer, bool isFirst, vec2 normal, int offset) {
const int extra = paintInfo.capExtraDivisions();
const int extraOffset = (extra + 1) / 2;
const int capIndex = isFirst
? 2 * offset + 6 + 2 * (extra + extraOffset)
: offset + 2 + 2 * extraOffset;
if (isFirst) normal *= -1;
// TODO: this normal should be scaled by radialScale if extra != 0, see totalOffsetFromNormals()
vec2 AAOffset = paintInfo.deriveAAOffset(normal);
vec2 strokeOffset = normal;
paintInfo.scaleOffsetForStrokeWidth(strokeOffset);
vec2 outerOffset = strokeOffset + AAOffset;
vec2 innerOffset = strokeOffset - AAOffset;
vec2 capAAOffset;
if (paintInfo.cap != SkPaint::kRound_Cap) {
// if the cap is square or butt, the inside primary cap vertices will be inset in two
// directions - both normal to the stroke, and parallel to it.
capAAOffset = vec2(-AAOffset.y, AAOffset.x);
}
// determine referencePoint, the center point for the 4 primary cap vertices
const Vertex* point = isFirst ? vertices.begin() : (vertices.end() - 1);
vec2 referencePoint(point->position[0], point->position[1]);
if (paintInfo.cap == SkPaint::kSquare_Cap) {
// To account for square cap, move the primary cap vertices (that create the AA edge) by the
// stroke offset vector (rotated to be parallel to the stroke)
referencePoint += vec2(-strokeOffset.y, strokeOffset.x);
}
AlphaVertex::set(&buffer[capIndex + 0],
referencePoint.x + outerOffset.x + capAAOffset.x,
referencePoint.y + outerOffset.y + capAAOffset.y,
0.0f);
AlphaVertex::set(&buffer[capIndex + 1],
referencePoint.x + innerOffset.x - capAAOffset.x,
referencePoint.y + innerOffset.y - capAAOffset.y,
paintInfo.maxAlpha);
bool isRound = paintInfo.cap == SkPaint::kRound_Cap;
const int postCapIndex = (isRound && isFirst) ? (2 * extraOffset - 2) : capIndex + (2 * extra);
AlphaVertex::set(&buffer[postCapIndex + 2],
referencePoint.x - outerOffset.x + capAAOffset.x,
referencePoint.y - outerOffset.y + capAAOffset.y,
0.0f);
AlphaVertex::set(&buffer[postCapIndex + 3],
referencePoint.x - innerOffset.x - capAAOffset.x,
referencePoint.y - innerOffset.y - capAAOffset.y,
paintInfo.maxAlpha);
if (isRound) {
const float dTheta = PI / (extra + 1);
const float radialScale = 2.0f / (1 + cos(dTheta));
float theta = atan2(normal.y, normal.x);
int capPerimIndex = capIndex + 2;
for (int i = 0; i < extra; i++) {
theta += dTheta;
vec2 radialOffset(cos(theta), sin(theta));
// scale to compensate for pinching at sharp angles, see totalOffsetFromNormals()
radialOffset *= radialScale;
AAOffset = paintInfo.deriveAAOffset(radialOffset);
paintInfo.scaleOffsetForStrokeWidth(radialOffset);
AlphaVertex::set(&buffer[capPerimIndex++],
referencePoint.x + radialOffset.x + AAOffset.x,
referencePoint.y + radialOffset.y + AAOffset.y,
0.0f);
AlphaVertex::set(&buffer[capPerimIndex++],
referencePoint.x + radialOffset.x - AAOffset.x,
referencePoint.y + radialOffset.y - AAOffset.y,
paintInfo.maxAlpha);
if (isFirst && i == extra - extraOffset) {
//copy most recent two points to first two points
copyAlphaVertex(&buffer[0], &buffer[capPerimIndex - 2]);
copyAlphaVertex(&buffer[1], &buffer[capPerimIndex - 1]);
capPerimIndex = 2; // start writing the rest of the round cap at index 2
}
}
if (isFirst) {
const int startCapFillIndex = capIndex + 2 * (extra - extraOffset) + 4;
int capFillIndex = startCapFillIndex;
for (int i = 0; i < extra + 2; i += 2) {
copyAlphaVertex(&buffer[capFillIndex++], &buffer[1 + i]);
// TODO: to support odd numbers of divisions, break here on the last iteration
copyAlphaVertex(&buffer[capFillIndex++], &buffer[startCapFillIndex - 3 - i]);
}
} else {
int capFillIndex = 6 * vertices.size() + 2 + 6 * extra - (extra + 2);
for (int i = 0; i < extra + 2; i += 2) {
copyAlphaVertex(&buffer[capFillIndex++], &buffer[capIndex + 1 + i]);
// TODO: to support odd numbers of divisions, break here on the last iteration
copyAlphaVertex(&buffer[capFillIndex++], &buffer[capIndex + 3 + 2 * extra - i]);
}
}
return;
}
if (isFirst) {
copyAlphaVertex(&buffer[0], &buffer[postCapIndex + 2]);
copyAlphaVertex(&buffer[1], &buffer[postCapIndex + 3]);
copyAlphaVertex(&buffer[postCapIndex + 4], &buffer[1]); // degenerate tris (the only two!)
copyAlphaVertex(&buffer[postCapIndex + 5], &buffer[postCapIndex + 1]);
} else {
copyAlphaVertex(&buffer[6 * vertices.size()], &buffer[postCapIndex + 1]);
copyAlphaVertex(&buffer[6 * vertices.size() + 1], &buffer[postCapIndex + 3]);
}
}
/*
the geometry for an aa, capped stroke consists of the following:
# vertices | function
----------------------------------------------------------------------
a) 2 | Start AA perimeter
b) 2, 2 * roundDivOff | First half of begin cap's perimeter
|
2 * middlePts | 'Outer' or 'Top' AA perimeter half (between caps)
|
a) 4 | End cap's
b) 2, 2 * roundDivs, 2 | AA perimeter
|
2 * middlePts | 'Inner' or 'bottom' AA perimeter half
|
a) 6 | Begin cap's perimeter
b) 2, 2*(rD - rDO + 1), | Last half of begin cap's perimeter
roundDivs, 2 |
|
2 * middlePts | Stroke's full opacity center strip
|
a) 2 | end stroke
b) 2, roundDivs | (and end cap fill, for round)
Notes:
* rows starting with 'a)' denote the Butt or Square cap vertex use, 'b)' denote Round
* 'middlePts' is (number of points in the unclosed input vertex list, minus 2) times two
* 'roundDivs' or 'rD' is the number of extra vertices (beyond the minimum of 2) that define the
round cap's shape, and is at least two. This will increase with cap size to sufficiently
define the cap's level of tessellation.
* 'roundDivOffset' or 'rDO' is the point about halfway along the start cap's round perimeter, where
the stream of vertices for the AA perimeter starts. By starting and ending the perimeter at
this offset, the fill of the stroke is drawn from this point with minimal extra vertices.
This means the outer perimeter starts at:
outerIndex = (2) OR (2 + 2 * roundDivOff)
the inner perimeter (since it is filled in reverse) starts at:
innerIndex = outerIndex + (4 * middlePts) + ((4) OR (4 + 2 * roundDivs)) - 1
the stroke starts at:
strokeIndex = innerIndex + 1 + ((6) OR (6 + 3 * roundDivs - 2 * roundDivOffset))
The total needed allocated space is either:
2 + 4 + 6 + 2 + 3 * (2 * middlePts) = 14 + 6 * middlePts = 2 + 6 * pts
or, for rounded caps:
(2 + 2 * rDO) + (4 + 2 * rD) + (2 * (rD - rDO + 1)
+ roundDivs + 4) + (2 + roundDivs) + 3 * (2 * middlePts)
= 14 + 6 * middlePts + 6 * roundDivs
= 2 + 6 * pts + 6 * roundDivs
*/
void getStrokeVerticesFromUnclosedVerticesAA(const PaintInfo& paintInfo,
const Vector<Vertex>& vertices, VertexBuffer& vertexBuffer) {
const int extra = paintInfo.capExtraDivisions();
const int allocSize = 6 * vertices.size() + 2 + 6 * extra;
AlphaVertex* buffer = vertexBuffer.alloc<AlphaVertex>(allocSize);
const int extraOffset = (extra + 1) / 2;
int offset = 2 * (vertices.size() - 2);
// there is no outer/inner here, using them for consistency with below approach
int currentAAOuterIndex = 2 + 2 * extraOffset;
int currentAAInnerIndex = currentAAOuterIndex + (2 * offset) + 3 + (2 * extra);
int currentStrokeIndex = currentAAInnerIndex + 7 + (3 * extra - 2 * extraOffset);
const Vertex* last = &(vertices[0]);
const Vertex* current = &(vertices[1]);
vec2 lastNormal(current->position[1] - last->position[1],
last->position[0] - current->position[0]);
lastNormal.normalize();
// TODO: use normal from bezier traversal for cap, instead of from vertices
storeCapAA(paintInfo, vertices, buffer, true, lastNormal, offset);
for (unsigned int i = 1; i < vertices.size() - 1; i++) {
const Vertex* next = &(vertices[i + 1]);
vec2 nextNormal(next->position[1] - current->position[1],
current->position[0] - next->position[0]);
nextNormal.normalize();
vec2 totalOffset = totalOffsetFromNormals(lastNormal, nextNormal);
vec2 AAOffset = paintInfo.deriveAAOffset(totalOffset);
vec2 innerOffset = totalOffset;
paintInfo.scaleOffsetForStrokeWidth(innerOffset);
vec2 outerOffset = innerOffset + AAOffset;
innerOffset -= AAOffset;
AlphaVertex::set(&buffer[currentAAOuterIndex++],
current->position[0] + outerOffset.x,
current->position[1] + outerOffset.y,
0.0f);
AlphaVertex::set(&buffer[currentAAOuterIndex++],
current->position[0] + innerOffset.x,
current->position[1] + innerOffset.y,
paintInfo.maxAlpha);
AlphaVertex::set(&buffer[currentStrokeIndex++],
current->position[0] + innerOffset.x,
current->position[1] + innerOffset.y,
paintInfo.maxAlpha);
AlphaVertex::set(&buffer[currentStrokeIndex++],
current->position[0] - innerOffset.x,
current->position[1] - innerOffset.y,
paintInfo.maxAlpha);
AlphaVertex::set(&buffer[currentAAInnerIndex--],
current->position[0] - innerOffset.x,
current->position[1] - innerOffset.y,
paintInfo.maxAlpha);
AlphaVertex::set(&buffer[currentAAInnerIndex--],
current->position[0] - outerOffset.x,
current->position[1] - outerOffset.y,
0.0f);
current = next;
lastNormal = nextNormal;
}
// TODO: use normal from bezier traversal for cap, instead of from vertices
storeCapAA(paintInfo, vertices, buffer, false, lastNormal, offset);
DEBUG_DUMP_ALPHA_BUFFER();
}
void getStrokeVerticesFromPerimeterAA(const PaintInfo& paintInfo, const Vector<Vertex>& perimeter,
VertexBuffer& vertexBuffer) {
AlphaVertex* buffer = vertexBuffer.alloc<AlphaVertex>(6 * perimeter.size() + 8);
int offset = 2 * perimeter.size() + 3;
int currentAAOuterIndex = 0;
int currentStrokeIndex = offset;
int currentAAInnerIndex = offset * 2;
const Vertex* last = &(perimeter[perimeter.size() - 1]);
const Vertex* current = &(perimeter[0]);
vec2 lastNormal(current->position[1] - last->position[1],
last->position[0] - current->position[0]);
lastNormal.normalize();
for (unsigned int i = 0; i < perimeter.size(); i++) {
const Vertex* next = &(perimeter[i + 1 >= perimeter.size() ? 0 : i + 1]);
vec2 nextNormal(next->position[1] - current->position[1],
current->position[0] - next->position[0]);
nextNormal.normalize();
vec2 totalOffset = totalOffsetFromNormals(lastNormal, nextNormal);
vec2 AAOffset = paintInfo.deriveAAOffset(totalOffset);
vec2 innerOffset = totalOffset;
paintInfo.scaleOffsetForStrokeWidth(innerOffset);
vec2 outerOffset = innerOffset + AAOffset;
innerOffset -= AAOffset;
AlphaVertex::set(&buffer[currentAAOuterIndex++],
current->position[0] + outerOffset.x,
current->position[1] + outerOffset.y,
0.0f);
AlphaVertex::set(&buffer[currentAAOuterIndex++],
current->position[0] + innerOffset.x,
current->position[1] + innerOffset.y,
paintInfo.maxAlpha);
AlphaVertex::set(&buffer[currentStrokeIndex++],
current->position[0] + innerOffset.x,
current->position[1] + innerOffset.y,
paintInfo.maxAlpha);
AlphaVertex::set(&buffer[currentStrokeIndex++],
current->position[0] - innerOffset.x,
current->position[1] - innerOffset.y,
paintInfo.maxAlpha);
AlphaVertex::set(&buffer[currentAAInnerIndex++],
current->position[0] - innerOffset.x,
current->position[1] - innerOffset.y,
paintInfo.maxAlpha);
AlphaVertex::set(&buffer[currentAAInnerIndex++],
current->position[0] - outerOffset.x,
current->position[1] - outerOffset.y,
0.0f);
last = current;
current = next;
lastNormal = nextNormal;
}
// wrap each strip around to beginning, creating degenerate tris to bridge strips
copyAlphaVertex(&buffer[currentAAOuterIndex++], &buffer[0]);
copyAlphaVertex(&buffer[currentAAOuterIndex++], &buffer[1]);
copyAlphaVertex(&buffer[currentAAOuterIndex++], &buffer[1]);
copyAlphaVertex(&buffer[currentStrokeIndex++], &buffer[offset]);
copyAlphaVertex(&buffer[currentStrokeIndex++], &buffer[offset + 1]);
copyAlphaVertex(&buffer[currentStrokeIndex++], &buffer[offset + 1]);
copyAlphaVertex(&buffer[currentAAInnerIndex++], &buffer[2 * offset]);
copyAlphaVertex(&buffer[currentAAInnerIndex++], &buffer[2 * offset + 1]);
// don't need to create last degenerate tri
DEBUG_DUMP_ALPHA_BUFFER();
}
void PathTessellator::tessellatePath(const SkPath &path, const SkPaint* paint,
const mat4 *transform, VertexBuffer& vertexBuffer) {
ATRACE_CALL();
const PaintInfo paintInfo(paint, transform);
Vector<Vertex> tempVertices;
float threshInvScaleX = paintInfo.inverseScaleX;
float threshInvScaleY = paintInfo.inverseScaleY;
if (paintInfo.style == SkPaint::kStroke_Style) {
// alter the bezier recursion threshold values we calculate in order to compensate for
// expansion done after the path vertices are found
SkRect bounds = path.getBounds();
if (!bounds.isEmpty()) {
threshInvScaleX *= bounds.width() / (bounds.width() + paint->getStrokeWidth());
threshInvScaleY *= bounds.height() / (bounds.height() + paint->getStrokeWidth());
}
}
// force close if we're filling the path, since fill path expects closed perimeter.
bool forceClose = paintInfo.style != SkPaint::kStroke_Style;
bool wasClosed = approximatePathOutlineVertices(path, forceClose,
threshInvScaleX * threshInvScaleX, threshInvScaleY * threshInvScaleY, tempVertices);
if (!tempVertices.size()) {
// path was empty, return without allocating vertex buffer
return;
}
#if VERTEX_DEBUG
for (unsigned int i = 0; i < tempVertices.size(); i++) {
ALOGD("orig path: point at %f %f",
tempVertices[i].position[0], tempVertices[i].position[1]);
}
#endif
if (paintInfo.style == SkPaint::kStroke_Style) {
if (!paintInfo.isAA) {
if (wasClosed) {
getStrokeVerticesFromPerimeter(paintInfo, tempVertices, vertexBuffer);
} else {
getStrokeVerticesFromUnclosedVertices(paintInfo, tempVertices, vertexBuffer);
}
} else {
if (wasClosed) {
getStrokeVerticesFromPerimeterAA(paintInfo, tempVertices, vertexBuffer);
} else {
getStrokeVerticesFromUnclosedVerticesAA(paintInfo, tempVertices, vertexBuffer);
}
}
} else {
// For kStrokeAndFill style, the path should be adjusted externally.
// It will be treated as a fill here.
if (!paintInfo.isAA) {
getFillVerticesFromPerimeter(tempVertices, vertexBuffer);
} else {
getFillVerticesFromPerimeterAA(paintInfo, tempVertices, vertexBuffer);
}
}
}
static void expandRectToCoverVertex(SkRect& rect, const Vertex& vertex) {
rect.fLeft = fminf(rect.fLeft, vertex.position[0]);
rect.fTop = fminf(rect.fTop, vertex.position[1]);
rect.fRight = fmaxf(rect.fRight, vertex.position[0]);
rect.fBottom = fmaxf(rect.fBottom, vertex.position[1]);
}
void PathTessellator::tessellateLines(const float* points, int count, SkPaint* paint,
const mat4* transform, SkRect& bounds, VertexBuffer& vertexBuffer) {
ATRACE_CALL();
const PaintInfo paintInfo(paint, transform);
const int extra = paintInfo.capExtraDivisions();
int numLines = count / 4;
int lineAllocSize;
// pre-allocate space for lines in the buffer, and degenerate tris in between
if (paintInfo.isAA) {
lineAllocSize = 6 * (2) + 2 + 6 * extra;
vertexBuffer.alloc<AlphaVertex>(numLines * lineAllocSize + (numLines - 1) * 2);
} else {
lineAllocSize = 2 * ((2) + extra);
vertexBuffer.alloc<Vertex>(numLines * lineAllocSize + (numLines - 1) * 2);
}
Vector<Vertex> tempVertices;
tempVertices.push();
tempVertices.push();
Vertex* tempVerticesData = tempVertices.editArray();
bounds.set(points[0], points[1], points[0], points[1]);
for (int i = 0; i < count; i += 4) {
Vertex::set(&(tempVerticesData[0]), points[i + 0], points[i + 1]);
Vertex::set(&(tempVerticesData[1]), points[i + 2], points[i + 3]);
if (paintInfo.isAA) {
getStrokeVerticesFromUnclosedVerticesAA(paintInfo, tempVertices, vertexBuffer);
} else {
getStrokeVerticesFromUnclosedVertices(paintInfo, tempVertices, vertexBuffer);
}
// calculate bounds
expandRectToCoverVertex(bounds, tempVerticesData[0]);
expandRectToCoverVertex(bounds, tempVerticesData[1]);
}
expandBoundsForStroke(bounds, paint, true); // force-expand bounds to incorporate stroke
// since multiple objects tessellated into buffer, separate them with degen tris
if (paintInfo.isAA) {
vertexBuffer.createDegenerateSeparators<AlphaVertex>(lineAllocSize);
} else {
vertexBuffer.createDegenerateSeparators<Vertex>(lineAllocSize);
}
}
///////////////////////////////////////////////////////////////////////////////
// Simple path line approximation
///////////////////////////////////////////////////////////////////////////////
void pushToVector(Vector<Vertex>& vertices, float x, float y) {
// TODO: make this not yuck
vertices.push();
Vertex* newVertex = &(vertices.editArray()[vertices.size() - 1]);
Vertex::set(newVertex, x, y);
}
bool PathTessellator::approximatePathOutlineVertices(const SkPath& path, bool forceClose,
float sqrInvScaleX, float sqrInvScaleY, Vector<Vertex>& outputVertices) {
ATRACE_CALL();
// TODO: to support joins other than sharp miter, join vertices should be labelled in the
// perimeter, or resolved into more vertices. Reconsider forceClose-ing in that case.
SkPath::Iter iter(path, forceClose);
SkPoint pts[4];
SkPath::Verb v;
while (SkPath::kDone_Verb != (v = iter.next(pts))) {
switch (v) {
case SkPath::kMove_Verb:
pushToVector(outputVertices, pts[0].x(), pts[0].y());
ALOGV("Move to pos %f %f", pts[0].x(), pts[0].y());
break;
case SkPath::kClose_Verb:
ALOGV("Close at pos %f %f", pts[0].x(), pts[0].y());
break;
case SkPath::kLine_Verb:
ALOGV("kLine_Verb %f %f -> %f %f", pts[0].x(), pts[0].y(), pts[1].x(), pts[1].y());
pushToVector(outputVertices, pts[1].x(), pts[1].y());
break;
case SkPath::kQuad_Verb:
ALOGV("kQuad_Verb");
recursiveQuadraticBezierVertices(
pts[0].x(), pts[0].y(),
pts[2].x(), pts[2].y(),
pts[1].x(), pts[1].y(),
sqrInvScaleX, sqrInvScaleY, outputVertices);
break;
case SkPath::kCubic_Verb:
ALOGV("kCubic_Verb");
recursiveCubicBezierVertices(
pts[0].x(), pts[0].y(),
pts[1].x(), pts[1].y(),
pts[3].x(), pts[3].y(),
pts[2].x(), pts[2].y(),
sqrInvScaleX, sqrInvScaleY, outputVertices);
break;
default:
break;
}
}
int size = outputVertices.size();
if (size >= 2 && outputVertices[0].position[0] == outputVertices[size - 1].position[0] &&
outputVertices[0].position[1] == outputVertices[size - 1].position[1]) {
outputVertices.pop();
return true;
}
return false;
}
///////////////////////////////////////////////////////////////////////////////
// Bezier approximation
///////////////////////////////////////////////////////////////////////////////
void PathTessellator::recursiveCubicBezierVertices(
float p1x, float p1y, float c1x, float c1y,
float p2x, float p2y, float c2x, float c2y,
float sqrInvScaleX, float sqrInvScaleY, Vector<Vertex>& outputVertices) {
float dx = p2x - p1x;
float dy = p2y - p1y;
float d1 = fabs((c1x - p2x) * dy - (c1y - p2y) * dx);
float d2 = fabs((c2x - p2x) * dy - (c2y - p2y) * dx);
float d = d1 + d2;
// multiplying by sqrInvScaleY/X equivalent to multiplying in dimensional scale factors
if (d * d < THRESHOLD * THRESHOLD * (dx * dx * sqrInvScaleY + dy * dy * sqrInvScaleX)) {
// below thresh, draw line by adding endpoint
pushToVector(outputVertices, p2x, p2y);
} else {
float p1c1x = (p1x + c1x) * 0.5f;
float p1c1y = (p1y + c1y) * 0.5f;
float p2c2x = (p2x + c2x) * 0.5f;
float p2c2y = (p2y + c2y) * 0.5f;
float c1c2x = (c1x + c2x) * 0.5f;
float c1c2y = (c1y + c2y) * 0.5f;
float p1c1c2x = (p1c1x + c1c2x) * 0.5f;
float p1c1c2y = (p1c1y + c1c2y) * 0.5f;
float p2c1c2x = (p2c2x + c1c2x) * 0.5f;
float p2c1c2y = (p2c2y + c1c2y) * 0.5f;
float mx = (p1c1c2x + p2c1c2x) * 0.5f;
float my = (p1c1c2y + p2c1c2y) * 0.5f;
recursiveCubicBezierVertices(
p1x, p1y, p1c1x, p1c1y,
mx, my, p1c1c2x, p1c1c2y,
sqrInvScaleX, sqrInvScaleY, outputVertices);
recursiveCubicBezierVertices(
mx, my, p2c1c2x, p2c1c2y,
p2x, p2y, p2c2x, p2c2y,
sqrInvScaleX, sqrInvScaleY, outputVertices);
}
}
void PathTessellator::recursiveQuadraticBezierVertices(
float ax, float ay,
float bx, float by,
float cx, float cy,
float sqrInvScaleX, float sqrInvScaleY, Vector<Vertex>& outputVertices) {
float dx = bx - ax;
float dy = by - ay;
float d = (cx - bx) * dy - (cy - by) * dx;
if (d * d < THRESHOLD * THRESHOLD * (dx * dx * sqrInvScaleY + dy * dy * sqrInvScaleX)) {
// below thresh, draw line by adding endpoint
pushToVector(outputVertices, bx, by);
} else {
float acx = (ax + cx) * 0.5f;
float bcx = (bx + cx) * 0.5f;
float acy = (ay + cy) * 0.5f;
float bcy = (by + cy) * 0.5f;
// midpoint
float mx = (acx + bcx) * 0.5f;
float my = (acy + bcy) * 0.5f;
recursiveQuadraticBezierVertices(ax, ay, mx, my, acx, acy,
sqrInvScaleX, sqrInvScaleY, outputVertices);
recursiveQuadraticBezierVertices(mx, my, bx, by, bcx, bcy,
sqrInvScaleX, sqrInvScaleY, outputVertices);
}
}
}; // namespace uirenderer
}; // namespace android

View File

@@ -14,43 +14,65 @@
* limitations under the License.
*/
#ifndef ANDROID_HWUI_PATH_RENDERER_H
#define ANDROID_HWUI_PATH_RENDERER_H
#ifndef ANDROID_HWUI_PATH_TESSELLATOR_H
#define ANDROID_HWUI_PATH_TESSELLATOR_H
#include <utils/Vector.h>
#include "Matrix.h"
#include "Rect.h"
#include "Vertex.h"
namespace android {
namespace uirenderer {
class Matrix4;
typedef Matrix4 mat4;
class VertexBuffer {
public:
VertexBuffer():
mBuffer(0),
mSize(0),
mCleanupMethod(0)
mCleanupMethod(NULL)
{}
~VertexBuffer() {
if (mCleanupMethod)
mCleanupMethod(mBuffer);
if (mCleanupMethod) mCleanupMethod(mBuffer);
}
/**
This should be the only method used by the PathTessellator. Subsequent calls to alloc will
allocate space within the first allocation (useful if you want to eventually allocate
multiple regions within a single VertexBuffer, such as with PathTessellator::tesselateLines()
*/
template <class TYPE>
TYPE* alloc(int size) {
if (mSize) {
TYPE* reallocBuffer = (TYPE*)mReallocBuffer;
// already have allocated the buffer, re-allocate space within
if (mReallocBuffer != mBuffer) {
// not first re-allocation, leave space for degenerate triangles to separate strips
reallocBuffer += 2;
}
mReallocBuffer = reallocBuffer + size;
return reallocBuffer;
}
mSize = size;
mBuffer = (void*)new TYPE[size];
mReallocBuffer = mBuffer = (void*)new TYPE[size];
mCleanupMethod = &(cleanup<TYPE>);
return (TYPE*)mBuffer;
}
void* getBuffer() { return mBuffer; }
unsigned int getSize() { return mSize; }
void* getBuffer() const { return mBuffer; }
unsigned int getSize() const { return mSize; }
template <class TYPE>
void createDegenerateSeparators(int allocSize) {
TYPE* end = (TYPE*)mBuffer + mSize;
for (TYPE* degen = (TYPE*)mBuffer + allocSize; degen < end; degen += 2 + allocSize) {
memcpy(degen, degen - 1, sizeof(TYPE));
memcpy(degen + 1, degen + 2, sizeof(TYPE));
}
}
private:
template <class TYPE>
@@ -60,18 +82,24 @@ private:
void* mBuffer;
unsigned int mSize;
void* mReallocBuffer; // used for multi-allocation
void (*mCleanupMethod)(void*);
};
class PathRenderer {
class PathTessellator {
public:
static SkRect computePathBounds(const SkPath& path, const SkPaint* paint);
static void expandBoundsForStroke(SkRect& bounds, const SkPaint* paint, bool forceExpand);
static void convexPathVertices(const SkPath& path, const SkPaint* paint,
static void tessellatePath(const SkPath& path, const SkPaint* paint,
const mat4 *transform, VertexBuffer& vertexBuffer);
static void tessellateLines(const float* points, int count, SkPaint* paint,
const mat4* transform, SkRect& bounds, VertexBuffer& vertexBuffer);
private:
static bool convexPathPerimeterVertices(const SkPath &path, bool forceClose,
static bool approximatePathOutlineVertices(const SkPath &path, bool forceClose,
float sqrInvScaleX, float sqrInvScaleY, Vector<Vertex> &outputVertices);
/*
@@ -101,4 +129,4 @@ private:
}; // namespace uirenderer
}; // namespace android
#endif // ANDROID_HWUI_PATH_RENDERER_H
#endif // ANDROID_HWUI_PATH_TESSELLATOR_H

View File

@@ -81,8 +81,6 @@ namespace uirenderer {
#define PROGRAM_IS_SIMPLE_GRADIENT 41
#define PROGRAM_IS_VERTEX_SHAPE_SHIFT 42
///////////////////////////////////////////////////////////////////////////////
// Types
///////////////////////////////////////////////////////////////////////////////
@@ -129,8 +127,7 @@ struct ProgramDescription {
bool hasBitmap;
bool isBitmapNpot;
bool isAA;
bool isVertexShape;
bool isAA; // drawing with a per-vertex alpha
bool hasGradient;
Gradient gradientType;
@@ -168,7 +165,6 @@ struct ProgramDescription {
hasTextureTransform = false;
isAA = false;
isVertexShape = false;
modulate = false;
@@ -263,7 +259,6 @@ struct ProgramDescription {
if (hasTextureTransform) key |= programid(0x1) << PROGRAM_HAS_TEXTURE_TRANSFORM_SHIFT;
if (hasGammaCorrection) key |= programid(0x1) << PROGRAM_HAS_GAMMA_CORRECTION;
if (isSimpleGradient) key |= programid(0x1) << PROGRAM_IS_SIMPLE_GRADIENT;
if (isVertexShape) key |= programid(0x1) << PROGRAM_IS_VERTEX_SHAPE_SHIFT;
return key;
}

View File

@@ -40,9 +40,6 @@ const char* gVS_Header_Attributes =
"attribute vec4 position;\n";
const char* gVS_Header_Attributes_TexCoords =
"attribute vec2 texCoords;\n";
const char* gVS_Header_Attributes_AALineParameters =
"attribute float vtxWidth;\n"
"attribute float vtxLength;\n";
const char* gVS_Header_Attributes_AAVertexShapeParameters =
"attribute float vtxAlpha;\n";
const char* gVS_Header_Uniforms_TextureTransform =
@@ -68,9 +65,6 @@ const char* gVS_Header_Uniforms_HasBitmap =
"uniform mediump vec2 textureDimension;\n";
const char* gVS_Header_Varyings_HasTexture =
"varying vec2 outTexCoords;\n";
const char* gVS_Header_Varyings_IsAALine =
"varying float widthProportion;\n"
"varying float lengthProportion;\n";
const char* gVS_Header_Varyings_IsAAVertexShape =
"varying float alpha;\n";
const char* gVS_Header_Varyings_HasBitmap =
@@ -129,9 +123,6 @@ const char* gVS_Main_Position =
" gl_Position = projection * transform * position;\n";
const char* gVS_Main_PointSize =
" gl_PointSize = pointSize;\n";
const char* gVS_Main_AALine =
" widthProportion = vtxWidth;\n"
" lengthProportion = vtxLength;\n";
const char* gVS_Main_AAVertexShape =
" alpha = vtxAlpha;\n";
const char* gVS_Footer =
@@ -149,9 +140,6 @@ const char* gFS_Header =
"precision mediump float;\n\n";
const char* gFS_Uniforms_Color =
"uniform vec4 color;\n";
const char* gFS_Uniforms_AALine =
"uniform float boundaryWidth;\n"
"uniform float boundaryLength;\n";
const char* gFS_Header_Uniforms_PointHasBitmap =
"uniform vec2 textureDimension;\n"
"uniform float pointSize;\n";
@@ -259,9 +247,6 @@ const char* gFS_Main_FetchColor =
" fragColor = color;\n";
const char* gFS_Main_ModulateColor =
" fragColor *= color.a;\n";
const char* gFS_Main_AccountForAALine =
" fragColor *= (1.0 - smoothstep(boundaryWidth, 0.5, abs(0.5 - widthProportion)))\n"
" * (1.0 - smoothstep(boundaryLength, 0.5, abs(0.5 - lengthProportion)));\n";
const char* gFS_Main_AccountForAAVertexShape =
" fragColor *= alpha;\n";
@@ -472,11 +457,7 @@ String8 ProgramCache::generateVertexShader(const ProgramDescription& description
shader.append(gVS_Header_Attributes_TexCoords);
}
if (description.isAA) {
if (description.isVertexShape) {
shader.append(gVS_Header_Attributes_AAVertexShapeParameters);
} else {
shader.append(gVS_Header_Attributes_AALineParameters);
}
shader.append(gVS_Header_Attributes_AAVertexShapeParameters);
}
// Uniforms
shader.append(gVS_Header_Uniforms);
@@ -497,11 +478,7 @@ String8 ProgramCache::generateVertexShader(const ProgramDescription& description
shader.append(gVS_Header_Varyings_HasTexture);
}
if (description.isAA) {
if (description.isVertexShape) {
shader.append(gVS_Header_Varyings_IsAAVertexShape);
} else {
shader.append(gVS_Header_Varyings_IsAALine);
}
shader.append(gVS_Header_Varyings_IsAAVertexShape);
}
if (description.hasGradient) {
shader.append(gVS_Header_Varyings_HasGradient[gradientIndex(description)]);
@@ -520,11 +497,7 @@ String8 ProgramCache::generateVertexShader(const ProgramDescription& description
shader.append(gVS_Main_OutTexCoords);
}
if (description.isAA) {
if (description.isVertexShape) {
shader.append(gVS_Main_AAVertexShape);
} else {
shader.append(gVS_Main_AALine);
}
shader.append(gVS_Main_AAVertexShape);
}
if (description.hasBitmap) {
shader.append(description.isPoint ?
@@ -574,11 +547,7 @@ String8 ProgramCache::generateFragmentShader(const ProgramDescription& descripti
shader.append(gVS_Header_Varyings_HasTexture);
}
if (description.isAA) {
if (description.isVertexShape) {
shader.append(gVS_Header_Varyings_IsAAVertexShape);
} else {
shader.append(gVS_Header_Varyings_IsAALine);
}
shader.append(gVS_Header_Varyings_IsAAVertexShape);
}
if (description.hasGradient) {
shader.append(gVS_Header_Varyings_HasGradient[gradientIndex(description)]);
@@ -603,9 +572,6 @@ String8 ProgramCache::generateFragmentShader(const ProgramDescription& descripti
} else if (description.hasExternalTexture) {
shader.append(gFS_Uniforms_ExternalTextureSampler);
}
if (description.isAA && !description.isVertexShape) {
shader.append(gFS_Uniforms_AALine);
}
if (description.hasGradient) {
shader.append(gFS_Uniforms_GradientSampler[gradientIndex(description)]);
}
@@ -618,8 +584,7 @@ String8 ProgramCache::generateFragmentShader(const ProgramDescription& descripti
// Optimization for common cases
if (!description.isAA && !blendFramebuffer &&
description.colorOp == ProgramDescription::kColorNone &&
!description.isPoint && !description.isVertexShape) {
description.colorOp == ProgramDescription::kColorNone && !description.isPoint) {
bool fast = false;
const bool noShader = !description.hasGradient && !description.hasBitmap;
@@ -754,11 +719,7 @@ String8 ProgramCache::generateFragmentShader(const ProgramDescription& descripti
shader.append(gFS_Main_ApplyColorOp[description.colorOp]);
if (description.isAA) {
if (description.isVertexShape) {
shader.append(gFS_Main_AccountForAAVertexShape);
} else {
shader.append(gFS_Main_AccountForAALine);
}
shader.append(gFS_Main_AccountForAAVertexShape);
}
// Output the fragment

View File

@@ -68,25 +68,6 @@ struct AlphaVertex : Vertex {
}
}; // struct AlphaVertex
/**
* Simple structure to describe a vertex with a position and an alpha value.
*/
struct AAVertex : Vertex {
float width;
float length;
static inline void set(AAVertex* vertex, float x, float y, float width, float length) {
Vertex::set(vertex, x, y);
vertex[0].width = width;
vertex[0].length = length;
}
static inline void setColor(AAVertex* vertex, float width, float length) {
vertex[0].width = width;
vertex[0].length = length;
}
}; // struct AlphaVertex
}; // namespace uirenderer
}; // namespace android

View File

@@ -122,6 +122,12 @@ public abstract class DisplayModifier {
paint.setStrokeWidth(5);
}
});
put("30", new DisplayModifier() {
@Override
public void modifyDrawing(Paint paint, Canvas canvas) {
paint.setStrokeWidth(30);
}
});
}
});
put("strokeCap", new LinkedHashMap<String, DisplayModifier>() {