blob: 4eeb448d82ac86b5108a51f6e635a1ba6a22e460 [file] [log] [blame]
/*
* Copyright 2022 Google LLC
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "src/gpu/graphite/render/AnalyticRRectRenderStep.h"
#include "src/core/SkRRectPriv.h"
#include "src/gpu/graphite/DrawParams.h"
#include "src/gpu/graphite/DrawWriter.h"
#include "src/gpu/graphite/render/CommonDepthStencilSettings.h"
// This RenderStep is flexible and can draw filled rectangles, filled quadrilaterals with per-edge
// AA, filled rounded rectangles with arbitrary corner radii, stroked rectangles with any join,
// and stroked rounded rectangles with circular corners (each corner can be different or square).
// We combine all of these together to maximize batching across simple geometric draws and reduce
// the number pipeline specializations. Additionally, these primitives are the most common
// operations and help us avoid triggering MSAA.
//
// Each of these "primitives" is represented by a single instance. The instance attributes are
// flexible enough to describe any of the above shapes without relying on uniforms to define its
// operation. The attributes encode shape as follows:
//
// float4 xRadiiOrFlags - if any components is > 0, the instance represents a filled round rect
// with elliptical corners and these values specify the X radii in top-left CW order.
// Otherwise, if .x < -1, the instance represents a stroked [round] rect and .y holds the
// stroke radius and .z holds the join limit (matching StrokeStyle's conventions).
// Else it's a filled quadrilateral with per-edge AA defined by each component: aa != 0.
// float4 radiiOrQuadXs - if in filled round rect mode, these values provide the Y radii in
// top-left CW order. If in stroked [round] rect mode, these values provide the circular
// corner radii (same order). Otherwise, when in per-edge quad mode, these values provide
// the X coordinates of the quadrilateral (same order).
// float4 ltrbOrQuadYs - if in filled round rect mode or stroked [round] rect mode, these values
// define the LTRB edge coordinates of the rectangle surrounding the round rect (or the
// rect itself when the radii are 0s). Otherwise, in per-edge quad mode, these values provide
// the Y coordinates of the quadrilateral.
//
// From the other direction, shapes produce instance values like:
// - filled rect: [-1 -1 -1 -1] [L R R L] [T T B B]
// - stroked rect: [-2 stroke join 0] [0 0 0 0] [L T R B]
// - filled rrect: [xRadii(tl,tr,br,bl)] [yRadii(tl,tr,br,bl)] [L T R B]
// - stroked rrect: [-2 stroke join 0] [radii(tl,tr,br,bl)] [L T R B]
// - per-edge quad: [aa(tl,tr,br,bl) ? -1 : 0] [xs(tl,tr,br,bl)] [ys(tl,tr,br,bl)]
//
// This encoding relies on the fact that a valid SkRRect with all x radii equal to 0 must have
// y radii equal to 0 (so it's a rectangle and we can treat it as a quadrilateral with
// all edges AA'ed). This avoids other encodings' inability to represent a quad with all edges
// anti-aliased (e.g. checking for negatives in xRadiiOrFlags to turn on per-edge mode).
//
// From this encoding, data can be unpacked for each corner, which are equivalent under
// rotational symmetry. A corner can have an outer curve, be mitered, or be beveled. It can
// have an inner curve, an inner miter, or fill the interior. Per-edge quads are always mitered
// and fill the interior, but the vertices are placed such that the edge coverage ramps can
// collapse to 0 area on non-AA edges.
//
// The vertices that describe each corner are placed so that edges, miters, and bevels calculate
// coverage by interpolating a varying and then clamping in the fragment shader. Triangles that
// cover the inner and outer curves calculate distance to the curve within the fragment shader.
//
// See https://docs.google.com/presentation/d/1MCPstNsSlDBhR8CrsJo0r-cZNbu-sEJEvU9W94GOJoY/edit?usp=sharing
// for diagrams and explanation of how the geometry is defined.
//
// AnalyticRRectRenderStep uses the common technique of approximating distance to the level set by
// one expansion of the Taylor's series for the level set's equation. Given a level set function
// C(x,y), this amounts to calculating C(px,py)/|∇C(px,py)|. For a round-rect's linear edges, C is
// linear and the gradient is a constant, so can be computed in the vertex shader and interpolated
// exactly. For the curved corners, C's inputs are interpolated and then evaluated in the fragment
// shader, but the gradient is linear and can be interpolated exactly as well. A separate varying
// is used so that the more expensive non-linear equations are only computed on triangles that cover
// the curved corners.
//
// However, for both linear and curved corners, C is much easier to define in a local space instead
// of the pixel-space required for final anti-aliasing. (px,py) is the projected point of (u,v)
// transformed by a 4x4 matrix: [x] [m00 m01 * m03] [u]
// [x(u,v) / w(u,v)] [y] [m10 m11 * m13]X[v]
// (px,py) = [y(u,v) / w(u,v)] where [*] = [ * * * * ] [0] = M*(u,v,0,1)
// [w] [m30 m31 * m33] [1]
//
// C(px,py) can be defined in terms of a local Cl(u,v) as C(px,py) = Cl(p^-1(px,py)), where p^-1 =
// [x'] [m00' m01' * m03'] [px]
// [x'(px,py) / w'(px,py)] [y'] [m10' m11' * m13'] [py]
// (u,v) = [y'(px,py) / w'(px,py)] where [* ] = [ * * * * ]X[ 0] = M^-1*(px,py,0,1)
// [w'] [m30' m31' * m33'] [ 1]
//
// Using the chain rule, then ∇C(px,py) [m00' m01']
// = ∇Cl(u,v)X[1/w'(px,py) 0 0 -x'(px,py)/w'(px,py)^2]X[m10' m11']
// [ 0 1/w'(px,py) 0 -y'(px,py)/w'(px,py)^2] [ * * ]
// [m30' m31']
//
// [m00' m01']
// = 1/w'(px,py)*∇Cl(u,v)X[1 0 0 -x'(px,py)/w'(px,py)]X[m10' m11']
// [0 1 0 -y'(px,py)/w'(px,py)] [ * * ]
// [m30' m31']
// [m00' m01']
// = w(u,v)*∇Cl(u,v)X[1 0 0 -u]X[m10' m11']
// [0 1 0 -v] [ * * ]
// [m30' m31']
//
// = w(u,v)*∇Cl(u,v)X[m00'-m30'u m01'-m31'u]
// [m10'-m30'v m11'-m31'v]
//
// For AnalyticRRectRenderStep, the "local" space used for these calculations is the normalized
// position scaled by the corner radii. This provides a stable local space for all vertices within a
// corner (which is not the case for the original normalized space and strokes, since the inner and
// outer curves differ). The main impact of this is that the M and M^-1 used in the above derivation
// are not the exact local-to-device matrix of the draw, but includes an additional basis
// adjustment in the form of A = [x0 y0 0 tx] so M^-1=A^-1x(L2D)^-1.
// [x1 y1 0 ty]
// [0 0 1 0]
// [0 0 0 1]
//
// A is different for each corner. This makes interpolating the Jacobian and (u,v) over the entire
// mesh a little more complex. Luckily, per-pixel coverage calculations can be contained entirely
// to triangles assigned to each corner, so within their area, the interpolated values are correct.
// All the triangles connecting adjacent corners are always linear edges so their coverage can
// be calculated by computing C/|∇C| in the vertex shader instead of the fragment shader. This
// final edge distance is stable across changes to A so as long as we carry another control
// attribute to identify whether or not a triangle uses the linear edge distance or the varying
// Jacobian and (u,v), it will all work out. To assist with this, though, and to avoid branching,
// the Jacobian, edge distance, and (u,v) coordinates are calculated for every vertex since it
// could belong to both linear and per-pixel coverage triangles.
//
// Within each corner, however, we do need to evaluate up to two circle equations: one with
// radius = corner-radius(r)+stroke-radius(s), and another with radius = r-s. This can be
// consolidated into a common evaluation against a circle of radius sqrt(r^2+s^2) as follows:
//
// (x/(r+/-s))^2 + (y/(r+/-s))^2 = 1
// x^2 + y^2 = (r+/-s)^2
// x^2 + y^2 = r^2 + s^2 +/- 2rs
// (x/sqrt(r^2+s^2))^2 + (y/sqrt(r^2+s^2)) = 1 +/- 2rs/(r^2+s^2)
//
// We pass r/sqrt(r^2+s^2) and s/sqrt(r^2+s^2) down as two varyings and then add and subtract
// their product to get coverage values for the inner and outer circles of the stroked corner.
// However, if their difference is negative, this is consistent with (r-s) < 0 and we have a
// collapsed inner corner so only the outer coverage should be used. This happens automatically
// for strokes with small corners or for rectangular corners (r=0) with round joins. For fills
// and hairlines, the normal s=0 so as expected 2rs is 0 and we're effectively testing a single
// circle curve. However, we have need to differentiate the two cases by setting r=0,s=1 for
// hairlines and r=1,s=0 for filled round rects.
namespace skgpu::graphite {
static skvx::float4 load_x_radii(const SkRRect& rrect) {
// We swizzle X and Y radii for the anti-diagonal corners to match overall winding of the rrect
// when processed as vertices on the GPU.
return skvx::float4{rrect.radii(SkRRect::kUpperLeft_Corner).fX,
rrect.radii(SkRRect::kUpperRight_Corner).fY,
rrect.radii(SkRRect::kLowerRight_Corner).fX,
rrect.radii(SkRRect::kLowerLeft_Corner).fY};
}
static skvx::float4 load_y_radii(const SkRRect& rrect) {
return skvx::float4{rrect.radii(SkRRect::kUpperLeft_Corner).fY,
rrect.radii(SkRRect::kUpperRight_Corner).fX,
rrect.radii(SkRRect::kLowerRight_Corner).fY,
rrect.radii(SkRRect::kLowerLeft_Corner).fX};
}
static float local_aa_radius(const Transform& t, const SkV2& p) {
// TODO: This should be the logic for Transform::scaleFactor()
// [m00 m01 * m03] [f(u,v)]
// Assuming M = [m10 m11 * m13], define the projected p'(u,v) = [g(u,v)] where
// [ * * * * ]
// [m30 m31 * m33]
// [x] [u]
// f(u,v) = x(u,v) / w(u,v), g(u,v) = y(u,v) / w(u,v) and [y] = M*[v]
// [*] = [0]
// [w] [1]
//
// x(u,v) = m00*u + m01*v + m03
// y(u,v) = m10*u + m11*v + m13
// w(u,v) = m30*u + m31*v + m33
//
// dx/du = m00, dx/dv = m01,
// dy/du = m10, dy/dv = m11
// dw/du = m30, dw/dv = m31
//
// df/du = (dx/du*w - x*dw/du)/w^2 = (m00*w - m30*x)/w^2 = (m00 - m30*f)/w
// df/dv = (dx/dv*w - x*dw/dv)/w^2 = (m01*w - m31*x)/w^2 = (m01 - m31*f)/w
// dg/du = (dy/du*w - y*dw/du)/w^2 = (m10*w - m30*y)/w^2 = (m10 - m30*g)/w
// dg/dv = (dy/dv*w - y*dw/du)/w^2 = (m11*w - m31*y)/w^2 = (m11 - m31*g)/w
//
// Singular values of [df/du df/dv] define perspective correct minimum and maximum scale factors
// [dg/du dg/dv]
// for M evaluated at (u,v)
const SkM44& matrix = t.matrix();
SkV4 devP = matrix.map(p.x, p.y, 0.f, 1.f);
const float dxdu = matrix.rc(0,0);
const float dxdv = matrix.rc(0,1);
const float dydu = matrix.rc(1,0);
const float dydv = matrix.rc(1,1);
const float dwdu = matrix.rc(3,0);
const float dwdv = matrix.rc(3,1);
float invW2 = sk_ieee_float_divide(1.f, (devP.w * devP.w));
// non-persp has invW2 = 1, devP.w = 1, dwdu = 0, dwdv = 0
float dfdu = (devP.w*dxdu - devP.x*dwdu) * invW2; // non-persp -> dxdu -> m00
float dfdv = (devP.w*dxdv - devP.x*dwdv) * invW2; // non-persp -> dxdv -> m01
float dgdu = (devP.w*dydu - devP.y*dwdu) * invW2; // non-persp -> dydu -> m10
float dgdv = (devP.w*dydv - devP.y*dwdv) * invW2; // non-persp -> dydv -> m11
// no-persp, these are the singular values of [m00,m01][m10,m11], which is just the upper 2x2
// and equivalent to SkMatrix::getMinmaxScales().
float s1 = dfdu*dfdu + dfdv*dfdv + dgdu*dgdu + dgdv*dgdv;
float e = dfdu*dfdu + dfdv*dfdv - dgdu*dgdu - dgdv*dgdv;
float f = dfdu*dgdu + dfdv*dgdv;
float s2 = SkScalarSqrt(e*e + 4*f*f);
float singular1 = SkScalarSqrt(0.5f * (s1 + s2));
float singular2 = SkScalarSqrt(0.5f * (s1 - s2));
// singular1 and 2 represent the minimum and maximum scale factors at that transformed point.
// Moving 1 from 'p' before transforming will move at least minimum and at most maximum from
// the transformed point. Thus moving between [1/max, 1/min] pre-transformation means post
// transformation moves between [1,max/min] so using 1/min as the local AA radius ensures that
// the post-transformed point is at least 1px away from the original.
float aaRadius = sk_ieee_float_divide(1.f, std::min(singular1, singular2));
if (sk_float_isfinite(aaRadius)) {
return aaRadius;
} else {
// Treat NaNs and infinities as +inf, which will always trigger the inset self-intersection
// logic that snaps inner vertices to the center instead of insetting by the local AA radius
return SK_FloatInfinity;
}
}
static float local_aa_radius(const Transform& t, const Rect& bounds) {
// Use the maximum radius of the 4 corners so that every local vertex uses the same offset
// even if it's more conservative on some corners (when the min/max scale isn't constant due
// to perspective).
if (t.type() < Transform::Type::kProjection) {
// Scale factors are constant, so the point doesn't really matter
return local_aa_radius(t, SkV2{0.f, 0.f});
} else {
// TODO can we share calculation here?
float tl = local_aa_radius(t, SkV2{bounds.left(), bounds.top()});
float tr = local_aa_radius(t, SkV2{bounds.right(), bounds.top()});
float br = local_aa_radius(t, SkV2{bounds.right(), bounds.bot()});
float bl = local_aa_radius(t, SkV2{bounds.left(), bounds.bot()});
return std::max(std::max(tl, tr), std::max(bl, br));
}
}
static bool corner_insets_intersect(const SkRRect& rrect, float strokeRadius, float aaRadius) {
// One AA inset per side
const float maxInset = strokeRadius + 2.f * aaRadius;
return // Horizontal insets would intersect opposite corner's curve
maxInset >= rrect.width() - rrect.radii(SkRRect::kLowerLeft_Corner).fX ||
maxInset >= rrect.width() - rrect.radii(SkRRect::kLowerRight_Corner).fX ||
maxInset >= rrect.width() - rrect.radii(SkRRect::kUpperLeft_Corner).fX ||
maxInset >= rrect.width() - rrect.radii(SkRRect::kUpperRight_Corner).fX ||
// Vertical insets would intersect opposite corner's curve
maxInset >= rrect.height() - rrect.radii(SkRRect::kLowerLeft_Corner).fY ||
maxInset >= rrect.height() - rrect.radii(SkRRect::kLowerRight_Corner).fY ||
maxInset >= rrect.height() - rrect.radii(SkRRect::kUpperLeft_Corner).fY ||
maxInset >= rrect.height() - rrect.radii(SkRRect::kUpperRight_Corner).fY;
}
// Represents the per-vertex attributes used in each instance.
struct Vertex {
SkV2 fPosition;
SkV2 fNormal;
float fNormalScale;
float fMirrorOffset;
float fCenterWeight;
};
// Allowed values for the center weight instance value (selected at record time based on style
// and transform), and are defined such that when (insance-weight > vertex-weight) is true, the
// vertex should be snapped to the center instead of its regular calculation.
static constexpr float kSolidInterior = 1.f;
static constexpr float kStrokeInterior = 0.f;
static constexpr float kFilledStrokeInterior = -1.f;
// Special value for local AA radius to signal when the self-intersections of a stroke interior
// need extra calculations in the vertex shader.
static constexpr float kComplexAAInsets = -1.f;
static constexpr int kCornerVertexCount = 15; // sk_VertexID is divided by this in SkSL
static constexpr int kVertexCount = 4 * kCornerVertexCount;
static constexpr int kIndexCount = 114;
static void write_index_buffer(VertexWriter writer) {
static constexpr uint16_t kTL = 0 * kCornerVertexCount;
static constexpr uint16_t kTR = 1 * kCornerVertexCount;
static constexpr uint16_t kBR = 2 * kCornerVertexCount;
static constexpr uint16_t kBL = 3 * kCornerVertexCount;
static const uint16_t kIndices[kIndexCount] = {
// Exterior AA ramp outset
kTL+0,kTL+6,kTL+1,kTL+7,kTL+2,kTL+7,kTL+3,kTL+8,kTL+4,kTL+8,kTL+5,kTL+9,
kTR+0,kTR+6,kTR+1,kTR+7,kTR+2,kTR+7,kTR+3,kTR+8,kTR+4,kTR+8,kTR+5,kTR+9,
kBR+0,kBR+6,kBR+1,kBR+7,kBR+2,kBR+7,kBR+3,kBR+8,kBR+4,kBR+8,kBR+5,kBR+9,
kBL+0,kBL+6,kBL+1,kBL+7,kBL+2,kBL+7,kBL+3,kBL+8,kBL+4,kBL+8,kBL+5,kBL+9,
kTL+0,kTL+6, // close and jump to next strip
// Outer to inner edge triangles
kTL+6,kTL+10,kTL+7,kTL+11,kTL+8,kTL+12,kTL+9,kTL+12,
kTR+6,kTR+10,kTR+7,kTR+11,kTR+8,kTR+12,kTR+9,kTR+12,
kBR+6,kBR+10,kBR+7,kBR+11,kBR+8,kBR+12,kBR+9,kBR+12,
kBL+6,kBL+10,kBL+7,kBL+11,kBL+8,kBL+12,kBL+9,kBL+12,
kTL+6,kTL+10, // close and extra vertex to jump to next strip
// Inner inset to center of shape
kTL+10,kTL+13,kTL+11,kTL+11,kTL+14,kTL+12,kTL+14,
kTR+10,kTR+13,kTR+11,kTR+11,kTR+14,kTR+12,kTR+14,
kBR+10,kBR+13,kBR+11,kBR+11,kBR+14,kBR+12,kBR+14,
kBL+10,kBL+13,kBL+11,kBL+11,kBL+14,kBL+12,kBL+14,
kTL+10,kTL+13 // close
};
writer << kIndices;
}
static void write_vertex_buffer(VertexWriter writer) {
// Allowed values for the normal scale attribute. +1 signals a device-space outset along the
// normal away from the outer edge of the stroke. 0 signals no outset, but placed on the outer
// edge of the stroke. -1 signals a local inset along the normal from the inner edge.
static constexpr float kOutset = 1.0;
static constexpr float kInset = -1.0;
static constexpr float kMirror = 1.f; // "true" as a float
static constexpr float kCenter = 1.f; // "true" as a float
// Zero, but named this way to help call out non-zero parameters.
static constexpr float _______ = 0.f;
static constexpr float kHR2 = 0.5f * SK_FloatSqrt2; // "half root 2"
// This template is repeated 4 times in the vertex buffer, for each of the four corners.
// The vertex ID is used to determine which corner the normalized position is transformed to.
static constexpr Vertex kCornerTemplate[kCornerVertexCount] = {
// Device-space AA outsets from outer curve
{ {1.0f, 0.0f}, {1.0f, 0.0f}, kOutset, _______, _______ },
{ {1.0f, 0.0f}, {1.0f, 0.0f}, kOutset, kMirror, _______ },
{ {1.0f, 0.0f}, {kHR2, kHR2}, kOutset, kMirror, _______ },
{ {0.0f, 1.0f}, {kHR2, kHR2}, kOutset, kMirror, _______ },
{ {0.0f, 1.0f}, {0.0f, 1.0f}, kOutset, kMirror, _______ },
{ {0.0f, 1.0f}, {0.0f, 1.0f}, kOutset, _______, _______ },
// Outer anchors (no local or device-space normal outset)
{ {1.0f, 0.0f}, {1.0f, 0.0f}, _______, _______, _______ },
{ {1.0f, 0.0f}, {kHR2, kHR2}, _______, kMirror, _______ },
{ {0.0f, 1.0f}, {kHR2, kHR2}, _______, kMirror, _______ },
{ {0.0f, 1.0f}, {0.0f, 1.0f}, _______, _______, _______ },
// Inner curve (with additional AA inset in the common case)
{ {1.0f, 0.0f}, {1.0f, 0.0f}, kInset, _______, _______ },
{ {0.5f, 0.5f}, {kHR2, kHR2}, kInset, kMirror, _______ },
{ {0.0f, 1.0f}, {0.0f, 1.0f}, kInset, _______, _______ },
// Center filling vertices (equal to inner AA insets unless 'center' triggers a fill).
// TODO: On backends that support "cull" distances (and with SkSL support), these vertices
// and their corresponding triangles can be completely removed. The inset vertices can
// set their cull distance value to cause all filling triangles to be discarded or not
// depending on the instance's style.
{ {1.0f, 0.0f}, {1.0f, 0.0f}, kInset, _______, kCenter },
{ {0.0f, 1.0f}, {0.0f, 1.0f}, kInset, _______, kCenter },
};
writer << kCornerTemplate // TL
<< kCornerTemplate // TR
<< kCornerTemplate // BR
<< kCornerTemplate; // BL
}
AnalyticRRectRenderStep::AnalyticRRectRenderStep(StaticBufferManager* bufferManager)
: RenderStep("AnalyticRRectRenderStep",
"",
Flags::kPerformsShading | Flags::kEmitsCoverage,
/*uniforms=*/{},
PrimitiveType::kTriangleStrip,
kDirectDepthGreaterPass,
/*vertexAttrs=*/{
{"position", VertexAttribType::kFloat2, SkSLType::kFloat2},
{"normalAttr", VertexAttribType::kFloat2, SkSLType::kFloat2},
// FIXME these values are all +1/0/-1, or +1/0, so could be packed
// much more densely than as three floats.
{"normalScale", VertexAttribType::kFloat, SkSLType::kFloat},
{"mirrorOffset", VertexAttribType::kFloat, SkSLType::kFloat},
{"centerWeight", VertexAttribType::kFloat, SkSLType::kFloat}
},
/*instanceAttrs=*/
{{"xRadiiOrFlags", VertexAttribType::kFloat4, SkSLType::kFloat4},
{"radiiOrQuadXs", VertexAttribType::kFloat4, SkSLType::kFloat4},
{"ltrbOrQuadYs", VertexAttribType::kFloat4, SkSLType::kFloat4},
// XY stores center of rrect in local coords. Z and W store values to
// control interior fill behavior. Z can be -1, 0, or 1:
// -1: A stroked interior where AA insets overlap, but isn't solid.
// 0: A stroked interior with no complications.
// 1: A solid interior (fill or sufficiently large stroke width).
// W specifies the size of the AA inset if it's >= 0, or signals that
// the inner curves intersect in a complex manner (rare).
{"center", VertexAttribType::kFloat4, SkSLType::kFloat4},
// TODO: pack depth and ssboIndex into 32-bits
{"depth", VertexAttribType::kFloat, SkSLType::kFloat},
{"ssboIndex", VertexAttribType::kInt, SkSLType::kInt},
{"mat0", VertexAttribType::kFloat3, SkSLType::kFloat3},
{"mat1", VertexAttribType::kFloat3, SkSLType::kFloat3},
{"mat2", VertexAttribType::kFloat3, SkSLType::kFloat3},
// We need the first two columns of the inverse transform to calculate
// the normal matrix.
{"invMat0", VertexAttribType::kFloat3, SkSLType::kFloat3},
{"invMat1", VertexAttribType::kFloat3, SkSLType::kFloat3}},
/*varyings=*/{
{"jacobian", SkSLType::kFloat4}, // float2x2
{"coverageWidth", SkSLType::kFloat2},
// Either two opposing distances, so taking the min calculates coverage
// for both inner and outer edges; or one distance and an orthogonal
// width so taking the min clamps the coverage.
{"edgeDistances", SkSLType::kFloat2},
// Controls regular AA, hairline AA, or subpixel AA
{"scaleAndBias", SkSLType::kFloat2},
// TODO: With flat shading and careful control of indices for provoking
// vertex we can detect linear/circle coverage using just coverageWidth
{"perPixelControl", SkSLType::kFloat},
{"uv", SkSLType::kFloat2},
}) {
// Initialize the static buffers we'll use when recording draw calls.
// NOTE: Each instance of this RenderStep gets its own copy of the data. Since there should only
// ever be one AnalyticRRectRenderStep at a time, this shouldn't be an issue.
write_vertex_buffer(bufferManager->getVertexWriter(sizeof(Vertex) * kVertexCount,
&fVertexBuffer));
write_index_buffer(bufferManager->getIndexWriter(sizeof(uint16_t) * kIndexCount,
&fIndexBuffer));
}
AnalyticRRectRenderStep::~AnalyticRRectRenderStep() {}
std::string AnalyticRRectRenderStep::vertexSkSL() const {
// TODO: Move this into a module
return R"(
const float kMiterScale = 1.0;
const float kBevelScale = 0.0;
const float kRoundScale = 0.41421356237; // sqrt(2)-1
const float kEpsilon = 0.00024; // SK_ScalarNearlyZero
// Store a local copy of `normal`, which can get mutated below.
float2 normal = normalAttr;
int cornerID = sk_VertexID / 15; // KEEP IN SYNC WITH kCornerVertexCount
// Corner variables that are the same for all vertices in a corner, but depend on style.
float4 xs, ys; // should be TL, TR, BR, BL
float2 cornerRadii = float2(0.0);
float strokeRadius = 0.0; // fill and hairline are differentiated by center weighting
float joinScale; // the amount of mirror offseting to apply based on corner/stroke join
float2 uvScale; // Normalization for circular corners.
bool bidirectionalCoverage = center.z <= 0.0;
if (xRadiiOrFlags.x < -1.0) {
// Stroked rect or round rect
xs = ltrbOrQuadYs.LRRL;
ys = ltrbOrQuadYs.TTBB;
strokeRadius = xRadiiOrFlags.y;
cornerRadii = float2(radiiOrQuadXs[cornerID]); // strokes require circular corners
// Configure corner shape based on style, defaulting to kRoundStyle since any circular
// corner remains round regardless.
// TODO should this analysis be done on the CPU and we upload 4 joinScales instead of
// 1 join style that has to be combined with the per-corner radii?
joinScale = kRoundScale;
if (cornerRadii.x <= kEpsilon) {
// Join type only affects rectangular corners. For simplicity, hairline rect corners
// are always mitered.
if (strokeRadius == 0.0 || xRadiiOrFlags.z > 0.0) {
joinScale = kMiterScale;
} else if (xRadiiOrFlags.z == 0.0) {
joinScale = kBevelScale;
} // else remain rounded
}
// When not rounded, this will be overwritten to sensible default values later on.
uvScale = inversesqrt(cornerRadii*cornerRadii + strokeRadius*strokeRadius);
coverageWidth = float2(cornerRadii.x, strokeRadius) * uvScale;
if (!bidirectionalCoverage && coverageWidth.x > coverageWidth.y) {
// Flip the two components of coverage width so that "r"-"s" is less than 0 and the
// fragment shader will skip the inner circle distance evaluation.
coverageWidth = coverageWidth.yx;
}
} else if (any(greaterThan(xRadiiOrFlags, float4(0.0)))) {
// Filled round rect
xs = ltrbOrQuadYs.LRRL;
ys = ltrbOrQuadYs.TTBB;
cornerRadii = float2(xRadiiOrFlags[cornerID], radiiOrQuadXs[cornerID]);
if (cornerRadii.x > kEpsilon && cornerRadii.y > kEpsilon) {
joinScale = kRoundScale;
uvScale = 1.0 / cornerRadii;
// The final width is 0, but "r-s"<0 so we skip an inner circle calculation later.
coverageWidth = float2(0.0, 1.0);
} else {
// This specific corner is rectangular
joinScale = kMiterScale;
}
} else {
// Per-edge quadrilateral, so we have to calculate the corner's basis from the
// quad's edges.
xs = radiiOrQuadXs;
ys = ltrbOrQuadYs;
joinScale = kMiterScale;
}
// Instance-level state that controls how the interior is filled (or not).
// There are two rings of inner vertices differentiated by the centerWeight attribute so
// that we can control per-pixel coverage carefully when we need to fill the interior. For
// typical stroked shapes, the "center" vertices are co-located with the inner vertices.
// See note on 'center's declaration for how its components are interpreted.
bool snapToCenter = centerWeight * center.z != 0.0 ||
(center.w < 0.0 && normalScale < 0.0 &&
(strokeRadius == 0.0 || !bidirectionalCoverage));
bool complexCenter = center.w < 0.0 && strokeRadius > 0.0 && bidirectionalCoverage;
float localAARadius = center.w; // this will only be used when it's at least 0
bool isCurve = mirrorOffset != 0.0 && normalScale >= 0.0 && joinScale == kRoundScale;
bool isMidVertex = normal.x != 0.0 && normal.y != 0.0;
if (joinScale != kRoundScale) {
// Provide sensible default values for these, although the vertex structure should
// ensure they aren't used (or always multiplied with a 0 term).
cornerRadii = float2(0.0);
uvScale = float2(1.0);
coverageWidth = float2(0.0);
}
float2 corner = float2(xs.xyzw[cornerID], ys.xyzw[cornerID]);
float2 cornerCW = float2(xs.yzwx[cornerID], ys.yzwx[cornerID]);
float2 cornerCCW = float2(xs.wxyz[cornerID], ys.wxyz[cornerID]);
float w, h;
float2 xAxis = normalize_with_length(corner - cornerCW, w);
float2 yAxis = normalize_with_length(corner - cornerCCW, h);
// Additional transform from the local corner space to the standard local coordinates
float2x2 basis = float2x2(xAxis, yAxis);
float2 translate = corner - basis*cornerRadii;
float2x2 invBasis = inverse(basis);
if (isMidVertex) {
// Correct the middle normals depending on style
if (joinScale == kMiterScale) {
normal = position;
} else if (cornerRadii.y != cornerRadii.x) {
// Update normals for elliptical corners.
normal = normalize(cornerRadii.yx * normal);
}
}
float2 normalizedUV = position + (joinScale * mirrorOffset * position.yx);
float2 outerUV = (cornerRadii + strokeRadius) * normalizedUV;
float2 centerUV = invBasis * (center.xy - translate);
float shapeWidth;
float orthogonalWidth = 0.0;
if (bidirectionalCoverage) {
// The distance between the two edges, w, is interpolated from 0 to w and from w to 0
// in order to produce exterior and interior coverage ramps. This means that both inner
// and outer vertices have to calculate both positions.
float2 innerRadii = cornerRadii - strokeRadius;
float2 innerUV;
bool innerIsCurved = false;
if (complexCenter) {
// FIXME treat as a miter for now, but this will be wrong visually.
innerUV = min(innerRadii, float2(0.0));
} else if (any(lessThanEqual(innerRadii, float2(0.0)))) {
// The stroke exceeds the corner radius so the interior is mitered
innerUV = min(innerRadii, float2(0.0));
} else {
innerUV = innerRadii * normalizedUV;
innerIsCurved = true;
}
if (normalScale >= 0.0) {
// Nothing complicated for outer vertices of a stroke
uv = outerUV;
} else {
// The actual inner vertex may either be 'innerUV', have an additional AA offset,
// or be snapped to the center.
if (snapToCenter) {
uv = centerUV;
} else if (complexCenter || any(lessThanEqual(innerRadii, float2(localAARadius)))) {
if (centerWeight == 0.0) {
// The inner vertex is placed on the inner edge w/o AA inset, and turns on
// per-pixel coverage if it's rounded.
uv = innerUV;
isCurve = isMidVertex && innerIsCurved;
} else {
// The fill vertex is placed at the AA-inset miter position (assuming it
// didn't hit the snapToCenter branch first).
uv = min(innerRadii - localAARadius, float2(0.0));
}
} else {
// The inner and fill vertices are coincident and include the AA offset, which
// allows the triangles between the inner and fill vertices to be discarded,
// having zero area.
uv = innerUV - localAARadius * normal;
isCurve = isMidVertex && innerIsCurved && localAARadius == 0.0;
}
}
// Our normals point outwards, so we negate normal when calculating the outer edge's
// distance to have it increase towards the center of the shape.
edgeDistances = float2(distance_to_line(uv, -normal, outerUV),
distance_to_line(uv, normal, innerUV));
shapeWidth = 2.0 * strokeRadius;
} else {
// Only the distance to the outer edge needs to be interpolated, the second distance
// is set to the max coverage to use for subpixel filled shapes.
shapeWidth = normal.y != 0 ? h : w;
orthogonalWidth = normal.x != 0 ? h : w;
if (normalScale >= 0.0) {
// The outer vertex always has distance = 0
uv = outerUV;
edgeDistances = float2(0.0);
} else {
// The inner vertices calculate the distance to the line through the paired outer
// position with the shared normal.
const float kHR2 = 0.70710678118; // sqrt(2)/2
if (snapToCenter) {
uv = centerUV;
} else if (joinScale != kRoundScale ||
any(lessThanEqual(kHR2*cornerRadii + strokeRadius,
float2(localAARadius)))) {
// We inset bevel/miter corners if other corners have rounding, since it helps
// keep the triangles more well-formed. We test kHR2*cornerRadii instead of
// cornerRadii so that all 3 inner vertices of a corner miter at the same time.
float2 inset = cornerRadii + strokeRadius - localAARadius;
uv = min(inset, float2(0.0));
} else {
uv = outerUV - localAARadius * normal;
}
edgeDistances = float2(distance_to_line(uv, -normal, outerUV), 0.0);
}
}
// Explicitly use the center coordinates when snapping to the center so that all corner's
// fill vertices have the same bit-exact device positions.
float2 localPos = snapToCenter ? center.xy : (basis*uv + translate);
float2 relUV = invBasis * translate + uv;
float3 devPos = float3x3(mat0, mat1, mat2)*localPos.xy1;
invBasis = invBasis * float2x2(invMat0.xy, invMat1.xy);
jacobian = float4(invBasis[0], invBasis[1]);
jacobian.xy -= invMat0.z*relUV;
jacobian.zw -= invMat1.z*relUV;
// Update edge distances and width to be in device-space
float2 gradient = float2(dot(normal, jacobian.xy), dot(normal, jacobian.zw));
float invGradLength = inversesqrt(dot(gradient, gradient));
edgeDistances *= invGradLength;
shapeWidth *= invGradLength;
if (normalScale > 0.0) {
float2 nx, ny;
if (joinScale == kMiterScale && isMidVertex) {
// Produce a bisecting vector in device space (ignoring 'normal' since that was
// previously corrected to match the mitered edge normals).
nx = normalize(jacobian.xz);
ny = normalize(jacobian.yw);
if (dot(nx, ny) < -0.8) {
// Normals are in nearly opposite directions, so adjust to avoid float error.
float s = sign(cross_length_2d(nx, ny));
nx = s*ortho(nx);
ny = -s*ortho(ny);
}
} else {
// Note that when there's no perspective, the jacobian is equivalent to the normal
// matrix (inverse transpose), but produces correct results when there's perspective
// because it accounts for the position's influence on a line's projected direction.
nx = normal.x * jacobian.xz;
ny = normal.y * jacobian.yw;
}
// Adding the normal components together directly results in what we'd have
// calculated if we'd just transformed 'normal' in one go, assuming they weren't
// normalized in the if-block above. If they were normalized, the sum equals the
// bisector between the original nx and ny.
//
// We multiply by W so that after perspective division the new point is offset by the
// normal.
devPos.xy += devPos.z * normalize(nx + ny);
// By construction these points are 1px away from the outer edge, but we multiply
// by W since to get screen-space linear interpolation.
edgeDistances -= devPos.z;
}
// NOTE: the scale and bias is applied as S*(d + B) so that we can still interpolate the
// (shapeWidth/W)^2 term for subpixel correction. It's also possible to apply the scale and
// bias to 'edgeDistances' here, but then we'd have to scale+bias the curve coverage before
// combining with linear coverage and we're not saving any varyings. Given that we determine
// the scale+bias in the VS (modulo division by W) and then apply it at the end of the FS.
if (shapeWidth == 0.0) {
// Hairline, so shift by 1px so that the centerline of the geometry is full coverage.
scaleAndBias = float2(devPos.z);
} else if (shapeWidth < 1.0) {
// Subpixel, so scale and shift the coverage ramps so that a 2px region interpolates
// between 0 and shapeWidth. The vertex geometry emits triangles to cover 2+shapeWidth
// pixels so we can always hit the 2px width that ensures we don't miss pixel centers.
scaleAndBias = float2(shapeWidth, devPos.z - 0.5 * shapeWidth);
} else {
// Full coverage, so shift by 1/2px so a shapeWidth+1 region has positive coverage.
scaleAndBias = float2(devPos.z, 0.5*devPos.z);
}
if (orthogonalWidth > 0.0) {
float2 orthoGrad = float2(dot(ortho(normal), jacobian.xy),
dot(ortho(normal), jacobian.zw));
orthogonalWidth *= inversesqrt(dot(orthoGrad, orthoGrad));
// Undo scale+bias on this field so that after scaling/biasing in the FS it is exactly
// orthogonalWidth for clamping purposes.
edgeDistances.y = orthogonalWidth / scaleAndBias.x - scaleAndBias.y;
}
// Apply the uv scaling in the vertex shader since it's a constant factor across the
// triangle, which reduces what the fragment shader has to calculate.
uv *= uvScale;
jacobian *= uvScale.xyxy;
// Write out final results
stepLocalCoords = localPos;
float4 devPosition = float4(devPos.xy, devPos.z*depth, devPos.z);
perPixelControl = isCurve ? 1.0 : 0.0;
)";
}
const char* AnalyticRRectRenderStep::fragmentCoverageSkSL() const {
// TODO: Actually implement this for linear edges (that get clamp a varying to [0,1]) and for
// corners calculating distance to an ellipse.
return R"(
// We multiply by sk_FragCoord.w (really 1/w) to either adjust the distance to linear
// (for outside edge triangles), or to account for W in the length of the gradient we
// had earlier divided by.
float2 scaleAndBiasAdjusted = scaleAndBias * sk_FragCoord.w;
float2 edgeDistancesAdjusted = edgeDistances * sk_FragCoord.w;
float c;
if (perPixelControl > 0.0 && uv.x > 0.0 && uv.y > 0.0) {
// Inside a triangle that covers a quarter circle, so the coverage is non-linear.
float2 gradient = float2(dot(uv, jacobian.xy), dot(uv, jacobian.zw));
float invGradLength = 0.5 * inversesqrt(dot(gradient, gradient)) * sk_FragCoord.w;
float f = dot(uv, uv) - 1.0;
float width = 2 * coverageWidth.x * coverageWidth.y;
// We include edgeDistancesAdjusted.x in the outer curve's coverage if it's less than
// the bias because that corresponds to the linear outset that had clamped UVs, so the
// curve's implicit function isn't accurate near the outer edge.
c = min(edgeDistancesAdjusted.x, 0.0) - (f - width) * invGradLength;
if (coverageWidth.x > coverageWidth.y) {
// In the case of an interior curve, it is incorrect to incorporate
// edgeDistancesAdjusted.y since that would form an interior miter.
c = min(c, (f + width) * invGradLength);
} else {
// An interior miter or a fill that needs to clamp to the other dimension's coverage
c = min(c, edgeDistancesAdjusted.y);
}
} else {
// A fill or miter w/o any outer or inner curve to evaluate
c = min(edgeDistancesAdjusted.x, edgeDistancesAdjusted.y);
}
outputCoverage =
half4(clamp(scaleAndBiasAdjusted.x*(c + scaleAndBiasAdjusted.y), 0.0, 1.0));
)";
}
void AnalyticRRectRenderStep::writeVertices(DrawWriter* writer,
const DrawParams& params,
int ssboIndex) const {
SkASSERT(params.geometry().isShape());
const Shape& shape = params.geometry().shape();
DrawWriter::Instances instance{*writer, fVertexBuffer, fIndexBuffer, kIndexCount};
auto vw = instance.append(1);
// The bounds of a rect is the rect, and the bounds of a rrectv is tight (== SkRRect::getRect()).
Rect bounds = params.geometry().bounds();
// aaRadius will be set to a negative value to signal a complex self-intersection that has to
// be calculated in the vertex shader.
float aaRadius = local_aa_radius(params.transform(), bounds);
float centerWeight;
if (params.isStroke()) {
SkASSERT(params.strokeStyle().halfWidth() >= 0.f);
SkASSERT(shape.isRect() ||
(shape.isRRect() && SkRRectPriv::AllCornersCircular(shape.rrect())));
const float strokeRadius = params.strokeStyle().halfWidth();
skvx::float2 innerGap = bounds.size() - 2.f * params.strokeStyle().halfWidth();
if (any(innerGap <= 0.f)) {
centerWeight = kSolidInterior;
// For strokes that overlap so much the interior is solid, we move the inset vertices
// to match the "filled" cases.
if (shape.isRRect()) {
// Check if insets from the outer curve would also overlap
if (corner_insets_intersect(shape.rrect(), -strokeRadius, aaRadius)) {
aaRadius = kComplexAAInsets;
} // else VS will adjust stroke-control attribute to place at outer curve+aa inset.
} else {
// Insets for quads/filled-rects are always at the center, but treat round joins
// as a round rect instead (i.e. AA insets from join's curve if it's large enough).
if (!params.strokeStyle().isRoundJoin() || strokeRadius < aaRadius) {
aaRadius = kComplexAAInsets;
}
}
} else {
if (any(innerGap <= 2.f * aaRadius) ||
(shape.isRRect() && corner_insets_intersect(shape.rrect(), strokeRadius, aaRadius))) {
// When the insets intersect we separate the inner vertices from the center vertices
// by placing the inner vertices on the base inner curve w/o the AA inset. However,
// if the inner curves would intersect then we switch to a complex interior.
centerWeight = kFilledStrokeInterior;
if (shape.isRRect() && corner_insets_intersect(shape.rrect(), strokeRadius, 0.f)) {
aaRadius = kComplexAAInsets;
} else {
aaRadius = 0.f;
}
} else {
centerWeight = kStrokeInterior;
}
}
skvx::float4 cornerRadii;
if (shape.isRRect()) {
// X and Y radii are the same, but each corner could be different. Take X arbitrarily.
cornerRadii = load_x_radii(shape.rrect());
} else {
// All four corner radii are 0s for a rectangle
cornerRadii = 0.f;
}
// Write a negative value outside [-1, 0] to signal a stroked shape, then the style params,
// followed by corner radii and bounds.
vw << -2.f << strokeRadius << params.strokeStyle().joinLimit() << /*unused*/0.f
<< cornerRadii << bounds.ltrb();
} else {
// TODO: Add quadrilateral support to Shape with per-edge flags.
if (shape.isRect() || (shape.isRRect() && shape.rrect().isRect())) {
// Rectangles (or rectangles embedded in an SkRRect) are converted to the quadrilateral
// case, but with all edges anti-aliased (== -1).
skvx::float4 ltrb = bounds.ltrb();
vw << /*edge flags*/ skvx::float4(-1.f)
<< /*xs*/ skvx::shuffle<0,2,2,0>(ltrb)
<< /*ys*/ skvx::shuffle<1,1,3,3>(ltrb);
// For simplicity, it's assumed arbitrary quad insets could self-intersect, so force
// all interior vertices to the center.
centerWeight = kSolidInterior;
aaRadius = kComplexAAInsets;
} else {
// A filled rounded rectangle
const SkRRect& rrect = shape.rrect();
SkASSERT(any(load_x_radii(rrect) > 0.f)); // If not, the shader won't detect this case
vw << load_x_radii(rrect) << load_y_radii(rrect) << bounds.ltrb();
centerWeight = kSolidInterior;
if (corner_insets_intersect(rrect, 0.f, aaRadius)) {
aaRadius = kComplexAAInsets;
}
}
}
// All instance types share the remaining instance attribute definitions
const SkM44& m = params.transform().matrix();
const SkM44& invM = params.transform().inverse();
vw << bounds.center() << centerWeight << aaRadius
<< params.order().depthAsFloat()
<< ssboIndex
<< m.rc(0,0) << m.rc(1,0) << m.rc(3,0) // mat0
<< m.rc(0,1) << m.rc(1,1) << m.rc(3,1) // mat1
<< m.rc(0,3) << m.rc(1,3) << m.rc(3,3) // mat2
<< invM.rc(0,0) << invM.rc(1,0) << invM.rc(3,0) // invMat0
<< invM.rc(0,1) << invM.rc(1,1) << invM.rc(3,1); // invMat1
}
void AnalyticRRectRenderStep::writeUniformsAndTextures(const DrawParams&,
PipelineDataGatherer*) const {
// All data is uploaded as instance attributes, so no uniforms are needed.
}
} // namespace skgpu::graphite