blob: 961a89b02062119fe55d18e3e3612270444a5cc8 [file]
/*
* Copyright 2023 Rive
*/
#include "rive/renderer/draw.hpp"
#include "gr_inner_fan_triangulator.hpp"
#include "rive_render_path.hpp"
#include "rive/math/bezier_utils.hpp"
#include "rive/math/wangs_formula.hpp"
#include "rive/renderer/render_context_impl.hpp"
#include "rive/renderer/rive_renderer.hpp"
#include "rive/renderer/rive_render_paint.hpp"
#include "rive/renderer/stack_vector.hpp"
#include "rive/renderer/texture.hpp"
#include "gradient.hpp"
#include "shaders/constants.glsl"
#include "rive/profiler/profiler_macros.h"
#include <cmath>
namespace rive::gpu
{
namespace
{
// Maximum # of outerCurve patches a curve on the path can be subdivided into.
constexpr static size_t kMaxCurveSubdivisions = (kMaxParametricSegments +
OuterCubicPatchSegmentSpan -
1) /
OuterCubicPatchSegmentSpan;
static uint32_t find_outer_cubic_subdivision_count(
const Vec2D pts[],
const wangs_formula::VectorXform& vectorXform)
{
float numSubdivisions = ceilf(
wangs_formula::cubic(pts, kParametricPrecision, vectorXform) *
(1.f / OuterCubicPatchSegmentSpan));
return static_cast<uint32_t>(
math::clamp(numSubdivisions, 1, kMaxCurveSubdivisions));
}
constexpr static int NumSpokesInMiterOrBevelJoin = 4;
// depthStencil bevels need 2 internal spokes: both colocated the ends, but with
// AA outsets in the direction of the bevel.
constexpr static int NumSpokesInDepthStencilBevelJoin = 2;
// depthStencil miters need 3 internal spokes: all colocated on the miter
// corner, with different AA outsets.
constexpr static int NumSpokesInDepthStencilMiterJoin = 3;
// depthStencil miterClip joins need 4 internal spokes: 2 colocated per corner,
// with different AA outsets.
constexpr static int NumSpokesInDepthStencilMiterClipJoin = 4;
// "Fixed" join types, i.e., miter or bevel (not round), all have a fixed number
// of spokes regardless of angle.
constexpr static int numSpokesInFixedJoin(
PathDraw::CoverageType coverageType,
StrokeJoin join)
{
assert(join != StrokeJoin::round);
if (coverageType == PathDraw::CoverageType::depthStencil)
{
switch (join)
{
case StrokeJoin::bevel:
return NumSpokesInDepthStencilBevelJoin;
case StrokeJoin::miter:
return NumSpokesInDepthStencilMiterJoin;
case StrokeJoin::round:
RIVE_UNREACHABLE();
}
}
return NumSpokesInMiterOrBevelJoin;
}
constexpr static int numSegmentsInFixedJoin(
PathDraw::CoverageType coverageType,
StrokeJoin join)
{
// Since a join sits between 2 cubics, it already has an implicit spoke at
// either end, shared with its cubic neighbor. A join SEGMENT count is
// therefore its internal spoke count plus 1.
return numSpokesInFixedJoin(coverageType, join) + 1;
}
constexpr static int numSegmentsInMiterClipJoin(
PathDraw::CoverageType coverageType)
{
return (coverageType != PathDraw::CoverageType::depthStencil
? NumSpokesInMiterOrBevelJoin
: NumSpokesInDepthStencilMiterClipJoin) +
1;
}
constexpr static int STROKE_OR_FEATHER_STYLE_FLAG = 8;
constexpr static int ROUND_JOIN_STYLE_FLAG = STROKE_OR_FEATHER_STYLE_FLAG << 1;
RIVE_ALWAYS_INLINE constexpr int style_flags(
bool isStrokeOrFeather,
bool roundJoinStroked)
{
int styleFlags = (isStrokeOrFeather << 3) | (roundJoinStroked << 4);
assert(bool(styleFlags & STROKE_OR_FEATHER_STYLE_FLAG) == isStrokeOrFeather);
assert(bool(styleFlags & ROUND_JOIN_STYLE_FLAG) == roundJoinStroked);
return styleFlags;
}
// Switching on a StyledVerb reduces "if (stroked)" branching and makes the code
// cleaner.
enum class StyledVerb
{
filledMove = static_cast<int>(PathVerb::move),
strokedMove = STROKE_OR_FEATHER_STYLE_FLAG | static_cast<int>(PathVerb::move),
roundJoinStrokedMove = STROKE_OR_FEATHER_STYLE_FLAG | ROUND_JOIN_STYLE_FLAG |
static_cast<int>(PathVerb::move),
filledLine = static_cast<int>(PathVerb::line),
strokedLine = STROKE_OR_FEATHER_STYLE_FLAG | static_cast<int>(PathVerb::line),
roundJoinStrokedLine = STROKE_OR_FEATHER_STYLE_FLAG | ROUND_JOIN_STYLE_FLAG |
static_cast<int>(PathVerb::line),
filledQuad = static_cast<int>(PathVerb::quad),
strokedQuad = STROKE_OR_FEATHER_STYLE_FLAG | static_cast<int>(PathVerb::quad),
roundJoinStrokedQuad = STROKE_OR_FEATHER_STYLE_FLAG | ROUND_JOIN_STYLE_FLAG |
static_cast<int>(PathVerb::quad),
filledCubic = static_cast<int>(PathVerb::cubic),
strokedCubic = STROKE_OR_FEATHER_STYLE_FLAG |
static_cast<int>(PathVerb::cubic),
roundJoinStrokedCubic = STROKE_OR_FEATHER_STYLE_FLAG | ROUND_JOIN_STYLE_FLAG |
static_cast<int>(PathVerb::cubic),
filledClose = static_cast<int>(PathVerb::close),
strokedClose = STROKE_OR_FEATHER_STYLE_FLAG |
static_cast<int>(PathVerb::close),
roundJoinStrokedClose = STROKE_OR_FEATHER_STYLE_FLAG | ROUND_JOIN_STYLE_FLAG |
static_cast<int>(PathVerb::close),
};
RIVE_ALWAYS_INLINE constexpr StyledVerb styled_verb(
PathVerb verb,
int styleFlags)
{
return static_cast<StyledVerb>(styleFlags | static_cast<int>(verb));
}
// When chopping strokes, switching on a "chopKey" reduces "if (areChopsCusps)"
// branching and makes the code cleaner.
RIVE_ALWAYS_INLINE constexpr uint8_t chopKey(
bool areChopsCusps,
uint8_t numChops)
{
return (numChops << 1) | static_cast<uint8_t>(areChopsCusps);
}
RIVE_ALWAYS_INLINE constexpr uint8_t cuspChopKey(uint8_t n)
{
return chopKey(true, n);
}
RIVE_ALWAYS_INLINE constexpr uint8_t simpleChopKey(uint8_t n)
{
return chopKey(false, n);
}
// Produces a cubic equivalent to the given line, for which Wang's formula also
// returns 1.
RIVE_ALWAYS_INLINE std::array<Vec2D, 4> convert_line_to_cubic(
const Vec2D line[2])
{
float4 endPts = simd::load4f(line);
float4 controlPts = simd::mix(endPts, endPts.zwxy, float4(1 / 3.f));
std::array<Vec2D, 4> cubic;
cubic[0] = line[0];
simd::store(&cubic[1], controlPts);
cubic[3] = line[1];
return cubic;
}
RIVE_ALWAYS_INLINE std::array<Vec2D, 4> convert_line_to_cubic(
Vec2D p0,
Vec2D p1)
{
Vec2D line[2] = {p0, p1};
return convert_line_to_cubic(line);
}
// Room for the pieces of any chopped cubic. Two cusp chops require storage for
// 16 points. This is because when chopping around a cusp, we temporarily
// produce three cubics (one before the cusp, a tiny one at the cusp, and one
// after the cusp), then remove the tiny middle one (it's replaced with a cusp
// join). So performing two cusp chops temporarily produces: cubic A, cusp
// cubic, cubic B, cusp cubic, cubic C. Each cubic requires three points plus
// the last cubic's end control point, bringing us to 16 points. (After removing
// the two cusp cubics, we're back down to 10 points.)
using ChopBuffer = std::array<Vec2D, 16>;
// Chops a cubic at each of its n cusps into n + 1 pieces that meet exactly on
// the cusp points. Strokes draw a cusp join across each cusp.
//
// To avoid zero tangents that occur at cusps, for a cusp at T, two chops are
// performed at T - EPSILON and T + EPSILON and the middle cubic is collapsed by
// snapping the surrounding two cubics' endpoints to the cusp's position.
//
// dst needs room for 6 * n + 4 points because it has to hold an intermediate
// result. The result is the first 3 * n + 4 points.
RIVE_ALWAYS_INLINE static void chopCubicAtCusps(
const Vec2D p[4],
ChopBuffer* dst,
const float cuspT[],
int n)
{
float t[4];
assert(n * 2 <= std::size(t));
assert(6 * n + 4 <= std::tuple_size<ChopBuffer>::value);
// Generate chop points straddling each cusp with padding. This creates
// buffer space around the cusp that protects against fp32 precision issues.
for (int i = 0; i < n; ++i)
{
// If the cusps are extremely close together, don't allow the straddle
// points to cross.
float minT = i == 0 ? 0.f : (cuspT[i - 1] + cuspT[i]) * .5f;
float maxT = i + 1 == n ? 1.f : (cuspT[i + 1] + cuspT[i]) * .5f;
t[i * 2 + 0] = fmaxf(cuspT[i] - math::EPSILON, minT);
t[i * 2 + 1] = fminf(cuspT[i] + math::EPSILON, maxT);
}
// chopCubicAt() will produce 2n + 1 pieces, with piece j at dst[3 * j].
// Each odd piece is a sliver around a cusp.
math::chopCubicAt(p, dst->data(), t, n * 2);
// Collapse each sliver to the cusp by placing the previous piece's last
// control point (which is shared with the next piece's first control
// point) directly at the cusp's position and shifting the next piece down
// in the list.
ChopBuffer& pts = *dst;
for (int i = 0; i < n; ++i)
{
pts[3 * i + 3] = math::evalCubicAt(p, cuspT[i]);
pts[3 * i + 4] = pts[6 * i + 7];
pts[3 * i + 5] = pts[6 * i + 8];
}
// Copy the cubic's original endpoint.
pts[3 * n + 3] = pts[6 * n + 3];
}
// Chops a stroked cubic at the T values its chop key calls for, writing the
// pieces end to end into dst. The key must have at least one chop.
RIVE_ALWAYS_INLINE static void chopCubic(
const Vec2D p[4],
uint8_t chopKey,
const float t[2],
ChopBuffer* dst)
{
switch (chopKey)
{
case cuspChopKey(2): // 2 cusps
case cuspChopKey(1): // 1 cusp
chopCubicAtCusps(p, dst, t, chopKey >> 1);
break;
case simpleChopKey(2): // 2 non-cusp chops
math::chopCubicAt(p, dst->data(), t[0], t[1]);
break;
case simpleChopKey(1): // 1 non-cusp chop
math::chopCubicAt(p, dst->data(), t[0]);
break;
default:
RIVE_UNREACHABLE();
}
}
// Finds the starting tangent in a contour composed of the points [pts, end). If
// all points are equal, generates a tangent pointing horizontally to the right.
static Vec2D find_starting_tangent(const Vec2D pts[], const Vec2D* end)
{
assert(end > pts);
const Vec2D p0 = pts[0];
while (++pts < end)
{
Vec2D p = *pts;
if (p != p0)
{
return p - p0;
}
}
return {1, 0};
}
// Finds the ending tangent in a contour composed of the points [pts, end). If
// all points are equal, generates a tangent pointing horizontally to the left.
static Vec2D find_ending_tangent(const Vec2D pts[], const Vec2D* end)
{
assert(end > pts);
const Vec2D endpoint = end[-1];
while (--end > pts)
{
Vec2D p = end[-1];
if (p != endpoint)
{
return endpoint - p;
}
}
return {-1, 0};
}
static Vec2D find_join_tangent_full_impl(
const Vec2D* joinPoint,
const Vec2D* end,
bool closed,
const Vec2D* p0)
{
// Find the first point in the contour not equal to *joinPoint and return
// the difference. RawPath should have discarded empty verbs, so this should
// be a fast operation.
for (const Vec2D* p = joinPoint + 1; p != end; ++p)
{
if (*p != *joinPoint)
{
return *p - *joinPoint;
}
}
if (closed)
{
for (const Vec2D* p = p0; p != joinPoint; ++p)
{
if (*p != *joinPoint)
{
return *p - *joinPoint;
}
}
}
// This should never be reached because RawPath discards empty verbs.
RIVE_UNREACHABLE();
}
RIVE_ALWAYS_INLINE Vec2D find_join_tangent(
const Vec2D* joinPoint,
const Vec2D* end,
bool closed,
const Vec2D* p0)
{
// Quick early out for inlining and branch prediction: The next point in the
// contour is almost always the point that determines the join tangent.
const Vec2D* nextPoint = joinPoint + 1;
nextPoint = nextPoint != end ? nextPoint : p0;
Vec2D tangent = *nextPoint - *joinPoint;
return tangent != Vec2D{0, 0}
? tangent
: find_join_tangent_full_impl(joinPoint, end, closed, p0);
}
// Should an empty stroke emit round caps, square caps, or none?
//
// Just pick the cap type that makes the most sense for a contour that animates
// from non-empty to empty:
//
// * A non-closed contour with round caps and a CLOSED contour with round
// JOINS both converge to a
// circle when animated to empty.
// => round caps on the empty contour.
//
// * A non-closed contour with square caps converges to a square (albeit with
// potential rotation
// that is lost when the contour becomes empty).
// => square caps on the empty contour.
//
// * A closed contour with miter JOINS converges to... some sort of polygon
// with pointy corners.
// ~=> square caps on the empty contour.
//
// * All other contours converge to nothing.
// => butt caps on the empty contour, which are ignored.
//
static StrokeCap empty_stroke_cap(bool closed, StrokeJoin join, StrokeCap cap)
{
if (closed)
{
switch (join)
{
case StrokeJoin::round:
return StrokeCap::round;
case StrokeJoin::miter:
return StrokeCap::square;
case StrokeJoin::bevel:
return StrokeCap::butt;
}
}
return cap;
}
RIVE_ALWAYS_INLINE bool is_final_verb_of_contour(
const RawPath::Iter& iter,
const RawPath::Iter& end)
{
return iter.rawVerbsPtr() + 1 == end.rawVerbsPtr();
}
RIVE_ALWAYS_INLINE uint32_t join_type_flags(StrokeJoin join)
{
switch (join)
{
case StrokeJoin::miter:
return MITER_REVERT_JOIN_CONTOUR_FLAG;
case StrokeJoin::round:
return ROUND_JOIN_CONTOUR_FLAG;
case StrokeJoin::bevel:
return BEVEL_JOIN_CONTOUR_FLAG;
}
RIVE_UNREACHABLE();
}
inline float find_atlas_feather_scale_factor(
float featherRadius,
float matrixMaxScale)
{
// In practice, and with "gaussian -> linear -> gaussian" filtering, we
// never need to render a blur with a radius larger than 16 pixels. After
// this point, we can just scale it up to whatever resolution it needs to be
// displayed at.
return 16.f / fmaxf(featherRadius * matrixMaxScale, 16);
}
static uint32_t feather_join_segment_count(float polarSegmentsPerRadian)
{
uint32_t n = static_cast<uint32_t>(ceilf(polarSegmentsPerRadian * math::PI)) +
FEATHER_JOIN_HELPER_SEGMENT_COUNT;
n = std::max(n, FEATHER_JOIN_MIN_SEGMENT_COUNT);
// FEATHER_POLAR_SEGMENT_MIN_ANGLE should limit n long before we reach
// kMaxPolarSegments.
assert(n < gpu::kMaxPolarSegments);
return n;
}
// Finds where each of a stroke's batched cubics gets chopped, four at a time.
// Returns the total number of chops.
RIVE_NEVER_INLINE static size_t findCubicChops(
RenderContext::CubicBatch* batch,
size_t cubicCount)
{
// Most cubics need no chops. The candidates' keys get filled in below.
batch->chopKeys.assign(
math::round_up_to_multiple_of<4>(cubicCount),
chopKey(false, 0));
batch->chopCandidates.clear();
const float4* blocks = batch->points.data();
for (size_t blockStart = 0; blockStart < cubicCount; blockStart += 4)
{
// Padding lanes hold copies of the last cubic, so whole blocks are safe
// to process.
const float4* block = blocks + blockStart * 2;
auto [p0x, p0y, p1x, p1y] = simd::load4x4f(
reinterpret_cast<const float*>(block));
auto [p2x, p2y, p3x, p3y] = simd::load4x4f(
reinterpret_cast<const float*>(block + 4));
// The early-out finds cubics that need no chops four at a time. The
// rest become candidates for the chop finder.
uint32_t candidateLanes = ~simd::bitmask(math::cubicsNeedNoChops(
p0x,
p0y,
p1x,
p1y,
p2x,
p2y,
p3x,
p3y)) &
0xf;
if (candidateLanes == 0)
{
continue;
}
if (cubicCount - blockStart < 4)
{
// Padding lanes are never candidates.
candidateLanes &= (1u << (cubicCount - blockStart)) - 1;
}
for (uint32_t lane = 0; lane < 4; ++lane)
{
if (candidateLanes & (1u << lane))
{
batch->chopCandidates.push_back(
math::lossless_numeric_cast<uint32_t>(blockStart + lane));
}
}
}
// Find the candidates' chops four at a time.
size_t chopCount = 0;
const uint32_t* candidates = batch->chopCandidates.data();
size_t candidateCount = batch->chopCandidates.size();
// Room to store whole groups of 4.
size_t paddedCandidateCount = math::round_up_to_multiple_of<4>(
candidateCount);
if (batch->chopT0.size() < paddedCandidateCount)
{
batch->chopT0.resize(paddedCandidateCount);
batch->chopT1.resize(paddedCandidateCount);
}
for (size_t first = 0; first < candidateCount; first += 4)
{
// Gather up to four candidates' rows, repeating the last to fill a
// partial group.
float4 p01[4];
float4 p23[4];
for (size_t lane = 0; lane < 4; ++lane)
{
uint32_t i = candidates[std::min(first + lane, candidateCount - 1)];
const float4* block = blocks + (i / 4) * 8;
p01[lane] = block[i % 4];
p23[lane] = block[4 + i % 4];
}
auto [p0x, p0y, p1x, p1y] = simd::transpose4x4f(
p01[0],
p01[1],
p01[2],
p01[3]);
auto [p2x, p2y, p3x, p3y] = simd::transpose4x4f(
p23[0],
p23[1],
p23[2],
p23[3]);
math::Convex180Chops4 chops = math::findCubicConvex180Chops4(
p0x,
p0y,
p1x,
p1y,
p2x,
p2y,
p3x,
p3y);
// A group's candidates have consecutive slots, so store all 4 lanes at
// once. The scalar lanes below overwrite theirs.
simd::store(batch->chopT0.data() + first, chops.T0);
simd::store(batch->chopT1.data() + first, chops.T1);
for (size_t lane = 0; lane < 4 && first + lane < candidateCount; ++lane)
{
uint32_t i = candidates[first + lane];
uint8_t numChops;
if (chops.possibleCusps[lane])
{
// Cusps are rare enough to handle one at a time.
Vec2D p[4];
simd::store(&p[0].x, p01[lane]);
simd::store(&p[2].x, p23[lane]);
float t[2] = {0, 0};
bool areChopsCusps = false;
// Note: calling the full findCubicConvex180Chops() is doing a
// bit of redundant work (e.g. the early-out check, and tests
// for non-cusp cases). I haven't optimized for this case
// because the possibleCusps branch is fairly rare.
numChops = math::findCubicConvex180Chops(p, t, &areChopsCusps);
batch->chopKeys[i] = chopKey(areChopsCusps, numChops);
batch->chopT0[first + lane] = t[0];
batch->chopT1[first + lane] = t[1];
}
else
{
numChops = static_cast<uint8_t>(chops.numChops[lane]);
batch->chopKeys[i] = chopKey(false, numChops);
}
chopCount += numChops;
}
}
return chopCount;
}
// Finds the polar segment count of every rotation from its tangent pair, four
// at a time.
RIVE_NEVER_INLINE static void countPolarSegments(
std::array<Vec2D, 2>* tangentPairs,
size_t rotationCount,
float polarSegmentsPerRadian,
uint32_t* polarSegmentCounts)
{
size_t paddedRotationCount = math::round_up_to_multiple_of<4>(rotationCount);
for (size_t j = rotationCount; j < paddedRotationCount; ++j)
{
tangentPairs[j] = {Vec2D{0, 1}, Vec2D{0, 1}};
}
for (size_t j = 0; j < paddedRotationCount; j += 4)
{
auto [tx0, ty0, tx1, ty1] = simd::load4x4f(&tangentPairs[j][0].x);
float4 numer = tx0 * tx1 + ty0 * ty1;
float4 denomPow2 = (tx0 * tx0 + ty0 * ty0) * (tx1 * tx1 + ty1 * ty1);
#ifdef RIVE_HAS_FAST_INVERSE_SQRT
float4 cosTheta = numer * simd::inverseSqrtFast(denomPow2);
#else
float4 cosTheta = numer / simd::sqrt(denomPow2);
#endif
cosTheta = simd::clamp(cosTheta, float4(-1), float4(1));
float4 theta = simd::fast_acos(cosTheta);
float4 n = simd::ceil(theta * polarSegmentsPerRadian);
n = simd::clamp(n, float4(1), float4(kMaxPolarSegments));
simd::store(polarSegmentCounts + j, simd::cast<uint32_t>(n));
}
}
// Finds the parametric segment count of every piece, four at a time, and, when
// given tangentPairs, the tangents at both ends of each piece, which go to the
// piece's rotation slot. Returns the total parametric segment count.
static uint32_t processPieces(
const float4* blocks,
size_t pieceCount,
float parametricPrecision,
const wangs_formula::VectorXform& vectorXform,
uint32_t* parametricSegmentCounts,
std::array<Vec2D, 2>* tangentPairs,
const uint32_t* rotationSlots)
{
uint4 total = 0;
for (size_t first = 0; first < pieceCount; first += 4)
{
const float4* block = blocks + first * 2;
auto [p0x, p0y, p1x, p1y] = simd::load4x4f(
reinterpret_cast<const float*>(block));
auto [p2x, p2y, p3x, p3y] = simd::load4x4f(
reinterpret_cast<const float*>(block + 4));
uint4 counts = wangs_formula::cubicSegmentCounts<kMaxParametricSegments>(
p0x,
p0y,
p1x,
p1y,
p2x,
p2y,
p3x,
p3y,
parametricPrecision,
vectorXform);
// parametricSegmentCounts has room for whole blocks.
simd::store(parametricSegmentCounts + first, counts);
// Padding lanes don't count toward the total.
total += simd::if_then_else(
uint4{0, 1, 2, 3} + uint32_t(first) < uint32_t(pieceCount),
counts,
uint4(0));
if (tangentPairs == nullptr)
{
continue;
}
auto [tan0x, tan0y, tan1x, tan1y] = math::findCubicTangents4(
p0x,
p0y,
p1x,
p1y,
p2x,
p2y,
p3x,
p3y);
// Each column is one piece's tangent pair.
simd::gvec4x4<float> pairs = simd::transpose4x4f(
tan0x,
tan0y,
tan1x,
tan1y);
size_t laneCount = std::min<size_t>(pieceCount - first, 4);
for (size_t lane = 0; lane < laneCount; ++lane)
{
simd::store(
tangentPairs[rotationSlots[first + lane]].data(),
pairs.cols[lane]);
}
}
return simd::reduce_add(total);
}
} // namespace
Draw::Draw(
IAABB pixelBounds,
const Mat2D& paintMatrix,
const Mat2D* imageMatrix,
BlendMode blendMode,
float additiveness,
rcp<Texture> imageTexture,
ImageSampler imageSampler,
Type type)
: m_imageTextureRef(imageTexture.release())
, m_imageSampler(imageSampler)
, m_pixelBounds(pixelBounds)
, m_paintMatrix(paintMatrix)
, m_imageMatrix((imageMatrix != nullptr) ? *imageMatrix : paintMatrix)
, m_blendMode(blendMode)
, m_additiveness(math::clamp(additiveness, 0.f, 1.f))
, m_type(type)
, m_clippedPixelBounds(pixelBounds)
{
if (m_blendMode != BlendMode::srcOver)
{
m_combinedDrawContents |= gpu::DrawContents::advancedBlend;
}
}
void Draw::setClipID(uint32_t clipID)
{
m_clipID = clipID;
// For clipUpdates, m_clipID refers to the ID we are writing to the stencil
// buffer (NOT the ID we are clipping against). It therefore doesn't affect
// the activeClip flag in that case.
if (!enums::is_flag_set(
m_combinedDrawContents,
gpu::DrawContents::clipUpdate))
{
if (m_clipID != 0)
{
m_combinedDrawContents |= gpu::DrawContents::activeClip;
}
else
{
m_combinedDrawContents &= ~gpu::DrawContents::activeClip;
}
}
}
void Draw::releaseRefs() { safe_unref(m_imageTextureRef); }
PathDraw::CoverageType PathDraw::SelectCoverageType(
const RiveRenderPaint* paint,
float matrixMaxScale,
const gpu::PlatformFeatures& platformFeatures,
gpu::InterlockMode interlockMode)
{
if (paint->getFeather() != 0)
{
if (platformFeatures.alwaysFeatherToAtlas ||
interlockMode == gpu::InterlockMode::depthStencil ||
// Always switch to the atlas once we can render quarter-resultion.
find_atlas_feather_scale_factor(
featherRadiusFromFeather(paint->getFeather()),
matrixMaxScale) <= .5f)
{
return CoverageType::featherAtlas;
}
}
switch (interlockMode)
{
case gpu::InterlockMode::rasterOrdering:
case gpu::InterlockMode::atomics:
return CoverageType::pixelLocalStorage;
case gpu::InterlockMode::clockwise:
return CoverageType::clockwise;
case gpu::InterlockMode::clockwiseAtomic:
return CoverageType::clockwiseAtomic;
case gpu::InterlockMode::depthStencil:
return CoverageType::depthStencil;
}
RIVE_UNREACHABLE();
}
IAABB PathDraw::calculatePixelBounds(
const Mat2D& paintMatrix,
const RiveRenderPath* path,
const RiveRenderPaint* paint,
bool inkbleed)
{
std::optional<StrokeParams> stroke;
if (paint->getIsStroked())
{
stroke = paint->getStrokeParams();
switch (stroke->position)
{
case StrokePosition::inside:
// Measure these as if not stroked
stroke = std::nullopt;
break;
case StrokePosition::center:
// center is already good
break;
case StrokePosition::outside:
// Measure these with a double-width centered stroke
stroke->position = StrokePosition::center;
stroke->thickness *= 2.0f;
break;
}
}
return path->calculatePixelBounds(
paintMatrix,
stroke,
paint->getFeather(),
inkbleed && paint->getType() != gpu::PaintType::clipUpdate);
}
DrawUniquePtr PathDraw::Make(
RenderContext* context,
const Mat2D& paintMatrix,
const Mat2D* imageMatrix,
rcp<const RiveRenderPath> path,
FillRule fillRule,
const RiveRenderPaint* paint,
float modulatedOpacity,
ColorInt modulatedColor,
std::optional<IAABB> precomputedPixelBounds)
{
RIVE_PROF_SCOPE_L(2);
assert(path != nullptr);
assert(paint != nullptr);
CoverageType coverageType = SelectCoverageType(
paint,
paintMatrix.findMaxScale(),
context->platformFeatures(),
context->frameInterlockMode());
IAABB pixelBounds;
// In debug mode, we want to always calculate the bounds and validate
// against the pre-calculated ones so skip this
#ifdef NDEBUG
if (precomputedPixelBounds.has_value())
{
// These were already computed so we don't need to do it again.
pixelBounds = *precomputedPixelBounds;
}
else
#endif
{
// We weren't given pre-computed bounds, so calculate them.
pixelBounds = calculatePixelBounds(
paintMatrix,
path.get(),
paint,
context->frameDescriptor().inkbleedOverride);
}
// In debug mode, we always compute the pixel bounds, so validate that the
// value we got was exactly the same as the precomputed bounds.
assert(
!precomputedPixelBounds.has_value() ||
pixelBounds == *precomputedPixelBounds);
GrInnerFanTriangulator* triangulator = nullptr;
const AABB& localBounds = path->getBounds();
if (context->isOutsideCurrentFrame(pixelBounds))
{
// Anything precomputing the pixel bounds should be checking against the
// screen bounds rather than letting it bubble down into here.
assert(!precomputedPixelBounds.has_value());
return nullptr;
}
// Skip interior triangulation on inkbleed until we have a compute-based
// index generator. We're vertex bound on Android now, and outerCubic
// patches generate a lot of vertex waste, which then gets compounded by the
// multiple lanes and multiple passes of inkbleed hairlines.
// We also don't allow interior triangulation for draws with an additional
// outset.
if (!paint->getIsStroked() && paint->getFeather() == 0 &&
!context->frameDescriptor().inkbleedOverride &&
paint->getAdditionalOutset() == 0.0f)
{
// Use interior triangulation to draw filled paths if they're large
// enough to benefit from it.
//
// FIXME! Implement interior triangulation for feathers.
auto& triController = context->triangulationController();
const size_t triVerbCount = path->getRawPath().verbs().count();
const float triArea = gpu::find_transformed_area(localBounds, paintMatrix);
if (triController.isEligible(triArea, triVerbCount))
{
triangulator = path->cachedTriangulator();
if (triangulator != nullptr)
{
// A cached triangulation is free to reuse.
triController.recordCacheHit();
}
else if (triController.admits(triArea, triVerbCount))
{
const double triStart = context->impl()->secondsNow();
triangulator = path->createTriangulator(context->perFrameAllocator());
triController.recordBuilt(context->impl()->secondsNow() - triStart);
}
}
}
auto draw = PathDrawUniquePtr(context->make<PathDraw>(
pixelBounds,
paintMatrix,
imageMatrix,
std::move(path),
fillRule,
paint,
modulatedOpacity,
modulatedColor,
coverageType,
context->frameDescriptor()));
if (triangulator != nullptr)
{
draw->initForInteriorTriangulation(context, triangulator);
}
else
{
draw->initForMidpointFan(context, paint);
}
// Drop empty draws before they get sorted into batches.
if (draw->isEmpty())
{
return nullptr;
}
return draw;
}
PathDraw::PathDraw(
IAABB pixelBounds,
const Mat2D& paintMatrix,
const Mat2D* imageMatrix,
rcp<const RiveRenderPath> path,
FillRule initialFillRule,
const RiveRenderPaint* paint,
float modulatedOpacity,
ColorInt modulatedColor,
CoverageType coverageType,
const RenderContext::FrameDescriptor& frameDesc)
: Draw(
pixelBounds,
paintMatrix,
imageMatrix,
paint->getBlendMode(),
paint->getAdditiveness(),
ref_rcp(paint->getImageTexture()),
paint->getImageSampler(),
Type::path)
, m_pathRef(path.release())
, m_pathFillRule(
frameDesc.clockwiseFillOverride ? FillRule::clockwise
: initialFillRule)
, m_gradientRef(
paint->getModulatedGradient(modulatedOpacity, modulatedColor)
.release())
, m_inverseGradientTransform(paint->getInverseGradientTransform())
, m_paintType(paint->getType())
, m_coverageType(coverageType)
, m_additionalOutset(paint->getAdditionalOutset())
{
assert(m_pathRef != nullptr);
assert(!m_pathRef->getRawPath().empty());
assert(paint != nullptr);
if (paint->getIsOpaque() &&
// Inkbleed strokes introduce coverage, which is no longer opaque.
!(frameDesc.inkbleedOverride && paint->getIsStroked()))
{
m_combinedDrawContents |= gpu::DrawContents::opaquePaint;
}
m_hasHairlinePass = frameDesc.inkbleedOverride &&
m_coverageType == CoverageType::depthStencil &&
m_pathFillRule == FillRule::clockwise &&
!paint->getIsStroked() && paint->getFeather() == 0 &&
paint->getType() != gpu::PaintType::clipUpdate;
if (paint->getIsLayerMask())
{
m_isLayerMask = true;
m_layerMaskMode = paint->getLayerMaskMode();
}
if (paint->getFeather() != 0)
{
m_featherRadius = featherRadiusFromFeather(paint->getFeather());
assert(!std::isnan(m_featherRadius)); // These should get culled in
// RiveRenderer::drawPath().
assert(m_featherRadius > 0);
}
if (paint->getIsStroked())
{
m_strokeRadius = paint->getThickness() * .5f;
// Ensure stroke radius is nonzero. (In PLS, zero radius means the path
// is filled.)
m_strokeRadius = std::max(
std::numeric_limits<float>::min(),
m_strokeRadius);
assert(!std::isnan(m_strokeRadius)); // These should get culled in
// RiveRenderer::drawPath().
assert(m_strokeRadius > 0);
}
// For atlased paths, m_combinedDrawContents refer to the rectangle being
// drawn into the main render target (not the step that generates the atlas
// mask, which uses, e.g., fill rules, etc.).
if (m_coverageType != CoverageType::featherAtlas)
{
if (isStroke())
{
m_combinedDrawContents |= gpu::DrawContents::stroke;
}
else
{
if (m_featherRadius)
{
m_combinedDrawContents |= gpu::DrawContents::featheredFill;
}
if (initialFillRule == FillRule::clockwise ||
frameDesc.clockwiseFillOverride)
{
m_combinedDrawContents |= gpu::DrawContents::clockwiseFill;
}
else if (initialFillRule == FillRule::nonZero)
{
m_combinedDrawContents |= gpu::DrawContents::nonZeroFill;
}
else if (initialFillRule == FillRule::evenOdd)
{
m_combinedDrawContents |= gpu::DrawContents::evenOddFill;
}
}
}
if (paint->getType() == gpu::PaintType::clipUpdate)
{
m_combinedDrawContents |= gpu::DrawContents::clipUpdate;
if (paint->getSimpleValue().outerClipID != 0)
{
m_combinedDrawContents |= gpu::DrawContents::activeClip;
}
}
if (isStroke())
{
// Stroke triangles are always forward.
m_contourDirections = gpu::ContourDirections::forward;
}
else if (initialFillRule == FillRule::clockwise)
{
// Clockwise paths need to be reversed when the matrix is left-handed,
// so that the intended forward triangles remain clockwise.
float det = m_paintMatrix.xx() * m_paintMatrix.yy() -
m_paintMatrix.yx() * m_paintMatrix.xy();
if (det < 0)
{
m_contourDirections = (m_coverageType == CoverageType::depthStencil ||
m_coverageType == CoverageType::featherAtlas)
? gpu::ContourDirections::reverse
: gpu::ContourDirections::forwardThenReverse;
// Ignored by depthStencil.
m_contourFlags |= NEGATE_PATH_FILL_COVERAGE_FLAG;
}
else
{
m_contourDirections = (m_coverageType == CoverageType::depthStencil ||
m_coverageType == CoverageType::featherAtlas)
? gpu::ContourDirections::forward
: gpu::ContourDirections::reverseThenForward;
}
}
else if (m_coverageType != CoverageType::depthStencil)
{
// atomic and rasterOrdering fills need reverse AND forward triangles.
if (frameDesc.clockwiseFillOverride &&
!m_pathRef->isClockwiseDominant(m_paintMatrix))
{
// For clockwiseFill, this is also our opportunity to logically
// reverse the winding of the path, if it is predominantly
// counterclockwise.
m_contourDirections = (m_coverageType == CoverageType::featherAtlas)
? gpu::ContourDirections::reverse
: gpu::ContourDirections::forwardThenReverse;
m_contourFlags |= NEGATE_PATH_FILL_COVERAGE_FLAG;
}
else
{
m_contourDirections = (m_coverageType == CoverageType::featherAtlas)
? gpu::ContourDirections::forward
: gpu::ContourDirections::reverseThenForward;
}
}
else
{
if (initialFillRule == FillRule::nonZero || frameDesc.clockwiseFillOverride)
{
// Emit "nonZero" depthStencil fills in a direction such that the
// dominant triangle winding area is always clockwise. This
// maximizes pixel throughput since we will draw counterclockwise
// triangles twice and clockwise only once.
m_contourDirections = m_pathRef->isClockwiseDominant(m_paintMatrix)
? gpu::ContourDirections::forward
: gpu::ContourDirections::reverse;
}
else
{
// "evenOdd" depthStencil fills just get drawn twice, so any
// direction is fine.
m_contourDirections = gpu::ContourDirections::forward;
}
}
m_simplePaintValue = paint->getSimpleValue();
// Apply modulation to the paint value.
// Gradient modulation is handled upfront in the gradient initialization.
if (modulatedOpacity != 1.0f || modulatedColor != 0xFFFFFFFF)
{
switch (m_paintType)
{
case gpu::PaintType::solidColor:
m_simplePaintValue.color = colorModulate(
m_simplePaintValue.color,
modulatedColor,
modulatedOpacity);
break;
case gpu::PaintType::linearGradient:
case gpu::PaintType::radialGradient:
case gpu::PaintType::angularGradient:
case gpu::PaintType::clipUpdate:
break;
}
}
if (m_coverageType == CoverageType::featherAtlas)
{
// Reserve two triangles for our on-screen rectangle that reads coverage
// from the atlas.
m_resourceCounts.maxTriangleVertexCount = 6;
}
RIVE_DEBUG_CODE(m_pathRef->lockRawPathMutations();)
RIVE_DEBUG_CODE(m_rawPathMutationID = m_pathRef->getRawPathMutationID();)
assert(isStroke() == (strokeRadius() > 0));
assert(isFeatheredFill() == (!isStroke() && featherRadius() > 0));
assert(!isFeatheredFill() || featherRadius() > 0);
}
void PathDraw::releaseRefs()
{
RIVE_PROF_SCOPE_L(2)
Draw::releaseRefs();
RIVE_DEBUG_CODE(m_pathRef->unlockRawPathMutations();)
m_pathRef->unref();
safe_unref(m_gradientRef);
}
// The cubics start out as one list, with one curve slot and one rotation slot
// each, as if none get chopped. For each cubic that does get chopped, its first
// piece replaces the unchopped entry in-place and its other pieces are appended
// to the end of the curve list. Their rotations, plus a cusp join before each
// piece that follows a cusp, are appended to the end of the rotation list.
// Returns the number of rotations added.
size_t PathDraw::appendChoppedPieces(
RenderContext::CubicBatch* batch,
size_t cubicCount,
[[maybe_unused]] size_t contourCount,
size_t mainRotationCount)
{
float4* blocks = batch->points.data();
uint32_t* rotationSlots = batch->rotationSlots.data();
batch->cuspJoins.clear();
auto writePiece = [blocks](size_t curveIdx, const Vec2D* p) {
float4* block = blocks + (curveIdx / 4) * 8;
block[curveIdx % 4] = simd::load4f(p);
block[4 + curveIdx % 4] = simd::load4f(p + 2);
};
// The extra chopped entries start after all of the unchopped entries in the
// curve and rotation lists
size_t extraCurveIdx = cubicCount;
size_t extraRotationIdx = mainRotationCount;
ContourInfo* contour = m_contours;
for (size_t candidateIdx = 0; candidateIdx < batch->chopCandidates.size();
++candidateIdx)
{
uint32_t k = batch->chopCandidates[candidateIdx];
uint8_t chopKey = batch->chopKeys[k];
uint8_t numChops = chopKey >> 1;
if (numChops == 0)
{
continue;
}
bool areChopsCusps = (chopKey & 1) != 0;
// Advance to the contour that owns this cubic, skipping any with no
// cubics (e.g., only lines). A contour's curve slots are the indices
// of its cubics (extraCurveCount tracks the appended pieces).
while (k >= contour->endCurveIdx)
{
++contour;
assert(contour < m_contours + contourCount);
}
// Chop from the RawPath (batch->rawPathCubicPoints rather than
// batch->points). The chop functions need the points in one contiguous
// array, which the batch's block layout doesn't keep, and copying them
// out of the block just to read them back would stall on store
// forwarding.
float t[2] = {batch->chopT0[candidateIdx], batch->chopT1[candidateIdx]};
ChopBuffer localChopBuffer;
chopCubic(batch->rawPathCubicPoints[k], chopKey, t, &localChopBuffer);
// pushMidpointFanTessellationData() only gets the T values and chops
// the cubic again, which measured cheaper than saving these pieces for
// it to read back. (The pieces here only feed processPieces().)
m_chopVertices.push_back() = {t[0], t[1]};
const Vec2D* p = localChopBuffer.data();
writePiece(k, p);
uint32_t lastPieceRotationIdx = rotationSlots[k];
for (int piece = 1; piece <= numChops; ++piece)
{
p += 3;
if (areChopsCusps)
{
// A cusp join across the cusp before this piece. Its tangents
// come from the pieces on either side.
uint32_t joinIdx = math::lossless_numeric_cast<uint32_t>(
extraRotationIdx++);
uint32_t nextPieceRotationIdx = math::lossless_numeric_cast<uint32_t>(
extraRotationIdx);
batch->cuspJoins.push_back(
{joinIdx, lastPieceRotationIdx, nextPieceRotationIdx});
++contour->cuspJoinCount;
++contour->extraRotationCount;
}
writePiece(extraCurveIdx, p);
lastPieceRotationIdx = math::lossless_numeric_cast<uint32_t>(
extraRotationIdx++);
rotationSlots[extraCurveIdx++] = lastPieceRotationIdx;
++contour->extraCurveCount;
++contour->extraRotationCount;
}
}
return extraRotationIdx - mainRotationCount;
}
RIVE_NEVER_INLINE uint32_t PathDraw::countStrokeContourVertices(
ContourInfo* contour,
bool empty,
size_t* extraRotationIdx,
size_t* emptyStrokeCountForCaps)
{
uint32_t vertexCount = 0;
// Polar and parametric segments share the first and final vertices.
// Therefore:
//
// parametricVertexCount = parametricSegmentCount + 1
//
// polarVertexCount = polarVertexCount + 1
//
// mergedVertexCount
// = parametricVertexCount + polarVertexCount - 2
// = parametricSegmentCount + 1 + polarSegmentCount + 1 - 2
// = parametricSegmentCount + polarSegmentCount
//
for (size_t j = contour->firstRotationIdx; j < contour->endRotationIdx; ++j)
{
vertexCount += m_polarSegmentCounts[j];
}
size_t extraIdx = *extraRotationIdx;
for (size_t end = extraIdx + contour->extraRotationCount; extraIdx < end;
++extraIdx)
{
vertexCount += m_polarSegmentCounts[extraIdx];
}
*extraRotationIdx = extraIdx;
// Count joins.
if (!isStroke())
{
assert(isFeatheredFill());
uint32_t numSegmentsInFeatherJoin = feather_join_segment_count(
m_polarSegmentsPerRadian);
vertexCount += contour->strokeJoinCount * (numSegmentsInFeatherJoin - 1);
}
else if (m_strokeJoin == StrokeJoin::round)
{
// Round joins share their beginning and ending spokes with the curve
// on either side. Therefore, the number of spokes we need to allocate
// for a round join is "joinSegmentCount - 1". Do all the -1's here.
vertexCount -= contour->strokeJoinCount;
}
else
{
vertexCount += contour->strokeJoinCount *
numSpokesInFixedJoin(m_coverageType, m_strokeJoin);
}
// Cusp joins share both end vertices (like round joins). Their polar
// segments were summed above, with the other rotations.
vertexCount -= contour->cuspJoinCount;
// Count stroke caps, if any.
StrokeCap cap;
bool needsCaps = false;
if (!empty)
{
cap = m_strokeCap;
needsCaps = !contour->closed;
}
else if (isStroke())
{
cap = empty_stroke_cap(contour->closed, m_strokeJoin, m_strokeCap);
// Ignore butt caps when the contour is empty.
needsCaps = cap != StrokeCap::butt;
}
if (needsCaps)
{
// We emulate stroke caps as 180-degree joins.
if (cap == StrokeCap::round)
{
// Round caps rotate 180 degrees.
float strokeCapSegmentCount = ceilf(m_polarSegmentsPerRadian * math::PI);
// Make sure not to exceed kMaxPolarSegments.
strokeCapSegmentCount = std::min<float>(
kMaxPolarSegments,
strokeCapSegmentCount);
contour->strokeCapSegmentCount = static_cast<uint32_t>(
strokeCapSegmentCount);
}
else
{
// Butt caps are emulated as bevel joins, square caps as
// miterClip joins.
contour->strokeCapSegmentCount =
(cap == StrokeCap::square
? numSegmentsInMiterClipJoin(m_coverageType)
: numSegmentsInFixedJoin(m_coverageType, StrokeJoin::bevel));
}
// +2 because caps emulated as joins need to emit spokes at T=0 and
// T=1, unlike normal joins, which always have a cubic neighbor to
// share with on both sides.
contour->strokeCapSegmentCount = std::min(
contour->strokeCapSegmentCount + 2,
kMaxPolarSegments);
// As long as a contour isn't empty, we can tack the end cap onto the
// join section of the final curve in the stroke. Otherwise, we need to
// introduce 0-tessellation-segment curves with non-empty joins to
// carry the caps.
*emptyStrokeCountForCaps += empty ? 2 : 1;
vertexCount += (contour->strokeCapSegmentCount - 1) * 2;
}
return vertexCount;
}
void PathDraw::initForMidpointFan(
RenderContext* context,
const RiveRenderPaint* paint)
{
RIVE_PROF_SCOPE_L(2)
// Only call init() once.
assert(
(m_resourceCounts.midpointFanTessVertexCount |
m_resourceCounts.outerCubicTessVertexCount) == 0);
if (isStrokeOrFeather())
{
m_strokeMatrixMaxScale = m_paintMatrix.findMaxScale();
float r_ = 0;
if (m_featherRadius != 0)
{
r_ = m_featherRadius * m_strokeMatrixMaxScale;
// Inverse of 1/calcPolarSegmentsPerRadian at
// FEATHER_MIN_POLAR_SEGMENT_ANGLE.
//
// i.e., 1 / calcPolarSegmentsPerRadian<kPolarPrecision>(
// FEATHER_MIN_POLAR_SEGMENT_ANGLE) ==
// FEATHER_MAX_SCREEN_SPACE_RADIUS
//
constexpr static float FEATHER_MAX_SCREEN_SPACE_RADIUS =
1 /
(kPolarPrecision * (1 - COS_FEATHER_POLAR_SEGMENT_MIN_ANGLE_OVER_2));
assert(math::nearly_equal(
1 / math::calcPolarSegmentsPerRadian<kPolarPrecision>(
FEATHER_MAX_SCREEN_SPACE_RADIUS),
gpu::FEATHER_POLAR_SEGMENT_MIN_ANGLE));
// r_ is used to calculate how many polar segments are needed. Limit
// r_ for large feathers. (See FEATHER_POLAR_SEGMENT_MIN_ANGLE.)
r_ = std::min(r_, FEATHER_MAX_SCREEN_SPACE_RADIUS);
}
if (isStroke())
{
r_ += m_strokeRadius * m_strokeMatrixMaxScale;
}
m_polarSegmentsPerRadian = math::calcPolarSegmentsPerRadian<
kPolarPrecision>(r_);
m_strokeJoin = paint->getJoin();
m_strokeCap = paint->getCap();
}
// Count up how much temporary storage this function will need to reserve in
// CPU buffers.
const RawPath& rawPath = m_pathRef->getRawPath();
assert(rawPath.verbs()[0] == PathVerb::move);
size_t contourCount = rawPath.verbCount(PathVerb::move);
assert(contourCount != 0);
m_contours = reinterpret_cast<ContourInfo*>(
context->perFrameAllocator().alloc(sizeof(ContourInfo) * contourCount));
assert(rawPath.verbCount(PathVerb::quad) == 0);
size_t cubicCount = rawPath.verbCount(PathVerb::cubic);
// Only cubics take curve slots. A stroked cubic chops into at most 3
// pieces.
size_t maxCurves = isStrokeOrFeather() ? cubicCount * 3 : cubicCount;
size_t maxRotations = 0;
if (isStrokeOrFeather())
{
// Each piece of a stroked cubic takes a rotation slot, as does the cusp
// join across each cusp.
maxRotations += cubicCount * 5;
if (m_strokeJoin == StrokeJoin::round)
{
// If the stroke has round joins, we also record the rotations
// between (pre-chopped) joins in order to calculate how many
// vertices are in each round join. Each line or cubic has at most
// one join before it, and closing a contour can add an additional
// join. Note that we append `contourCount` rather than the number
// of explicit `close` verbs to account for implicit closes.
maxRotations += rawPath.verbCount(PathVerb::line) + cubicCount +
contourCount;
}
}
// Each stroked cubic will record the number of chops it requires (either
// 0, 1, or 2).
size_t maxChops = isStrokeOrFeather() ? cubicCount : 0;
// Each chopped cubic saves its T values for
// pushMidpointFanTessellationData() to chop it again.
size_t maxChopVertices = maxChops;
// Curves and rotations are processed in whole blocks of 4.
size_t maxPaddedRotations = isStrokeOrFeather()
? math::round_up_to_multiple_of<4>(
maxRotations)
: 0;
size_t maxPaddedCurves = math::round_up_to_multiple_of<4>(maxCurves);
// Reserve intermediate space for the polar segment counts of each curve and
// round join.
if (isStrokeOrFeather())
{
m_numChops.reset(context->numChopsAllocator(), maxChops);
m_chopVertices.reset(context->chopVerticesAllocator(), maxChopVertices);
m_tangentPairs = context->tangentPairsAllocator().alloc(maxPaddedRotations);
m_polarSegmentCounts = context->polarSegmentCountsAllocator().alloc(
maxPaddedRotations);
}
m_parametricSegmentCounts = context->parametricSegmentCountsAllocator().alloc(
maxPaddedCurves);
float parametricPrecision = gpu::kParametricPrecision;
if (m_featherRadius > 1)
{
// Once the blur radius is above ~50 pixels, we don't have to tessellate
// within 1/4px of the edge anymore.
// At this point, tessellate within strokeRadius/200 pixels of the edge.
// (parametricPrecision == 1/tolerance.)
parametricPrecision = std::min(
parametricPrecision * 100.f /
(m_featherRadius * m_strokeMatrixMaxScale),
parametricPrecision);
}
size_t lineCount = 0;
size_t emptyStrokeCountForCaps = 0;
// Iteration pass 1: Collect contours, round joins and line counts, and copy
// the cubics into the batch buffer. Each cubic gets one curve and rotation
// slot. appendChoppedPieces() will later add any slots for the extra pieces
// that result from chopping.
size_t contourIdx = 0;
size_t curveIdx = 0;
// We measure rotations on both curves and round joins.
size_t rotationIdx = 0;
bool roundJoinStroked = isStroke() && m_strokeJoin == StrokeJoin::round;
wangs_formula::VectorXform vectorXform(m_paintMatrix);
RenderContext::CubicBatch& cubicBatch = context->cubicBatch();
cubicBatch.resize(cubicCount, maxCurves, isStroke());
float4* cubicBlocks = cubicBatch.points.data();
// Where the next cubic's [p0 p1] goes. Its [p2 p3] goes 4 lanes later.
float4* nextCubicLane = cubicBlocks;
// Copies a cubic into its block and gives it the next curve slot. In pass 1
// the curve slots are the cubics' indices.
auto gatherCubic = [&](const Vec2D* p) {
assert(curveIdx < cubicCount);
nextCubicLane[0] = simd::load4f(p);
nextCubicLane[4] = simd::load4f(p + 2);
++curveIdx;
// Skip the [p2 p3] half after a block's last lane.
nextCubicLane += (curveIdx % 4 != 0) ? 1 : 5;
};
RawPath::Iter startOfContour = rawPath.begin();
RawPath::Iter end = rawPath.end();
// Original number of lines and curves, before chopping.
int preChopVerbCount = 0;
Vec2D endpointsSum{};
bool closed = !isStroke() || paint->getForceClosed();
Vec2D lastTangent = {0, 1};
Vec2D firstTangent = {0, 1};
size_t roundJoinCount = 0;
size_t contourFirstCurveIdx = curveIdx;
size_t contourFirstRotationIdx = rotationIdx;
// Inlined so the counters it shares with the loop can stay in registers.
auto finishAndAppendContour = [&](RawPath::Iter iter) RIVE_INLINE_LAMBDA {
if (closed)
{
Vec2D finalPtInContour = iter.rawPtsPtr()[-1];
// Bit-cast to uint64_t because we don't want the special equality
// rules for NaN. If we're empty or otherwise return back to p0, we
// want to detect this, regardless of whether there are NaN values.
if (math::bit_cast<uint64_t>(startOfContour.movePt()) !=
math::bit_cast<uint64_t>(finalPtInContour))
{
assert(preChopVerbCount > 0);
if (roundJoinStroked)
{
// Round join before implicit closing line.
Vec2D tangent = startOfContour.movePt() - finalPtInContour;
assert(rotationIdx < maxPaddedRotations);
m_tangentPairs[rotationIdx++] = {lastTangent, tangent};
lastTangent = tangent;
++roundJoinCount;
}
++lineCount; // Implicit closing line.
// The first point in the contour hasn't gotten
// counted yet.
++preChopVerbCount;
endpointsSum += startOfContour.movePt();
}
if (roundJoinStroked && preChopVerbCount != 0)
{
// Round join back to the beginning of the contour.
assert(rotationIdx < maxPaddedRotations);
m_tangentPairs[rotationIdx++] = {lastTangent, firstTangent};
++roundJoinCount;
}
}
size_t strokeJoinCount = preChopVerbCount;
if (!closed)
{
strokeJoinCount = std::max<size_t>(strokeJoinCount, 1) - 1;
}
assert(contourIdx < contourCount);
m_contours[contourIdx++] = {
iter,
lineCount,
contourFirstCurveIdx,
curveIdx,
contourFirstRotationIdx,
rotationIdx,
isStroke() ? Vec2D() : endpointsSum * (1.f / preChopVerbCount),
closed,
strokeJoinCount,
0, // cuspJoinCount (see appendChoppedPieces())
0, // extraCurveCount
0, // extraRotationCount
0, // strokeCapSegmentCount
0, // paddingVertexCount
RIVE_DEBUG_CODE(0) // tessVertexCount
};
contourFirstCurveIdx = curveIdx;
contourFirstRotationIdx = rotationIdx;
};
const int styleFlags = style_flags(isStrokeOrFeather(), roundJoinStroked);
for (RawPath::Iter iter = startOfContour; iter != end; ++iter)
{
switch (styled_verb(iter.verb(), styleFlags))
{
case StyledVerb::roundJoinStrokedMove:
case StyledVerb::strokedMove:
case StyledVerb::filledMove:
if (iter != startOfContour)
{
finishAndAppendContour(iter);
startOfContour = iter;
}
preChopVerbCount = 0;
endpointsSum = {0, 0};
closed = !isStroke() || paint->getForceClosed();
lastTangent = {0, 1};
firstTangent = {0, 1};
roundJoinCount = 0;
break;
case StyledVerb::roundJoinStrokedClose:
case StyledVerb::strokedClose:
case StyledVerb::filledClose:
assert(iter != startOfContour);
closed = true;
break;
case StyledVerb::roundJoinStrokedLine:
{
const Vec2D* p = iter.linePts();
Vec2D tangent = p[1] - p[0];
if (preChopVerbCount == 0)
{
firstTangent = tangent;
}
else
{
assert(rotationIdx < maxPaddedRotations);
m_tangentPairs[rotationIdx++] = {lastTangent, tangent};
++roundJoinCount;
}
lastTangent = tangent;
[[fallthrough]];
}
case StyledVerb::strokedLine:
case StyledVerb::filledLine:
{
const Vec2D* p = iter.linePts();
++preChopVerbCount;
endpointsSum += p[1];
++lineCount;
break;
}
case StyledVerb::roundJoinStrokedQuad:
case StyledVerb::strokedQuad:
case StyledVerb::filledQuad:
RIVE_UNREACHABLE();
break;
case StyledVerb::roundJoinStrokedCubic:
{
const Vec2D* p = iter.cubicPts();
Vec2D unchoppedTangents[2];
math::findCubicTangents(p, unchoppedTangents);
if (preChopVerbCount == 0)
{
firstTangent = unchoppedTangents[0];
}
else
{
assert(rotationIdx < maxPaddedRotations);
m_tangentPairs[rotationIdx++] = {lastTangent, unchoppedTangents[0]};
++roundJoinCount;
}
lastTangent = unchoppedTangents[1];
[[fallthrough]];
}
case StyledVerb::strokedCubic:
{
const Vec2D* p = iter.cubicPts();
++preChopVerbCount;
endpointsSum += p[3];
assert(curveIdx < maxPaddedCurves);
assert(rotationIdx < maxPaddedRotations);
if (isStroke())
{
// Where this cubic's tangents go. processPieces() fills
// them in after chopping, once the slot holds the cubic's
// first piece.
cubicBatch
.rotationSlots[curveIdx] = math::lossless_numeric_cast<uint32_t>(
rotationIdx);
cubicBatch.rawPathCubicPoints[curveIdx] = p;
}
else
{
// FIXME: Feathered fills don't have polar segments for now,
// but we're leaving space for them in m_tangentPairs
// because we will convert them to use a similar concept of
// "polar joins" once we move them onto the GPU.
m_tangentPairs[rotationIdx] = {Vec2D{0, 1}, Vec2D{0, 1}};
}
gatherCubic(p);
++rotationIdx;
break;
}
case StyledVerb::filledCubic:
{
const Vec2D* p = iter.cubicPts();
++preChopVerbCount;
endpointsSum += p[3];
assert(curveIdx < maxPaddedCurves);
gatherCubic(p);
break;
}
}
}
if (startOfContour != end)
{
finishAndAppendContour(end);
}
assert(curveIdx == cubicCount);
assert(contourIdx == contourCount);
assert(contourCount > 0);
assert(rotationIdx <= maxPaddedRotations);
assert(isStrokeOrFeather() || maxPaddedRotations == 0);
assert(isStrokeOrFeather() || rotationIdx == 0);
// Fill the final block's unused lanes with copies of the last cubic, so
// SIMD code never sees uninitialized data.
if (size_t lastLane = cubicCount % 4; lastLane != 0)
{
float4* block = cubicBlocks + (cubicCount / 4) * 8;
for (size_t lane = lastLane; lane < 4; ++lane)
{
block[lane] = block[lastLane - 1];
block[4 + lane] = block[4 + lastLane - 1];
}
}
// Chop strokes into sections that do not inflect (i.e, are convex), and do
// not rotate more than 180 degrees. This is required by the GPU
// parametric/polar sorter. (Feathered fills come here already chopped.)
size_t chopCount = 0;
if (isStroke() && cubicCount != 0)
{
chopCount = findCubicChops(&cubicBatch, cubicCount);
// pushMidpointFanTessellationData() chops each cubic again from its
// key and T values.
memcpy(
m_numChops.push_back_n(cubicCount),
cubicBatch.chopKeys.data(),
cubicCount);
}
else if (isStrokeOrFeather())
{
memset(m_numChops.push_back_n(cubicCount), chopKey(false, 0), cubicCount);
}
m_firstExtraCurveIdx = curveIdx;
m_firstExtraRotationIdx = rotationIdx;
if (chopCount != 0)
{
rotationIdx += appendChoppedPieces(
&cubicBatch,
cubicCount,
contourCount,
rotationIdx);
curveIdx += chopCount;
}
assert(curveIdx <= maxPaddedCurves);
assert(rotationIdx <= maxPaddedRotations);
// Count every curve's parametric segments and, for strokes, find its
// tangents.
uint32_t parametricSegmentTotal = processPieces(
cubicBatch.points.data(),
curveIdx,
parametricPrecision,
vectorXform,
m_parametricSegmentCounts,
isStroke() ? m_tangentPairs : nullptr,
cubicBatch.rotationSlots.data());
if (chopCount != 0)
{
// Fill in the tangents for cusp joins that were added for cusp chops.
// These just bridge the surrounding cubics' tangents.
for (auto [joinIdx, beforeIdx, afterIdx] : cubicBatch.cuspJoins)
{
m_tangentPairs[joinIdx] = {
m_tangentPairs[beforeIdx][1],
m_tangentPairs[afterIdx][0]};
}
}
// Return any data we conservatively allocated but did not use.
size_t paddedCurveCount = math::round_up_to_multiple_of<4>(curveIdx);
size_t paddedRotationCount = math::round_up_to_multiple_of<4>(rotationIdx);
if (isStrokeOrFeather())
{
m_numChops.shrinkToFit(context->numChopsAllocator(), maxChops);
m_chopVertices.shrinkToFit(
context->chopVerticesAllocator(),
maxChopVertices);
context->tangentPairsAllocator().rewindLastAllocation(
maxPaddedRotations - paddedRotationCount);
context->polarSegmentCountsAllocator().rewindLastAllocation(
maxPaddedRotations - paddedRotationCount);
}
context->parametricSegmentCountsAllocator().rewindLastAllocation(
maxPaddedCurves - paddedCurveCount);
if (isStrokeOrFeather())
{
countPolarSegments(
m_tangentPairs,
rotationIdx,
m_polarSegmentsPerRadian,
m_polarSegmentCounts);
}
// Iteration pass 2: Count the tessellation vertices in each contour.
size_t contourFirstLineIdx = 0;
size_t tessVertexCount = 0;
size_t extraCurveIdx = m_firstExtraCurveIdx;
size_t extraRotationIdx = m_firstExtraRotationIdx;
for (size_t i = 0; i < contourCount; ++i)
{
ContourInfo* contour = &m_contours[i];
size_t contourLineCount = contour->endLineIdx - contourFirstLineIdx;
uint32_t contourVertexCount = math::lossless_numeric_cast<uint32_t>(
contourLineCount * 2); // Each line tessellates to 2 vertices.
if (contourCount == 1)
{
// A single contour has every curve.
contourVertexCount += parametricSegmentTotal;
}
else
{
for (size_t c = contour->firstCurveIdx; c < contour->endCurveIdx; ++c)
{
contourVertexCount += m_parametricSegmentCounts[c];
}
for (size_t end = extraCurveIdx + contour->extraCurveCount;
extraCurveIdx < end;
++extraCurveIdx)
{
contourVertexCount += m_parametricSegmentCounts[extraCurveIdx];
}
}
if (isStrokeOrFeather())
{
bool empty = contour->endLineIdx == contourFirstLineIdx &&
contour->endCurveIdx == contour->firstCurveIdx;
contourVertexCount += countStrokeContourVertices(
contour,
empty,
&extraRotationIdx,
&emptyStrokeCountForCaps);
}
else
{
// Fills don't have polar segments:
//
// mergedVertexCount = parametricVertexCount =
// parametricSegmentCount + 1
//
// Just collect the +1 for each non-stroked curve.
size_t contourCurveCount = contour->endCurveIdx - contour->firstCurveIdx;
contourVertexCount += contourCurveCount;
}
// Add padding vertices until the number of tessellation vertices in the
// contour is an exact multiple of kMidpointFanPatchSegmentSpan. This
// ensures that patch boundaries align with contour boundaries.
contour->paddingVertexCount = math::padding_to_align_up<
kMidpointFanPatchSegmentSpan>(contourVertexCount);
contourVertexCount += contour->paddingVertexCount;
assert(contourVertexCount % kMidpointFanPatchSegmentSpan == 0);
RIVE_DEBUG_CODE(contour->tessVertexCount = contourVertexCount;)
tessVertexCount += contourVertexCount;
contourFirstLineIdx = contour->endLineIdx;
}
assert(contourFirstLineIdx == lineCount);
RIVE_DEBUG_CODE(m_pendingLineCount = lineCount);
RIVE_DEBUG_CODE(m_pendingCurveCount = curveIdx);
RIVE_DEBUG_CODE(m_pendingRotationCount = rotationIdx);
RIVE_DEBUG_CODE(m_pendingEmptyStrokeCountForCaps = emptyStrokeCountForCaps);
if (tessVertexCount > 0)
{
m_resourceCounts.pathCount = 1;
m_resourceCounts.contourCount = contourCount;
// maxTessellatedSegmentCount does not get doubled when we emit both
// forward and mirrored contours because the forward and mirrored pair
// both get packed into a single gpu::TessVertexSpan.
m_resourceCounts.maxTessellatedSegmentCount = lineCount + curveIdx +
emptyStrokeCountForCaps;
m_resourceCounts
.midpointFanTessVertexCount = gpu::ContourDirectionsAreDoubleSided(
m_contourDirections)
? tessVertexCount * 2
: tessVertexCount;
}
}
void PathDraw::initForInteriorTriangulation(
RenderContext* context,
GrInnerFanTriangulator* triangulator)
{
RIVE_PROF_SCOPE_L(2)
PUSH_DISABLE_CLANG_SIMD_ABI_WARNING()
assert(simd::all(m_resourceCounts.toVec() == 0)); // Only call init() once.
POP_DISABLE_CLANG_SIMD_ABI_WARNING()
assert(!isStrokeOrFeather());
assert(m_strokeRadius == 0);
assert(m_triangulator == nullptr);
assert(triangulator != nullptr);
m_triangulator = triangulator;
// The triangulator's mesh is transform- and fill-rule-independent (so it
// can be cached); the fill rule and winding are fed per draw.
float matrixDeterminant = m_paintMatrix[0] * m_paintMatrix[3] -
m_paintMatrix[2] * m_paintMatrix[1];
m_triangulatorReverseTriangles = matrixDeterminant < 0;
m_triangulatorNegateWinding = (matrixDeterminant < 0) !=
static_cast<bool>(
m_contourFlags &
NEGATE_PATH_FILL_COVERAGE_FLAG);
// Every path has at least 1 (non-cubic) move.
size_t originalNumChopsSize = m_pathRef->getRawPath().verbs().size() - 1;
m_numChops.reset(context->numChopsAllocator(), originalNumChopsSize);
iterateOuterCubics(nullptr); // Fill in m_resourceCounts.
m_numChops.shrinkToFit(context->numChopsAllocator(), originalNumChopsSize);
}
bool PathDraw::allocateResources(RenderContext::LogicalFlush* flush)
{
RIVE_PROF_SCOPE_L(2)
const RenderContext::FrameDescriptor& frameDesc = flush->frameDescriptor();
// Allocate a gradient if needed. Do this first since it's more expensive to
// fail after setting up an atlas draw than a gradient draw.
if (m_gradientRef != nullptr)
{
if (!flush->allocateGradient(
m_gradientRef,
&m_simplePaintValue.colorRampLocation))
{
return false;
}
}
// Allocate a coverage buffer range or feather atlas region if needed.
if (m_coverageType == CoverageType::featherAtlas ||
(m_coverageType == CoverageType::clockwiseAtomic &&
// Outermost (i.e., non-nested) clockwiseAtomic clips render directly
// to the clip buffer without using the coverage buffer.
!isOutermostClipUpdate()))
{
constexpr static int PADDING = 2;
// We don't need any coverage space for areas outside the viewport.
// TODO: Account for active clips as well once we have the info.
IAABB renderTargetBounds = {
0,
0,
static_cast<int32_t>(frameDesc.renderTargetWidth),
static_cast<int32_t>(frameDesc.renderTargetHeight),
};
IAABB visibleBounds = renderTargetBounds.intersect(m_pixelBounds);
if (m_coverageType == CoverageType::featherAtlas)
{
const float scaleFactor = find_atlas_feather_scale_factor(
m_featherRadius,
m_strokeMatrixMaxScale);
auto w = static_cast<uint16_t>(
ceilf(visibleBounds.width() * scaleFactor));
auto h = static_cast<uint16_t>(
ceilf(visibleBounds.height() * scaleFactor));
uint16_t x, y;
if (!flush->allocateFeatherAtlasDraw(
this,
w,
h,
PADDING,
&x,
&y,
&m_featherAtlasScissor))
{
return false; // There wasn't room for our path in the atlas.
}
m_featherAtlasTransform.scaleFactor = scaleFactor;
m_featherAtlasTransform.translateX = x - visibleBounds.left * scaleFactor;
m_featherAtlasTransform.translateY = y - visibleBounds.top * scaleFactor;
m_featherAtlasScissorEnabled = visibleBounds != m_pixelBounds;
}
else
{
// Round up width and height to multiples of BUFFER_IMAGE_TILE_SIZE
// for tiling.
uint32_t
coverageWidth = math::round_up_to_multiple_of<BUFFER_IMAGE_TILE_SIZE>(
visibleBounds.width() + PADDING * 2);
uint32_t coverageHeight = math::round_up_to_multiple_of<
BUFFER_IMAGE_TILE_SIZE>(visibleBounds.height() + PADDING * 2);
// Get our coverage allocation.
size_t offset = flush->allocateCoverageBufferRange(
coverageHeight * coverageWidth);
if (offset == -1)
{
return false; // There wasn't room for our coverage buffer.
}
m_coverageBufferRange.offset = math::lossless_numeric_cast<uint32_t>(
offset);
m_coverageBufferRange.pitch = coverageWidth;
m_coverageBufferRange.offsetX = -visibleBounds.left + PADDING;
m_coverageBufferRange.offsetY = -visibleBounds.top + PADDING;
}
}
return true;
}
void PathDraw::countSubpasses(const gpu::PlatformFeatures& platformFeatures)
{
RIVE_PROF_SCOPE_L(2)
m_subpassCount = 1;
m_prepassCount = 0;
switch (m_coverageType)
{
case CoverageType::featherAtlas:
assert(m_triangulator == nullptr);
m_subpassCount = 1;
break;
case CoverageType::pixelLocalStorage:
m_subpassCount = (m_triangulator != nullptr)
? 2 // outer cubics, interior triangles
: 1;
break;
case CoverageType::clockwise:
m_subpassCount = (m_triangulator != nullptr)
? 3 // ccw interior tris, outer cubics, cw interior
// tris
: 1;
break;
case CoverageType::clockwiseAtomic:
m_subpassCount = (m_triangulator != nullptr) ? 2 : 1;
if (needsBorrowedCoveragePrepass())
{
// Add prepasses for borrowed coverage.
m_prepassCount = m_subpassCount;
}
break;
case CoverageType::depthStencil:
{
if (isStroke())
{
m_subpassCount = 1; // Strokes can be rendered in a single pass.
}
else if (enums::all_flags_set(
m_combinedDrawContents,
gpu::kNestedClipUpdateMask))
{
// Nested clip updates only have a stencil pass. (The reset is
// handled by a separate ClipReset draw.)
m_subpassCount = 1;
}
else if (enums::is_flag_set(
m_combinedDrawContents,
gpu::DrawContents::evenOddFill))
{
// depthStencil "slow" path: stencil-then-cover.
m_subpassCount = 2;
}
else if (platformFeatures.supportsPipelineDynamicState)
{
// depthStencil "fast" path, combined: the three subpasses
// (borrowed coverage, fans, stencil reset) collapse onto a
// single stencilDynamicMidpointFans draw that switches between
// them with dynamic color/depth/stencil/cull state. One batch
// keeps the reorderer able to instance non-overlapping paths
// together.
m_subpassCount = 1;
}
else
{
// depthStencil "fast" path: (effectively) single pass
// rendering.
m_subpassCount = 3;
}
if (isOpaque())
{
const bool usesClipping = enums::any_flag_set(
m_combinedDrawContents,
gpu::DrawContents::activeClip | gpu::DrawContents::clipUpdate);
if (!usesClipping)
{
// Render this path front-to-back instead of back-to-front.
assert(m_prepassCount == 0);
m_prepassCount = m_subpassCount;
m_subpassCount = 0;
}
}
if (m_hasHairlinePass)
{
// The hairline is always a subpass (never a prepass because the
// AA makes it non-opaque).
++m_subpassCount;
}
}
}
const int passCount = m_prepassCount + m_subpassCount;
assert(passCount <= MaxSubpassCount);
std::fill_n(m_passDrawContents.begin(), passCount, m_combinedDrawContents);
if (m_hasHairlinePass)
{
// The hairline is never opaque because it has AA.
m_passDrawContents[passCount - 1] &= ~gpu::DrawContents::opaquePaint;
}
}
gpu::DrawBatch* PathDraw::pushToRenderContext(
RenderContext::LogicalFlush* flush,
int signedSubpassIdx,
uint32_t zIndex)
{
RIVE_PROF_SCOPE_L(2)
// Make sure the rawPath in our path reference hasn't changed since we began
// holding!
assert(m_rawPathMutationID == m_pathRef->getRawPathMutationID());
assert(!m_pathRef->getRawPath().empty());
assert(!isEmpty());
assert(
m_resourceCounts.outerCubicTessVertexCount == 0 ||
m_resourceCounts.midpointFanTessVertexCount == 0);
uint32_t tessVertexCount = math::lossless_numeric_cast<uint32_t>(
m_resourceCounts.outerCubicTessVertexCount |
m_resourceCounts.midpointFanTessVertexCount);
const gpu::DrawContents subpassDrawContents = this->subpassDrawContents(
signedSubpassIdx);
if (m_pathID == 0)
{
// Reserve our pathID and write out a path record.
m_pathID = flush->pushPath(this, zIndex);
}
switch (m_coverageType)
{
case CoverageType::pixelLocalStorage:
case CoverageType::clockwise:
{
const int mainSubpassIdx = (m_coverageType == CoverageType::clockwise &&
m_triangulator != nullptr)
? 1
: 0;
if (signedSubpassIdx == mainSubpassIdx)
{
// Tessellation (midpoint fan or outer cubic).
uint32_t tessLocation = allocateTessellationVertices(
flush,
tessVertexCount);
pushTessellationData(flush, tessVertexCount, tessLocation);
return &pushTessellationDraw(flush, tessVertexCount, tessLocation);
}
else
{
// Interior triangles.
assert(m_triangulator != nullptr);
assert(
(m_coverageType == CoverageType::pixelLocalStorage &&
signedSubpassIdx == 1) ||
(m_coverageType == CoverageType::clockwise &&
(signedSubpassIdx == 0 || signedSubpassIdx == 2)));
gpu::DrawBatch* batch = flush->pushInteriorTriangulationDraw(
this,
subpassDrawContents,
m_pathID,
(m_coverageType == CoverageType::clockwise)
// Clockwise mode renders counterclockwise (borrowed
// coverage) interior triangles in a separate pass.
? (signedSubpassIdx == 0) ? gpu::WindingFaces::negative
: gpu::WindingFaces::positive
: gpu::WindingFaces::all,
(signedSubpassIdx == 0) // => CoverageType::clockwise
? gpu::ShaderMiscFlags::borrowedCoveragePass
: gpu::ShaderMiscFlags::none RIVE_DEBUG_CODE(
,
&m_numInteriorTriangleVerticesPushed));
assert(
m_numInteriorTriangleVerticesPushed <=
m_triangulator->maxVertexCount(m_pathFillRule));
return batch;
}
RIVE_UNREACHABLE();
}
case CoverageType::clockwiseAtomic:
{
if (m_prepassCount != 0)
{
// The subpass and prepass each emit half the vertices.
assert(m_prepassCount == m_subpassCount);
assert(tessVertexCount % 2 == 0);
assert(needsBorrowedCoveragePrepass());
tessVertexCount /= 2;
}
switch (signedSubpassIdx)
{
case -1: // Tessellation (borrowed, midpointFan or outerCubic).
assert(!isStroke());
m_prepassTessLocation = allocateTessellationVertices(
flush,
tessVertexCount);
return &pushTessellationDraw(
flush,
tessVertexCount,
m_prepassTessLocation,
gpu::ShaderMiscFlags::borrowedCoveragePass);
case 0: // Tessellation (midpointFan or outerCubic).
{
uint32_t tessLocation = allocateTessellationVertices(
flush,
tessVertexCount);
pushTessellationData(flush, tessVertexCount, tessLocation);
return &pushTessellationDraw(flush, tessVertexCount, tessLocation);
}
case -2: // Interior triangles (borrowed).
case 1: // Interior triangles.
{
assert(!isStroke());
assert(m_triangulator != nullptr);
gpu::DrawBatch* batch = flush->pushInteriorTriangulationDraw(
this,
subpassDrawContents,
m_pathID,
m_prepassCount == 0 ? gpu::WindingFaces::all
: signedSubpassIdx < 0 ? gpu::WindingFaces::negative
: gpu::WindingFaces::positive,
signedSubpassIdx < 0 ? gpu::ShaderMiscFlags::borrowedCoveragePass
: gpu::ShaderMiscFlags::none RIVE_DEBUG_CODE(
,
&m_numInteriorTriangleVerticesPushed));
assert(
m_numInteriorTriangleVerticesPushed <=
m_triangulator->maxVertexCount(m_pathFillRule));
return batch;
}
}
RIVE_UNREACHABLE();
}
case CoverageType::depthStencil:
{
// The fill passes are either all opaque prepasses or else all
// subpasses. The hairline adds one extra subpass on top of either.
assert(m_prepassCount == 0 || m_subpassCount == int(m_hasHairlinePass));
int fillPassCount = m_prepassCount + m_subpassCount -
int(m_hasHairlinePass);
int passIdx = signedSubpassIdx + m_prepassCount;
if (passIdx == 0)
{
m_depthStencilTessLocation = allocateTessellationVertices(
flush,
tessVertexCount);
pushTessellationData(
flush,
tessVertexCount,
m_depthStencilTessLocation);
}
if (m_hasHairlinePass && passIdx == fillPassCount)
{
// Hairline pass: unlike polar strokes, hairlines have no polar
// spokes, no join spokes, and tessellate identically to
// midpointFans.
assert(m_triangulator == nullptr);
return &flush->pushMidpointFanDraw(
this,
gpu::DrawType::depthAAOuterHairline,
subpassDrawContents,
tessVertexCount,
m_depthStencilTessLocation);
}
assert(1 <= fillPassCount && fillPassCount <= 3);
assert(passIdx < fillPassCount);
if (m_triangulator != nullptr)
{
// depthStencil interior triangulation: the path interior is
// filled by smuggling its triangles in with outerCubic patches,
// rather than introducing a distinct triangle-buffer draw.
gpu::DrawType outerCubicDrawType;
if (fillPassCount == 1)
{
if (enums::all_flags_set(
subpassDrawContents,
gpu::kNestedClipUpdateMask))
{
outerCubicDrawType = gpu::DrawType::stencilOuterCubicWinding;
}
else
{
assert(flush->platformFeatures().supportsPipelineDynamicState);
outerCubicDrawType = gpu::DrawType::stencilDynamicOuterCubics;
}
}
else
{
constexpr static gpu::DrawType OuterCubicFillTypes[][3] = {
// Slow path (fillPassCount == 2): stencil-then-cover.
{
gpu::DrawType::stencilOuterCubicWinding,
gpu::DrawType::stencilOuterCubicCover,
},
// Fast path (fillPassCount == 3).
{
gpu::DrawType::stencilOuterCubicBorrowedCoverage,
gpu::DrawType::stencilOuterCubics,
gpu::DrawType::stencilOuterCubicReset,
},
};
outerCubicDrawType = OuterCubicFillTypes[fillPassCount - 2][passIdx];
}
return &flush->pushOuterCubicsDraw(
this,
outerCubicDrawType,
subpassDrawContents,
tessVertexCount,
m_depthStencilTessLocation);
}
else
{
gpu::DrawType depthStencilDrawType;
if (fillPassCount == 1)
{
if (isStroke())
{
depthStencilDrawType = flush->frameDescriptor().inkbleedOverride
? gpu::DrawType::depthAAStrokes
: gpu::DrawType::depthStrokes;
}
else if (enums::all_flags_set(
subpassDrawContents,
gpu::kNestedClipUpdateMask))
{
depthStencilDrawType = gpu::DrawType::stencilMidpointFanWinding;
}
else
{
assert(flush->platformFeatures().supportsPipelineDynamicState);
depthStencilDrawType = gpu::DrawType::stencilDynamicMidpointFans;
}
}
else
{
constexpr static gpu::DrawType MidpointFanFillTypes[][3] = {
// Slow path (fillPassCount == 2): stencil-then-cover
{
gpu::DrawType::stencilMidpointFanWinding,
gpu::DrawType::stencilMidpointFanCover,
},
// Fast path (fillPassCount == 3): (mostly) single pass
// rendering.
{
gpu::DrawType::stencilMidpointFanBorrowedCoverage,
gpu::DrawType::stencilMidpointFans,
gpu::DrawType::stencilMidpointFanReset,
},
};
depthStencilDrawType = MidpointFanFillTypes[fillPassCount - 2]
[passIdx];
}
return &flush->pushMidpointFanDraw(
this,
depthStencilDrawType,
subpassDrawContents,
tessVertexCount,
m_depthStencilTessLocation);
}
}
case CoverageType::featherAtlas:
// Atlas draws only have one subpass -- the rectangular blit from
// the atlas to the screen. The step that renders coverage to the
// offscreen atlas is handled separately, outside the subpass
// system.
assert(signedSubpassIdx == 0);
return &flush->pushFeatherAtlasBlit(this, subpassDrawContents, m_pathID);
}
RIVE_UNREACHABLE();
}
gpu::DrawBatch& PathDraw::pushTessellationDraw(
RenderContext::LogicalFlush* flush,
uint32_t tessVertexCount,
uint32_t tessLocation,
gpu::ShaderMiscFlags shaderMiscFlags)
{
if (m_triangulator != nullptr)
{
assert(!isStroke());
return flush->pushOuterCubicsDraw(
this,
gpu::DrawType::outerCurvePatches,
m_combinedDrawContents,
tessVertexCount,
tessLocation,
shaderMiscFlags);
}
else
{
return flush->pushMidpointFanDraw(
this,
isFeatheredFill() ? gpu::DrawType::midpointFanCenterAAPatches
: gpu::DrawType::midpointFanPatches,
m_combinedDrawContents,
tessVertexCount,
tessLocation,
shaderMiscFlags);
}
}
void PathDraw::pushFeatherAtlasTessellation(
RenderContext::LogicalFlush* flush,
uint32_t* tessVertexCount,
uint32_t* tessBaseVertex)
{
RIVE_PROF_SCOPE_L(2)
assert(m_coverageType == CoverageType::featherAtlas);
assert(!isEmpty());
assert(
m_resourceCounts.outerCubicTessVertexCount == 0 ||
m_resourceCounts.midpointFanTessVertexCount == 0);
*tessVertexCount = math::lossless_numeric_cast<uint32_t>(
m_resourceCounts.outerCubicTessVertexCount |
m_resourceCounts.midpointFanTessVertexCount);
*tessBaseVertex = allocateTessellationVertices(flush, *tessVertexCount);
pushTessellationData(flush, *tessVertexCount, *tessBaseVertex);
}
void PathDraw::pushTessellationData(
RenderContext::LogicalFlush* flush,
uint32_t tessVertexCount,
uint32_t tessLocation)
{
RIVE_PROF_SCOPE_L(2)
// Determine where to fill in forward and mirrored tessellations.
uint32_t forwardTessVertexCount, forwardTessLocation, mirroredTessVertexCount,
mirroredTessLocation;
switch (m_contourDirections)
{
case gpu::ContourDirections::forward:
forwardTessVertexCount = tessVertexCount;
forwardTessLocation = tessLocation;
mirroredTessLocation = mirroredTessVertexCount = 0;
break;
case gpu::ContourDirections::reverse:
forwardTessVertexCount = forwardTessLocation = 0;
mirroredTessVertexCount = tessVertexCount;
mirroredTessLocation = tessLocation + tessVertexCount;
break;
case gpu::ContourDirections::reverseThenForward:
if (m_coverageType == CoverageType::clockwiseAtomic &&
m_prepassTessLocation != 0) // With padding, this will only be
// zero if it's not needed.
{
// The tessellation for borrowed coverage was allocated at a
// different location than the forward tessellation, both with
// "tessVertexCount" vertices.
assert(needsBorrowedCoveragePrepass());
forwardTessVertexCount = mirroredTessVertexCount = tessVertexCount;
forwardTessLocation = tessLocation;
mirroredTessLocation = m_prepassTessLocation + tessVertexCount;
}
else
{
// The reverse and forward tessellations are allocated
// contiguously, with a combined vertex count of
// "tessVertexCount". (tessVertexCount/2 vertices each.)
assert(tessVertexCount % 2 == 0);
forwardTessVertexCount = mirroredTessVertexCount = tessVertexCount / 2;
forwardTessLocation = mirroredTessLocation = tessLocation +
tessVertexCount / 2;
}
break;
case gpu::ContourDirections::forwardThenReverse:
if (m_coverageType == CoverageType::clockwiseAtomic &&
m_prepassTessLocation != 0) // With padding, this will only be
// zero if it's not needed.
{
// The tessellation for borrowed coverage was allocated at a
// different location than the forward tessellation, both with
// "tessVertexCount" vertices.
assert(needsBorrowedCoveragePrepass());
forwardTessVertexCount = mirroredTessVertexCount = tessVertexCount;
forwardTessLocation = m_prepassTessLocation;
mirroredTessLocation = tessLocation + tessVertexCount;
}
else
{
// The reverse and forward tessellations are allocated
// contiguously, with a combined vertex count of
// "tessVertexCount". (tessVertexCount/2 vertices each.)
assert(tessVertexCount % 2 == 0);
forwardTessVertexCount = mirroredTessVertexCount = tessVertexCount / 2;
forwardTessLocation = tessLocation;
mirroredTessLocation = tessLocation + tessVertexCount;
}
break;
}
// Write out the TessVertexSpans and path contours.
RenderContext::TessellationWriter tessWriter(
flush,
m_pathID,
m_contourDirections,
forwardTessVertexCount,
forwardTessLocation,
mirroredTessVertexCount,
mirroredTessLocation);
if (m_triangulator != nullptr)
{
iterateOuterCubics(&tessWriter);
}
else
{
pushMidpointFanTessellationData(&tessWriter);
}
}
void PathDraw::pushMidpointFanTessellationData(
RenderContext::TessellationWriter* tessWriter)
{
RIVE_PROF_SCOPE_L(3)
const RawPath& rawPath = m_pathRef->getRawPath();
RawPath::Iter startOfContour = rawPath.begin();
// The second and later pieces of chopped cubics, and their cusp joins, come
// after all the path's other curves and rotations, in order.
size_t extraCurveIdx = m_firstExtraCurveIdx;
size_t extraRotationIdx = m_firstExtraRotationIdx;
for (size_t i = 0; i < m_resourceCounts.contourCount; ++i)
{
// Push a contour and curve records.
const ContourInfo& contour = m_contours[i];
assert(startOfContour.verb() == PathVerb::move);
assert(isStroke() || contour.closed); // Fills are always closed.
RIVE_DEBUG_CODE(m_pendingStrokeJoinCount = isStrokeOrFeather()
? contour.strokeJoinCount
: 0;)
RIVE_DEBUG_CODE(
m_pendingStrokeCapCount = contour.strokeCapSegmentCount != 0 ? 2 : 0;)
const Vec2D* pts = startOfContour.rawPtsPtr();
size_t curveIdx = contour.firstCurveIdx;
size_t rotationIdx = contour.firstRotationIdx;
RIVE_DEBUG_CODE(size_t contourFirstExtraCurveIdx = extraCurveIdx;)
RIVE_DEBUG_CODE(size_t contourFirstExtraRotationIdx = extraRotationIdx;)
const RawPath::Iter end = contour.endOfContour;
uint32_t joinTypeFlags = 0;
bool roundJoinStroked = false;
// Emit a starting cap before the next cubic?
bool needsFirstEmulatedCapAsJoin = false;
uint32_t emulatedCapAsJoinFlags = 0;
if (isStrokeOrFeather())
{
joinTypeFlags = isStroke() ? join_type_flags(m_strokeJoin)
: FEATHER_JOIN_CONTOUR_FLAG;
roundJoinStroked = joinTypeFlags == ROUND_JOIN_CONTOUR_FLAG;
if (contour.strokeCapSegmentCount != 0)
{
StrokeCap cap = !contour.closed
? m_strokeCap
: empty_stroke_cap(true, m_strokeJoin, m_strokeCap);
switch (cap)
{
case StrokeCap::butt:
emulatedCapAsJoinFlags = BEVEL_JOIN_CONTOUR_FLAG;
break;
case StrokeCap::square:
emulatedCapAsJoinFlags = MITER_CLIP_JOIN_CONTOUR_FLAG;
break;
case StrokeCap::round:
emulatedCapAsJoinFlags = ROUND_JOIN_CONTOUR_FLAG;
break;
}
emulatedCapAsJoinFlags |= EMULATED_STROKE_CAP_CONTOUR_FLAG;
needsFirstEmulatedCapAsJoin = true;
}
}
// Make a data record for this current contour on the GPU.
uint32_t contourIDWithFlags = m_contourFlags |
tessWriter->pushContour(
contour.midpoint,
isStroke(),
contour.closed,
contour.paddingVertexCount);
// When we don't have round joins, the number of segments per join is
// constant. (Round joins have a variable number of segments per join,
// depending on the angle.)
uint32_t fixedJoinSegmentCount = 0;
if (isFeatheredFill())
{
fixedJoinSegmentCount = feather_join_segment_count(
m_polarSegmentsPerRadian);
}
else if (isStroke() && !roundJoinStroked)
{
fixedJoinSegmentCount = numSegmentsInFixedJoin(
m_coverageType,
m_strokeJoin);
}
// Convert all curves in the contour to cubics and push them to the GPU.
const int styleFlags = style_flags(isStrokeOrFeather(), roundJoinStroked);
Vec2D joinTangent = {0, 1};
int joinSegmentCount = 1;
Vec2D implicitClose[2]; // In case we need an implicit closing line.
for (auto iter = startOfContour; iter != end; ++iter)
{
StyledVerb styledVerb = styled_verb(iter.verb(), styleFlags);
switch (styledVerb)
{
case StyledVerb::filledMove:
case StyledVerb::strokedMove:
case StyledVerb::roundJoinStrokedMove:
implicitClose[1] = iter.movePt(); // In case we need an
// implicit closing line.
break;
case StyledVerb::filledClose:
case StyledVerb::strokedClose:
case StyledVerb::roundJoinStrokedClose:
assert(contour.closed);
break;
case StyledVerb::roundJoinStrokedLine:
{
if (contour.closed || !is_final_verb_of_contour(iter, end))
{
joinTangent = m_tangentPairs[rotationIdx][1];
joinSegmentCount = m_polarSegmentCounts[rotationIdx];
++rotationIdx;
RIVE_DEBUG_CODE(--m_pendingRotationCount;)
RIVE_DEBUG_CODE(--m_pendingStrokeJoinCount;)
}
else
{
// End with a 180-degree join that looks like the stroke
// cap.
joinTangent = -find_ending_tangent(pts, end.rawPtsPtr());
joinTypeFlags = emulatedCapAsJoinFlags;
joinSegmentCount = contour.strokeCapSegmentCount;
RIVE_DEBUG_CODE(--m_pendingStrokeCapCount;)
}
goto line_common;
}
case StyledVerb::strokedLine:
if (contour.closed || !is_final_verb_of_contour(iter, end))
{
joinTangent = find_join_tangent(
iter.linePts() + 1,
end.rawPtsPtr(),
contour.closed,
pts);
joinSegmentCount = fixedJoinSegmentCount;
RIVE_DEBUG_CODE(--m_pendingStrokeJoinCount;)
}
else
{
// End with a 180-degree join that looks like the stroke
// cap.
joinTangent = -find_ending_tangent(pts, end.rawPtsPtr());
joinTypeFlags = emulatedCapAsJoinFlags;
joinSegmentCount = contour.strokeCapSegmentCount;
RIVE_DEBUG_CODE(--m_pendingStrokeCapCount;)
}
[[fallthrough]];
case StyledVerb::filledLine:
line_common:
{
std::array<Vec2D, 4> cubic = convert_line_to_cubic(iter.linePts());
if (needsFirstEmulatedCapAsJoin)
{
// Emulate the start cap as a 180-degree join before the
// first stroke.
pushEmulatedStrokeCapAsJoinBeforeCubic(
tessWriter,
cubic.data(),
contour.strokeCapSegmentCount,
contourIDWithFlags | emulatedCapAsJoinFlags);
needsFirstEmulatedCapAsJoin = false;
}
tessWriter->pushCubic(
cubic.data(),
m_contourDirections,
joinTangent,
1,
1,
joinSegmentCount,
contourIDWithFlags | joinTypeFlags);
RIVE_DEBUG_CODE(--m_pendingLineCount;)
break;
}
case StyledVerb::roundJoinStrokedQuad:
case StyledVerb::strokedQuad:
case StyledVerb::filledQuad:
RIVE_UNREACHABLE();
break;
case StyledVerb::roundJoinStrokedCubic:
case StyledVerb::strokedCubic:
{
const Vec2D* p = iter.cubicPts();
uint8_t chopKey = m_numChops.pop_front();
uint8_t numChops = chopKey >> 1;
ChopBuffer localChopBuffer;
if (numChops != 0)
{
auto [t0, t1] = m_chopVertices.pop_front();
const float t[2] = {t0, t1};
chopCubic(p, chopKey, t, &localChopBuffer);
p = localChopBuffer.data();
}
if (needsFirstEmulatedCapAsJoin)
{
// Emulate the start cap as a 180-degree join before the
// first stroke.
pushEmulatedStrokeCapAsJoinBeforeCubic(
tessWriter,
p,
contour.strokeCapSegmentCount,
contourIDWithFlags | emulatedCapAsJoinFlags);
needsFirstEmulatedCapAsJoin = false;
}
// Push chops before the final one. The first piece is in
// the cubic's own slots.
const bool areChopsCusps = (chopKey & 1) != 0;
size_t pieceCurveIdx = curveIdx++;
size_t pieceRotationIdx = rotationIdx++;
for (uint8_t piece = 0; piece < numChops; ++piece, p += 3)
{
uint32_t parametricSegmentCount = m_parametricSegmentCounts
[pieceCurveIdx];
uint32_t polarSegmentCount = m_polarSegmentCounts[pieceRotationIdx];
RIVE_DEBUG_CODE(--m_pendingRotationCount;)
Vec2D chopJoinTangent;
uint32_t chopJoinSegmentCount;
uint32_t chopJoinTypeFlags;
if (areChopsCusps)
{
// A cusp turns 180 degrees which gets drawn with a
// cusp join.
size_t cuspJoinIdx = extraRotationIdx++;
chopJoinTangent = m_tangentPairs[cuspJoinIdx][1];
chopJoinSegmentCount = m_polarSegmentCounts[cuspJoinIdx];
chopJoinTypeFlags = CUSP_JOIN_CONTOUR_FLAG;
RIVE_DEBUG_CODE(--m_pendingRotationCount;)
}
else
{
// A chop at an inflection or at 180 degrees
// continues smoothly, so it needs no join.
chopJoinTangent = joinTangent;
chopJoinSegmentCount = 1;
chopJoinTypeFlags = joinTypeFlags;
}
tessWriter->pushCubic(
p,
m_contourDirections,
chopJoinTangent,
parametricSegmentCount,
polarSegmentCount,
chopJoinSegmentCount,
contourIDWithFlags | chopJoinTypeFlags);
RIVE_DEBUG_CODE(--m_pendingCurveCount;)
pieceCurveIdx = extraCurveIdx++;
pieceRotationIdx = extraRotationIdx++;
}
// Push the final chop, with a join.
uint32_t
parametricSegmentCount = m_parametricSegmentCounts[pieceCurveIdx];
uint32_t polarSegmentCount = m_polarSegmentCounts[pieceRotationIdx];
RIVE_DEBUG_CODE(--m_pendingRotationCount;)
if (contour.closed || !is_final_verb_of_contour(iter, end))
{
if (styledVerb == StyledVerb::roundJoinStrokedCubic)
{
joinTangent = m_tangentPairs[rotationIdx][1];
joinSegmentCount = m_polarSegmentCounts[rotationIdx];
++rotationIdx;
RIVE_DEBUG_CODE(--m_pendingRotationCount;)
}
else
{
joinTangent = find_join_tangent(
iter.cubicPts() + 3,
end.rawPtsPtr(),
contour.closed,
pts);
joinSegmentCount = fixedJoinSegmentCount;
}
RIVE_DEBUG_CODE(--m_pendingStrokeJoinCount;)
}
else
{
// End with a 180-degree join that looks like the stroke
// cap.
joinTangent = -find_ending_tangent(pts, end.rawPtsPtr());
joinTypeFlags = emulatedCapAsJoinFlags;
joinSegmentCount = contour.strokeCapSegmentCount;
RIVE_DEBUG_CODE(--m_pendingStrokeCapCount;)
}
tessWriter->pushCubic(
p,
m_contourDirections,
joinTangent,
parametricSegmentCount,
polarSegmentCount,
joinSegmentCount,
contourIDWithFlags | joinTypeFlags);
RIVE_DEBUG_CODE(--m_pendingCurveCount;)
break;
}
case StyledVerb::filledCubic:
{
uint32_t
parametricSegmentCount = m_parametricSegmentCounts[curveIdx++];
tessWriter->pushCubic(
iter.cubicPts(),
m_contourDirections,
Vec2D{},
parametricSegmentCount,
1,
1,
contourIDWithFlags);
RIVE_DEBUG_CODE(--m_pendingCurveCount;)
break;
}
}
}
if (needsFirstEmulatedCapAsJoin)
{
// The contour was empty. Emit both caps on p0.
Vec2D p0 = pts[0], left = {p0.x - 1, p0.y}, right = {p0.x + 1, p0.y};
pushEmulatedStrokeCapAsJoinBeforeCubic(
tessWriter,
std::array{p0, right, right, right}.data(),
contour.strokeCapSegmentCount,
contourIDWithFlags | emulatedCapAsJoinFlags);
pushEmulatedStrokeCapAsJoinBeforeCubic(
tessWriter,
std::array{p0, left, left, left}.data(),
contour.strokeCapSegmentCount,
contourIDWithFlags | emulatedCapAsJoinFlags);
}
else if (contour.closed)
{
implicitClose[0] = end.rawPtsPtr()[-1];
// Bit-cast to uint64_t because we don't want the special equality
// rules for NaN. If we're empty or otherwise return back to p0, we
// want to detect this, regardless of whether there are NaN values.
if (math::bit_cast<uint64_t>(implicitClose[0]) !=
math::bit_cast<uint64_t>(implicitClose[1]))
{
// Draw a line back to the beginning of the contour.
std::array<Vec2D, 4> cubic = convert_line_to_cubic(implicitClose);
// Closing join back to the beginning of the contour.
if (roundJoinStroked)
{
joinTangent = m_tangentPairs[rotationIdx][1];
joinSegmentCount = m_polarSegmentCounts[rotationIdx];
++rotationIdx;
RIVE_DEBUG_CODE(--m_pendingRotationCount;)
RIVE_DEBUG_CODE(--m_pendingStrokeJoinCount;)
}
else if (isStrokeOrFeather())
{
joinTangent = find_starting_tangent(pts, end.rawPtsPtr());
joinSegmentCount = fixedJoinSegmentCount;
RIVE_DEBUG_CODE(--m_pendingStrokeJoinCount;)
}
tessWriter->pushCubic(
cubic.data(),
m_contourDirections,
joinTangent,
1,
1,
joinSegmentCount,
contourIDWithFlags | joinTypeFlags);
RIVE_DEBUG_CODE(--m_pendingLineCount;)
}
}
assert(curveIdx == contour.endCurveIdx);
assert(rotationIdx == contour.endRotationIdx);
assert(
extraCurveIdx - contourFirstExtraCurveIdx == contour.extraCurveCount);
assert(
extraRotationIdx - contourFirstExtraRotationIdx ==
contour.extraRotationCount);
assert(m_pendingStrokeJoinCount == 0);
assert(m_pendingStrokeCapCount == 0);
startOfContour = contour.endOfContour;
}
// Make sure we only pushed the amount of data we reserved.
assert(m_pendingLineCount == 0);
assert(m_pendingCurveCount == 0);
assert(m_pendingRotationCount == 0);
assert(m_pendingEmptyStrokeCountForCaps == 0);
}
void PathDraw::pushEmulatedStrokeCapAsJoinBeforeCubic(
RenderContext::TessellationWriter* tessWriter,
const Vec2D cubic[],
uint32_t strokeCapSegmentCount,
uint32_t contourIDWithFlags)
{
RIVE_PROF_SCOPE_L(3)
// Reverse the cubic and push it with zero parametric and polar segments,
// and a 180-degree join tangent. This results in a solitary join,
// positioned immediately before the provided cubic, that looks like the
// desired stroke cap.
assert(strokeCapSegmentCount >= 2);
tessWriter->pushCubic(
std::array{cubic[3], cubic[2], cubic[1], cubic[0]}.data(),
m_contourDirections,
math::findCubicTan0(cubic),
0,
0,
strokeCapSegmentCount,
contourIDWithFlags);
RIVE_DEBUG_CODE(--m_pendingStrokeCapCount;)
RIVE_DEBUG_CODE(--m_pendingEmptyStrokeCountForCaps;)
}
void PathDraw::iterateOuterCubics(RenderContext::TessellationWriter* tessWriter)
{
RIVE_PROF_SCOPE_L(2)
Vec2D chops[kMaxCurveSubdivisions * 3 + 1];
const RawPath& rawPath = m_pathRef->getRawPath();
assert(!rawPath.empty());
wangs_formula::VectorXform vectorXform(m_paintMatrix);
size_t patchCount = 0;
size_t contourCount = 0;
Vec2D p0 = {0, 0};
// A straight edge produces a single-segment cubic patch. In depthStencil we
// draw only the patch interior (not the AA border), and that interior is
// degenerate for a single segment, so we skip these patches entirely.
const bool skipFlatOuterCubics = m_coverageType == CoverageType::depthStencil;
// Only used on the "emit" pass (tessWriter != nullptr).
uint32_t contourIDWithFlags = 0;
for (const auto [verb, pts] : rawPath)
{
switch (verb)
{
case PathVerb::move:
if (contourCount != 0 && pts[-1] != p0 && !skipFlatOuterCubics)
{
if (tessWriter != nullptr)
{
tessWriter->pushCubic(
convert_line_to_cubic(pts[-1], p0).data(),
m_contourDirections,
{0, 0},
OuterCubicPatchSegmentSpan,
1,
/*bowtieSegmentCount=*/1,
contourIDWithFlags |
CULL_EXCESS_TESSELLATION_SEGMENTS_CONTOUR_FLAG);
}
++patchCount;
}
if (tessWriter != nullptr)
{
contourIDWithFlags = m_contourFlags | tessWriter->pushContour(
{0, 0},
isStroke(),
/*closed=*/true,
0);
}
p0 = pts[0];
++contourCount;
break;
case PathVerb::line:
if (!skipFlatOuterCubics)
{
if (tessWriter != nullptr)
{
tessWriter->pushCubic(
convert_line_to_cubic(pts).data(),
m_contourDirections,
{0, 0},
OuterCubicPatchSegmentSpan,
1,
/*bowtieSegmentCount=*/1,
contourIDWithFlags |
CULL_EXCESS_TESSELLATION_SEGMENTS_CONTOUR_FLAG);
}
++patchCount;
}
break;
case PathVerb::quad:
RIVE_UNREACHABLE();
case PathVerb::cubic:
{
uint32_t numSubdivisions;
if (tessWriter == nullptr)
{
numSubdivisions = find_outer_cubic_subdivision_count(
pts,
vectorXform);
m_numChops.push_back(numSubdivisions);
}
else
{
numSubdivisions = m_numChops.pop_front();
}
assert(numSubdivisions >= 1);
const size_t numGlueTriangles = numSubdivisions - 1;
const size_t numGlueTriStrips = (numGlueTriangles + 2) / 3;
const size_t numPatches = numSubdivisions + numGlueTriStrips;
patchCount += numPatches;
if (tessWriter != nullptr)
{
if (numSubdivisions == 1)
{
tessWriter->pushCubic(
pts,
m_contourDirections,
{0, 0},
OuterCubicPatchSegmentSpan,
1,
/*bowtieSegmentCount=*/1,
contourIDWithFlags |
CULL_EXCESS_TESSELLATION_SEGMENTS_CONTOUR_FLAG);
}
else
{
RIVE_DEBUG_CODE(size_t pushedPatchCount = 0;)
// Passing nullptr for the 'tValues' causes it to chop
// the cubic uniformly in T.
math::chopCubicAt(pts, chops, nullptr, numSubdivisions - 1);
const Vec2D* chop = chops;
for (size_t i = 0; i < numSubdivisions; ++i)
{
tessWriter->pushCubic(
chop,
m_contourDirections,
{0, 0},
OuterCubicPatchSegmentSpan,
1,
/*bowtieSegmentCount=*/1,
contourIDWithFlags |
CULL_EXCESS_TESSELLATION_SEGMENTS_CONTOUR_FLAG);
chop += 3;
RIVE_DEBUG_CODE(++pushedPatchCount;)
}
// Fill the gap between the sub-cubics by connecting
// their endpoints with a simple fan. Each fan blade is
// cubic patch with RETROFIT_TRI_STRIP_CONTOUR_FLAG (a
// standard strip of up to 3 triangles, retrofitted into
// a cubic patch), anchored at the first vertex
// (chops[0]).
for (size_t a = 1; a < numSubdivisions; a += 3)
{
const auto segmentsRemaining = numSubdivisions - a;
// Flip the triangle strip orthogonally to the fan
// blade in order to generate more efficient (less
// "sliver") triangles.
const size_t b = segmentsRemaining == 1 ? a : a + 1;
StackVector<Vec2D, 5> strip;
strip.push_back(chops[b * 3]);
strip.push_back(chops[(b + 1) * 3]);
if (segmentsRemaining > 1)
strip.push_back(chops[(b - 1) * 3]);
if (segmentsRemaining > 2)
strip.push_back(chops[(b + 2) * 3]);
strip.push_back(chops[0]);
tessWriter->pushRetrofitCubicTriStrip(
strip.data(),
strip.size(),
m_contourDirections,
contourIDWithFlags);
RIVE_DEBUG_CODE(++pushedPatchCount;)
}
assert(pushedPatchCount == numPatches);
}
}
break;
}
case PathVerb::close:
break;
}
}
Vec2D lastPt = rawPath.points().back();
if (contourCount != 0 && lastPt != p0 && !skipFlatOuterCubics)
{
if (tessWriter != nullptr)
{
tessWriter->pushCubic(
convert_line_to_cubic(lastPt, p0).data(),
m_contourDirections,
{0, 0},
OuterCubicPatchSegmentSpan,
1,
/*bowtieSegmentCount=*/1,
contourIDWithFlags | CULL_EXCESS_TESSELLATION_SEGMENTS_CONTOUR_FLAG);
}
++patchCount;
}
if (tessWriter == nullptr)
{
// We also draw each "grout" triangle using an outerCubic patch.
patchCount += m_triangulator->groutList().count();
if (m_coverageType == CoverageType::depthStencil)
{
// depthStencil fills the path interior by smuggling its triangles
// in with outerCubic patches (rather than a distinct
// triangle-buffer draw).
patchCount += m_triangulator->retrofitCubicPatchCount(m_pathFillRule);
}
if (patchCount > 0)
{
m_resourceCounts.pathCount = 1;
m_resourceCounts.contourCount = contourCount;
// maxTessellatedSegmentCount does not get doubled when we emit both
// forward and mirrored contours because the forward and mirrored
// pair both get packed into a single gpu::TessVertexSpan.
m_resourceCounts.maxTessellatedSegmentCount = patchCount;
// outerCubic patches emit their tessellated geometry twice: once
// forward and once mirrored.
m_resourceCounts.outerCubicTessVertexCount =
gpu::ContourDirectionsAreDoubleSided(m_contourDirections)
? patchCount * OuterCubicPatchSegmentSpanPlusBowtie * 2
: patchCount * OuterCubicPatchSegmentSpanPlusBowtie;
if (m_coverageType != CoverageType::depthStencil)
{
// In depthStencil, the interior triangles are smuggled in with
// outerCubic patches (counted above) instead of being drawn
// from the triangle buffer.
m_resourceCounts.maxTriangleVertexCount += m_triangulator
->maxVertexCount(
m_pathFillRule);
}
}
}
else
{
// Submit grout triangles, retrofitted into outerCubic patches.
for (auto* node = m_triangulator->groutList().head(); node;
node = node->fNext)
{
tessWriter->pushRetrofitCubicTriStrip(
node->fPts,
3,
m_contourDirections,
contourIDWithFlags);
++patchCount;
}
if (m_coverageType == CoverageType::depthStencil)
{
// Smuggle the interior triangles in with outerCubic patches.
patchCount += m_triangulator->polysToRetrofitCubicPatches(
m_pathFillRule,
gpu::WindingFaces::all,
[&](const Vec2D* strip, size_t cornerCount) {
tessWriter->pushRetrofitCubicTriStrip(
strip,
cornerCount,
m_contourDirections,
contourIDWithFlags);
});
}
assert(contourCount == m_resourceCounts.contourCount);
assert(patchCount == m_resourceCounts.maxTessellatedSegmentCount);
assert(
patchCount * OuterCubicPatchSegmentSpanPlusBowtie * 2 ==
m_resourceCounts.outerCubicTessVertexCount ||
patchCount * OuterCubicPatchSegmentSpanPlusBowtie ==
m_resourceCounts.outerCubicTessVertexCount);
}
}
ImageRectDraw::ImageRectDraw(
RenderContext* context,
IAABB pixelBounds,
const Mat2D& matrix,
BlendMode blendMode,
float additiveness,
rcp<Texture> imageTexture,
rcp<const Gradient> gradient,
const ImageSampler imageSampler,
ColorInt modulatedColor,
const Mat2D& imageMatrix,
const Mat2D& gradientMatrix,
const Mat2D& inverseGradientTransform)
: Draw(
pixelBounds,
matrix,
&imageMatrix,
blendMode,
additiveness,
std::move(imageTexture),
imageSampler,
Type::imageRect)
, m_modulatedColor(modulatedColor)
, m_gradientMatrix(gradientMatrix)
, m_inverseGradientTransform(inverseGradientTransform)
, m_gradientRef(gradient.release())
{
// If we support image paints for paths, the client should draw a
// rectangular path with an image paint instead of using this draw.
assert(!context->frameSupportsImagePaintForPaths());
m_resourceCounts.imageRectCount = 1;
}
bool ImageRectDraw::allocateResources(RenderContext::LogicalFlush* flush)
{
if (!Draw::allocateResources(flush))
{
return false;
}
if (m_gradientRef != nullptr)
{
if (!flush->allocateGradient(m_gradientRef, &m_rampLocation))
{
return false;
}
}
return true;
}
void ImageRectDraw::releaseRefs()
{
Draw::releaseRefs();
safe_unref(m_gradientRef);
}
gpu::DrawBatch* ImageRectDraw::pushToRenderContext(
RenderContext::LogicalFlush* flush,
int signedSubpassIdx,
uint32_t zIndex)
{
assert(signedSubpassIdx == 0);
return &flush->pushImageRectDraw(this, subpassDrawContents(0), zIndex);
}
ImageMeshDrawBase::ImageMeshDrawBase(
IAABB pixelBounds,
const Mat2D& matrix,
BlendMode blendMode,
float additiveness,
rcp<Texture> imageTexture,
const ImageSampler imageSampler,
rcp<RenderBuffer> vertexBuffer,
rcp<RenderBuffer> uvBuffer,
rcp<RenderBuffer> indexBuffer,
uint32_t indexCount)
: Draw(
pixelBounds,
matrix,
nullptr,
blendMode,
additiveness,
std::move(imageTexture),
imageSampler,
Type::imageMesh)
, m_vertexBufferRef(vertexBuffer.release())
, m_uvBufferRef(uvBuffer.release())
, m_indexBufferRef(indexBuffer.release())
, m_indexCount(indexCount)
{
assert(m_vertexBufferRef != nullptr);
assert(m_uvBufferRef != nullptr);
assert(m_indexBufferRef != nullptr);
}
void ImageMeshDrawBase::releaseRefs()
{
Draw::releaseRefs();
m_vertexBufferRef->unref();
m_uvBufferRef->unref();
m_indexBufferRef->unref();
}
ImageMeshDraw::ImageMeshDraw(
IAABB pixelBounds,
const Mat2D& matrix,
BlendMode blendMode,
float additiveness,
rcp<Texture> imageTexture,
const ImageSampler imageSampler,
rcp<RenderBuffer> vertexBuffer,
rcp<RenderBuffer> uvBuffer,
rcp<RenderBuffer> indexBuffer,
uint32_t indexCount,
ColorInt modulatedColor,
const Vec2D& uvTranslate,
const Vec2D& uvScale)
: ImageMeshDrawBase(
pixelBounds,
matrix,
blendMode,
additiveness,
std::move(imageTexture),
imageSampler,
std::move(vertexBuffer),
std::move(uvBuffer),
std::move(indexBuffer),
indexCount)
, m_modulatedColor(modulatedColor)
, m_uvTranslate(uvTranslate)
, m_uvScale(uvScale)
{
m_resourceCounts.imageMeshCount = 1;
}
gpu::DrawBatch* ImageMeshDraw::pushToRenderContext(
RenderContext::LogicalFlush* flush,
int signedSubpassIdx,
uint32_t zIndex)
{
assert(signedSubpassIdx == 0);
return &flush->pushImageMeshDraw(this, subpassDrawContents(0), zIndex);
}
ImageMeshInstancedDraw::ImageMeshInstancedDraw(
IAABB pixelBounds,
const Mat2D& matrix,
rcp<Texture> imageTexture,
const ImageSampler imageSampler,
rcp<RenderBuffer> vertexBuffer,
rcp<RenderBuffer> uvBuffer,
rcp<RenderBuffer> indexBuffer,
uint32_t indexCount,
rcp<ImageMeshInstances> instances,
ColorInt modulatedColor,
float modulatedOpacity)
: ImageMeshDrawBase(
pixelBounds,
matrix,
BlendMode::srcOver,
0.0f, // Additiveness is per instance here.
std::move(imageTexture),
imageSampler,
std::move(vertexBuffer),
std::move(uvBuffer),
std::move(indexBuffer),
indexCount)
, m_instancesRef(instances.release())
, m_modulatedColor(modulatedColor)
, m_modulatedOpacity(modulatedOpacity)
{
assert(m_instancesRef != nullptr);
assert(m_instancesRef->count() > 0);
assert(!m_instancesRef->isEditing());
m_resourceCounts.imageMeshCount = m_instancesRef->count();
RIVE_DEBUG_CODE(m_editCount = m_instancesRef->editCount();)
}
gpu::DrawBatch* ImageMeshInstancedDraw::pushToRenderContext(
RenderContext::LogicalFlush* flush,
int signedSubpassIdx,
uint32_t zIndex)
{
assert(signedSubpassIdx == 0);
return &flush->pushImageMeshInstancedDraw(
this,
subpassDrawContents(0),
zIndex);
}
void ImageMeshInstancedDraw::releaseRefs()
{
ImageMeshDrawBase::releaseRefs();
m_instancesRef->unref();
}
ClipReset::ClipReset(
RenderContext* context,
uint32_t previousClipID,
gpu::DrawContents previousClipDrawContents,
ResetAction resetAction)
: Draw(
context->getClipContentBounds(previousClipID),
Mat2D{},
nullptr,
BlendMode::srcOver,
/*additiveness =*/0,
nullptr,
ImageSampler::LinearClamp(),
Type::stencilClipReset)
, m_previousClipID(previousClipID)
{
constexpr static gpu::DrawContents
FILL_RULE_FLAGS = gpu::DrawContents::nonZeroFill |
gpu::DrawContents::evenOddFill |
gpu::DrawContents::clockwiseFill;
m_combinedDrawContents |= previousClipDrawContents & FILL_RULE_FLAGS;
switch (resetAction)
{
case ResetAction::intersectPreviousClip:
m_combinedDrawContents |= gpu::DrawContents::activeClip;
[[fallthrough]];
case ResetAction::clearPreviousClip:
m_combinedDrawContents |= gpu::DrawContents::clipUpdate;
break;
}
m_resourceCounts.maxTriangleVertexCount = 6;
}
gpu::DrawBatch* ClipReset::pushToRenderContext(
RenderContext::LogicalFlush* flush,
int signedSubpassIdx,
uint32_t zIndex)
{
assert(signedSubpassIdx == 0);
return &flush->pushClipResetDraw(this, subpassDrawContents(0), zIndex);
}
} // namespace rive::gpu