blob: 772ff2696a29cf96331c145519f9e13e9876d561 [file]
/*
* Copyright 2022 Rive
*/
#include "rive/renderer/rive_renderer.hpp"
#include "gradient.hpp"
#include "rive_render_paint.hpp"
#include "rive_render_path.hpp"
#include "rive/math/math_types.hpp"
#include "rive/math/simd.hpp"
#include "rive/renderer/rive_render_image.hpp"
#include "rive/profiler/profiler_macros.h"
namespace rive
{
static rcp<RiveRenderPath> invertClockwisePath(const RiveRenderPath* path,
FillRule pathFillRule,
const Mat2D& viewMatrix,
IAABB bounds);
bool RiveRenderer::IsAABB(const RawPath& path, AABB* result)
{
RIVE_PROF_SCOPE_L(3)
// Any quadrilateral begins with a move plus 3 lines.
constexpr static size_t kAABBVerbCount = 4;
constexpr static PathVerb aabbVerbs[kAABBVerbCount] = {PathVerb::move,
PathVerb::line,
PathVerb::line,
PathVerb::line};
Span<const PathVerb> verbs = path.verbs();
if (verbs.count() < kAABBVerbCount ||
memcmp(verbs.data(), aabbVerbs, sizeof(aabbVerbs)) != 0)
{
return false;
}
// Only accept extra verbs and points if every point after the quadrilateral
// is equal to p0.
Span<const Vec2D> pts = path.points();
for (size_t i = 4; i < pts.count(); ++i)
{
if (pts[i] != pts[0])
{
return false;
}
}
// We have a quadrilateral! Now check if it is an axis-aligned rectangle.
float4 corners = {pts[0].x, pts[0].y, pts[2].x, pts[2].y};
float4 oppositeCorners = {pts[1].x, pts[1].y, pts[3].x, pts[3].y};
if (simd::all(corners == oppositeCorners.zyxw) ||
simd::all(corners == oppositeCorners.xwzy))
{
float4 r = simd::join(simd::min(corners.xy, corners.zw),
simd::max(corners.xy, corners.zw));
simd::store(result, r);
return true;
}
return false;
}
RiveRenderer::ClipElement::ClipElement(const Mat2D& matrix_,
const RiveRenderPath* path_,
FillRule fillRule_,
IAABB pixelBounds_,
std::optional<StrokeParams> stroke_,
float feather_,
bool forceClosed_)
{
reset(matrix_,
path_,
fillRule_,
pixelBounds_,
stroke_,
feather_,
forceClosed_);
}
RiveRenderer::ClipElement::~ClipElement() {}
void RiveRenderer::ClipElement::reset(const Mat2D& matrix_,
const RiveRenderPath* path_,
FillRule fillRule_,
IAABB pixelBounds_,
std::optional<StrokeParams> stroke_,
float feather_,
bool forceClosed_)
{
matrix = matrix_;
rawPathMutationID = path_->getRawPathMutationID();
pathBounds = path_->getBounds();
path = ref_rcp(path_);
fillRule = fillRule_;
clipID = 0; // This gets initialized lazily.
pixelBounds = pixelBounds_;
stroke = stroke_;
feather = feather_;
forceClosed = forceClosed_;
}
bool RiveRenderer::ClipElement::isEquivalent(
const Mat2D& matrix_,
const RiveRenderPath* path_,
std::optional<StrokeParams> stroke_,
float feather_,
bool forceClosed_) const
{
if (stroke.has_value())
{
if (!stroke_.has_value() || *stroke != *stroke_)
{
return false;
}
}
else if (stroke_.has_value())
{
return false;
}
return matrix_ == matrix &&
path_->getRawPathMutationID() == rawPathMutationID &&
path_->getFillRule() == fillRule && feather == feather_ &&
forceClosed == forceClosed_;
}
RiveRenderer::RiveRenderer(gpu::RenderContext* context) : m_context(context) {}
RiveRenderer::~RiveRenderer() {}
void RiveRenderer::save()
{
// Copy the back of the stack before pushing, in case the vector grows and
// invalidates the reference.
RenderState copy = m_renderStateStack.back();
m_renderStateStack.push_back(copy);
}
void RiveRenderer::restore()
{
assert(m_renderStateStack.size() > 1);
assert(m_renderStateStack.back().clipStackHeight >=
m_renderStateStack[m_renderStateStack.size() - 2].clipStackHeight);
m_renderStateStack.pop_back();
}
void RiveRenderer::transform(const Mat2D& matrix)
{
m_renderStateStack.back().matrix =
m_renderStateStack.back().matrix * matrix;
}
void RiveRenderer::modulateOpacity(float opacity)
{
m_renderStateStack.back().modulatedOpacity =
std::max(0.0f, m_renderStateStack.back().modulatedOpacity * opacity);
}
ColorInt RiveRenderer::modulated(ColorInt color, float opacity) const
{
return colorModulate(color,
m_renderStateStack.back().modulatedColor,
opacity);
}
void RiveRenderer::modulateColor(ColorInt color, bool replace)
{
RenderState& state = m_renderStateStack.back();
state.modulatedColor =
replace ? color : colorModulate(state.modulatedColor, color);
}
void RiveRenderer::drawPath(RenderPath* renderPath, RenderPaint* renderPaint)
{
RIVE_PROF_SCOPE_L(2)
LITE_RTTI_CAST_OR_RETURN(path, RiveRenderPath*, renderPath);
LITE_RTTI_CAST_OR_RETURN(paint, RiveRenderPaint*, renderPaint);
if (path->getRawPath().empty())
{
return;
}
if (paint->getIsStroked() && m_context->frameDescriptor().strokesDisabled)
{
return;
}
if (!paint->getIsStroked() && m_context->frameDescriptor().fillsDisabled)
{
return;
}
if (paint->getIsStroked() &&
// Use inverse logic to ensure we abort when stroke thickness is NaN.
!(paint->getThickness() > 0))
{
return;
}
// Use inverse logic to ensure we abort when stroke thickness is NaN.
if (!(paint->getFeather() >= 0))
{
return;
}
if (m_renderStateStack.back().overallClipPixelBounds.empty())
{
return;
}
if (paint->getIsStroked() &&
paint->getStrokePosition() != StrokePosition::center)
{
save();
auto stroke = paint->getStrokeParams();
stroke.thickness *= 2.0f;
stroke.position = StrokePosition::center;
if (paint->getFeather() == 0.0f)
{
// Without a feather, we can clip by the stroke, with double its
// current thickness, then draw path into it.
IAABB pixelBounds;
clipPathImpl(path,
stroke,
paint->getFeather(),
ForceClosed::yes,
&pixelBounds);
// Make a version of the paint that is the same as the current one,
// except as a fill instead of stroke.
auto fillPaint = paint->clone();
fillPaint->style(RenderPaintStyle::fill);
if (paint->getStrokePosition() == StrokePosition::inside)
{
drawPath(renderPath, fillPaint.get());
}
else
{
assert(paint->getStrokePosition() == StrokePosition::outside);
// For outside strokes we need to fill the *inverse* of the
// path.
auto fillPath =
invertClockwisePath(path,
path->getFillRule(),
m_renderStateStack.back().matrix,
pixelBounds);
drawPath(fillPath.get(), fillPaint.get());
}
}
else
{
// For a feathered stroke we need to invert this: clip by the path
// (inverted for outer), then draw the (feathered) stroke
// TODO: A possible optimization here when we have stencil is to
// start with drawing the stroke, expanded by the feather amount (as
// thickness) into stencil. For large paths this would eliminate a
// lot of fill rate through the middle.
if (paint->getStrokePosition() == StrokePosition::inside)
{
clipPathImpl(path);
}
else
{
auto strokeBounds =
path->calculatePixelBounds(m_renderStateStack.back().matrix,
stroke,
paint->getFeather());
auto clip =
invertClockwisePath(path,
path->getFillRule(),
m_renderStateStack.back().matrix,
strokeBounds);
clipPathImpl(clip.get());
}
// Make a paint that has a centered stroke that's twice as thick,
// and force it to draw the stroke as closed.
auto thickenedCenteredStrokePaint = paint->clone();
thickenedCenteredStrokePaint->stroke(stroke);
thickenedCenteredStrokePaint->forceClosed(true);
drawPath(path, thickenedCenteredStrokePaint.get());
}
restore();
return;
}
Mat2D imageMatrix;
Mat2D* imageMatrixPtr = nullptr;
if (paint->getImageTexture() != nullptr)
{
if (!m_context->frameSupportsImagePaintForPaths())
{
// If we don't have image paint support we need to do this using
// ImageRect (which can do all of the drawing of image +
// color/gradient), clipped to the current path.
save();
AABB bounds;
if (!IsAABB(path->getRawPath(), &bounds) || paint->getIsStroked() ||
paint->getFeather() != 0.0f)
{
// If this is not an AABB directly we need to get the actual
// bounds and then clip against the path.
bounds = path->getBounds();
std::optional<StrokeParams> stroke;
if (paint->getIsStroked())
{
// We should only be here with centered strokes, since
// inner/outer strokes are handled above (with an early
// return)
assert(paint->getStrokePosition() ==
StrokePosition::center);
stroke = {
.thickness = paint->getThickness(),
.join = paint->getJoin(),
.cap = paint->getCap(),
};
}
float outset =
RiveRenderPath::calculateBoundsOutset(stroke,
paint->getFeather());
bounds = bounds.outset(outset, outset);
clipPathImpl(path, stroke, paint->getFeather());
}
// TODO: Multiply the paint's gradient matrix with this once it
// exists
const auto gradientMatrix = m_renderStateStack.back().matrix;
// ImageRectDraw draws as a unit square with upper-left corner of 0,
// 0 so we need to adjust our transform to set that up.
Mat2D adjust = Mat2D::fromScaleAndTranslation(bounds.width(),
bounds.height(),
bounds.left(),
bounds.top());
transform(adjust);
const auto& m = m_renderStateStack.back().matrix;
// The image matrix needs to map from the desired image space to the
// "box space" (where the upper-left and lower-right coordinates are
// (0, 0) and (1, 1) respectively). This is effectively the inverse
// of undoing the adjust matrix then applying the paint's
// imageMatrix, which becomes the following:
const auto imageMatrix =
paint->getImageTransform().invertOrIdentity() * adjust;
ColorInt paintColor = paint->getColor();
if (paint->getGradient() != nullptr)
{
// Paints with gradients have no color data so use solid white.
paintColor = 0xFFFFFFFF;
}
clipAndPushDraw(
gpu::DrawUniquePtr(m_context->make<gpu::ImageRectDraw>(
m_context,
m.mapBoundingBox(AABB{0, 0, 1, 1}).roundOut(),
m,
paint->getBlendMode(),
paint->getAdditiveness(),
ref_rcp(paint->getImageTexture()),
ref_rcp(paint->getGradient()),
paint->getImageSampler(),
modulated(paintColor,
m_renderStateStack.back().modulatedOpacity),
imageMatrix,
gradientMatrix)));
restore();
return;
}
imageMatrix =
m_renderStateStack.back().matrix * paint->getImageTransform();
imageMatrixPtr = &imageMatrix;
}
if (paint->getFeather() != 0 && !paint->getIsStroked())
{
if (path->getFillRule() != FillRule::clockwise &&
!m_context->frameDescriptor().clockwiseFillOverride)
{
// Don't draw feathered fills that aren't clockwise.
return;
}
float matrixMaxScale = m_renderStateStack.back().matrix.findMaxScale();
if (paint->getFeather() * matrixMaxScale > 1)
{
clipAndPushDraw(gpu::PathDraw::Make(
m_context,
m_renderStateStack.back().matrix,
imageMatrixPtr,
path->makeSoftenedCopyForFeathering(paint->getFeather(),
matrixMaxScale),
path->getFillRule(),
paint,
m_renderStateStack.back().modulatedOpacity,
m_renderStateStack.back().modulatedColor));
return;
}
}
clipAndPushDraw(
gpu::PathDraw::Make(m_context,
m_renderStateStack.back().matrix,
imageMatrixPtr,
ref_rcp(path),
path->getFillRule(),
paint,
m_renderStateStack.back().modulatedOpacity,
m_renderStateStack.back().modulatedColor));
}
void RiveRenderer::clipPath(RenderPath* renderPath)
{
RIVE_PROF_SCOPE_L(2)
LITE_RTTI_CAST_OR_RETURN(path, RiveRenderPath*, renderPath);
if (m_renderStateStack.back().overallClipPixelBounds.empty())
{
return;
}
if (path->getRawPath().empty())
{
m_renderStateStack.back().overallClipPixelBounds = {};
return;
}
// First try to handle axis-aligned rectangles using the "ENABLE_CLIP_RECT"
// shader feature. Multiple axis-aligned rectangles can be intersected into
// a single rectangle if their matrices are compatible.
AABB clipRectCandidate;
if (m_context->frameSupportsClipRects() &&
IsAABB(path->getRawPath(), &clipRectCandidate))
{
clipRectImpl(clipRectCandidate, path);
}
else
{
clipPathImpl(path);
}
}
void RiveRenderer::clipStroke(RenderPath* renderPath,
const StrokeParams& params)
{
RIVE_PROF_SCOPE_L(2)
LITE_RTTI_CAST_OR_RETURN(path, RiveRenderPath*, renderPath);
if (m_renderStateStack.back().overallClipPixelBounds.empty())
{
return;
}
if (path->getRawPath().empty())
{
m_renderStateStack.back().overallClipPixelBounds = {};
return;
}
// For centered strokes we can just clip against the path.
clipPathImpl(path, params);
}
// Finds a new rect, if such a rect exists, such that:
//
// currentMatrix * rect == newMatrix * newRect
//
// Returns true if *rect was replaced with newRect.
static bool transform_rect_to_new_space(AABB* rect,
const Mat2D& currentMatrix,
const Mat2D& newMatrix)
{
if (currentMatrix == newMatrix)
{
return true;
}
Mat2D currentToNew;
if (!newMatrix.invert(&currentToNew))
{
return false;
}
currentToNew = currentToNew * currentMatrix;
float maxSkew = fmaxf(fabsf(currentToNew.xy()), fabsf(currentToNew.yx()));
float maxScale = fmaxf(fabsf(currentToNew.xx()), fabsf(currentToNew.yy()));
if (maxSkew > math::EPSILON && maxScale > math::EPSILON)
{
// Transforming this rect to the new view matrix would turn it into
// something that isn't a rect.
return false;
}
Vec2D pts[2] = {{rect->left(), rect->top()},
{rect->right(), rect->bottom()}};
currentToNew.mapPoints(pts, pts, 2);
float4 p = simd::load4f(pts);
float2 topLeft = simd::min(p.xy, p.zw);
float2 botRight = simd::max(p.xy, p.zw);
*rect = {topLeft.x, topLeft.y, botRight.x, botRight.y};
return true;
}
void RiveRenderer::clipRectImpl(AABB rect, const RiveRenderPath* originalPath)
{
RIVE_PROF_SCOPE_L(3)
auto& renderState = m_renderStateStack.back();
bool hasClipRect = renderState.clipRectInverseMatrix != nullptr;
if (rect.isEmptyOrNaN())
{
renderState.overallClipPixelBounds = {};
return;
}
// If there already is a clipRect, we can only accept another one by
// intersecting it with the existing one. This means the new rect must be
// axis-aligned with the existing clipRect.
if (hasClipRect && !transform_rect_to_new_space(&rect,
renderState.matrix,
renderState.clipRectMatrix))
{
// 'rect' is not axis-aligned with the existing clipRect. Fall back to
// clipPath.
clipPathImpl(originalPath);
return;
}
if (!hasClipRect)
{
// There wasn't an existing clipRect. This is the one!
renderState.clipRect = rect;
renderState.clipRectMatrix = renderState.matrix;
}
else
{
// Both rects are in the same space now. Intersect the two
// geometrically.
float4 a = simd::load4f(&renderState.clipRect);
float4 b = simd::load4f(&rect);
float4 intersection =
simd::join(simd::max(a.xy, b.xy), simd::min(a.zw, b.zw));
simd::store(&renderState.clipRect, intersection);
}
// Grab the pixel bounds of the new combined (intersected) clip rect
renderState.clipRectPixelBounds =
renderState.clipRectMatrix.mapBoundingBox(renderState.clipRect)
.roundOut();
renderState.overallClipPixelBounds =
renderState.overallClipPixelBounds.intersect(
renderState.clipRectPixelBounds);
renderState.clipRectInverseMatrix =
m_context->make<gpu::ClipRectInverseMatrix>(renderState.clipRectMatrix,
renderState.clipRect);
}
void RiveRenderer::clipPathImpl(const RiveRenderPath* path,
std::optional<StrokeParams> stroke,
float feather,
ForceClosed forceClosed,
IAABB* boundsOut)
{
RIVE_PROF_SCOPE_L(3)
auto& renderState = m_renderStateStack.back();
if (path->getBounds().isEmptyOrNaN())
{
if (boundsOut != nullptr)
{
*boundsOut = IAABB{};
}
renderState.overallClipPixelBounds = {};
return;
}
if (stroke.has_value() && stroke->position != StrokePosition::center)
{
// To handle an inner or outer stroke it's actually two clips: we need
// to clip by the stroke then *additionally* the path by itself (which
// needs to be inverted for outside strokes)
auto clipStroke = *stroke;
clipStroke.thickness *= 2.0f;
clipStroke.position = StrokePosition::center;
IAABB outerBounds;
clipPathImpl(path, clipStroke, 0.0f, ForceClosed::yes, &outerBounds);
if (boundsOut != nullptr)
{
*boundsOut = outerBounds;
}
if (stroke->position == StrokePosition::inside)
{
clipPathImpl(path);
}
else
{
// TODO: There's an optimization that could be done here in some
// render modes where instead of inverting the path we could instead
// render them "subtractively" (in depthStencil this would be
// rendering with a "clear the upper bit" flag)
assert(stroke->position == StrokePosition::outside);
auto inverted =
invertClockwisePath(path,
path->getFillRule(),
m_renderStateStack.back().matrix,
outerBounds);
clipPath(inverted.get());
}
return;
}
// Only write a new clip element if this path isn't already on the stack
// from before. e.g.:
//
// clipPath(samePath);
// restore();
// save();
// clipPath(samePath); // <-- reuse the ClipElement (and clipID!)
// already in m_clipStack.
//
const size_t clipStackHeight = renderState.clipStackHeight;
assert(m_clipStack.size() >= clipStackHeight);
if (m_clipStack.size() == clipStackHeight ||
!m_clipStack[clipStackHeight].isEquivalent(renderState.matrix,
path,
stroke,
feather,
bool(forceClosed)))
{
// Calculate the pixel bounds for this clip path before we push it into
// the stack to ensure that we even need to do so
const auto pixelBounds =
path->calculatePixelBounds(renderState.matrix, stroke, feather);
renderState.overallClipPixelBounds =
renderState.overallClipPixelBounds.intersect(pixelBounds);
if (boundsOut != nullptr)
{
*boundsOut = pixelBounds;
}
if (renderState.overallClipPixelBounds.empty())
{
// Nothing can draw under this, so no need to add to the stack.
return;
}
m_clipStack.resize(clipStackHeight);
m_clipStack.emplace_back(renderState.matrix,
path,
path->getFillRule(),
pixelBounds,
stroke,
feather,
bool(forceClosed));
}
else
{
if (boundsOut != nullptr)
{
*boundsOut = m_clipStack[clipStackHeight].pixelBounds;
}
// We are going to reuse the element that is already in the clip stack,
// but need to re-update the overall clip pixel bounds.
renderState.overallClipPixelBounds =
renderState.overallClipPixelBounds.intersect(
m_clipStack[clipStackHeight].pixelBounds);
if (renderState.overallClipPixelBounds.empty())
{
// Nothing can draw under this, so no need to increment the stack
// height.
return;
}
}
renderState.clipStackHeight = clipStackHeight + 1;
}
void RiveRenderer::drawImage(const RenderImage* renderImage,
ImageSampler imageSampler,
BlendMode blendMode,
float opacity)
{
drawImage(renderImage, imageSampler, blendMode, opacity, 0);
}
void RiveRenderer::drawImage(const RenderImage* renderImage,
ImageSampler imageSampler,
BlendMode blendMode,
float opacity,
float additiveness)
{
RIVE_PROF_SCOPE_L(2)
LITE_RTTI_CAST_OR_RETURN(image, const RiveRenderImage*, renderImage);
rcp<gpu::Texture> imageTexture = image->refTexture();
if (imageTexture == nullptr)
{
// imageTexture may be null if the backend uses a custom factory and/or
// updates out-of-band assets asynchronously. If there's no texture yet,
// just don't draw anything.
return;
}
// Apply modulated opacity (clamp to prevent negative values)
float finalOpacity =
std::max(0.0f, opacity * m_renderStateStack.back().modulatedOpacity);
// Scale the view matrix so we can draw this image as the rect [0, 0, 1, 1].
save();
scale(image->width(), image->height());
if (!m_context->frameSupportsImagePaintForPaths())
{
// Fall back on ImageRectDraw if the current frame doesn't support
// drawing paths with image paints.
if (!m_renderStateStack.back().overallClipPixelBounds.empty())
{
const Mat2D& m = m_renderStateStack.back().matrix;
clipAndPushDraw(
gpu::DrawUniquePtr(m_context->make<gpu::ImageRectDraw>(
m_context,
m.mapBoundingBox(AABB{0, 0, 1, 1}).roundOut(),
m,
blendMode,
additiveness,
std::move(imageTexture),
nullptr, // gradient
imageSampler,
modulated(0xFFFFFFFF, finalOpacity),
Mat2D{}, // imageMatrix
Mat2D{}))); // gradientMatrix
}
}
else
{
// Implement drawImage() as drawPath() with a rectangular path and an
// image paint.
if (m_unitRectPath == nullptr)
{
m_unitRectPath = make_rcp<RiveRenderPath>();
m_unitRectPath->line({1, 0});
m_unitRectPath->line({1, 1});
m_unitRectPath->line({0, 1});
}
RiveRenderPaint paint;
paint.image(std::move(imageTexture), finalOpacity);
paint.blendMode(blendMode);
paint.additiveness(additiveness);
paint.imageSampler(imageSampler);
drawPath(m_unitRectPath.get(), &paint);
}
restore();
}
void RiveRenderer::drawImageMesh(const RenderImage* renderImage,
ImageSampler imageSampler,
rcp<RenderBuffer> vertices_f32,
rcp<RenderBuffer> uvCoords_f32,
rcp<RenderBuffer> indices_u16,
uint32_t vertexCount,
uint32_t indexCount,
BlendMode blendMode,
float opacity)
{
drawImageMesh(renderImage,
imageSampler,
std::move(vertices_f32),
std::move(uvCoords_f32),
std::move(indices_u16),
vertexCount,
indexCount,
blendMode,
opacity,
0);
}
void RiveRenderer::drawImageMesh(const RenderImage* renderImage,
ImageSampler imageSampler,
rcp<RenderBuffer> vertices_f32,
rcp<RenderBuffer> uvCoords_f32,
rcp<RenderBuffer> indices_u16,
uint32_t vertexCount,
uint32_t indexCount,
BlendMode blendMode,
float opacity,
float additiveness)
{
RIVE_PROF_SCOPE_L(2)
LITE_RTTI_CAST_OR_RETURN(image, const RiveRenderImage*, renderImage);
rcp<gpu::Texture> imageTexture = image->refTexture();
if (imageTexture == nullptr)
{
// imageTexture may be null if the backend uses a custom factory and/or
// updates out-of-band assets asynchronously. If there's no texture yet,
// just don't draw anything.
return;
}
assert(vertices_f32);
assert(uvCoords_f32);
assert(indices_u16);
if (m_renderStateStack.back().overallClipPixelBounds.empty())
{
return;
}
// Apply modulated opacity (clamp to prevent negative values)
float finalOpacity =
std::max(0.0f, opacity * m_renderStateStack.back().modulatedOpacity);
clipAndPushDraw(gpu::DrawUniquePtr(m_context->make<gpu::ImageMeshDraw>(
gpu::Draw::FULLSCREEN_PIXEL_BOUNDS,
m_renderStateStack.back().matrix,
blendMode,
additiveness,
std::move(imageTexture),
imageSampler,
std::move(vertices_f32),
std::move(uvCoords_f32),
std::move(indices_u16),
indexCount,
modulated(0xFFFFFFFF, finalOpacity))));
}
void RiveRenderer::clipAndPushDraw(gpu::DrawUniquePtr draw)
{
RIVE_PROF_SCOPE_L(3)
assert(!m_renderStateStack.back().overallClipPixelBounds.empty());
if (draw.get() == nullptr)
{
return;
}
if (m_context->isOutsideCurrentFrame(draw->pixelBounds()))
{
return;
}
// Make two attempts to issue the draw: once on the context as-is and once
// with a clean flush.
for (int i = 0; i < 2; ++i)
{
// Always make sure we begin this loop with the internal draw batch
// empty, and clear it when we're done.
struct AutoResetInternalDrawBatch
{
public:
AutoResetInternalDrawBatch(RiveRenderer* renderer) :
m_renderer(renderer)
{
assert(m_renderer->m_internalDrawBatch.empty());
}
~AutoResetInternalDrawBatch()
{
m_renderer->m_internalDrawBatch.clear();
}
private:
RiveRenderer* m_renderer;
};
AutoResetInternalDrawBatch aridb(this);
auto applyClipResult = applyClip(draw.get());
if (applyClipResult == ApplyClipResult::failure)
{
// There wasn't room in the GPU buffers for this path draw. Flush
// and try again.
m_context->logicalFlush();
continue;
}
else if (applyClipResult == ApplyClipResult::fullyClipped)
{
return;
}
m_internalDrawBatch.push_back(std::move(draw));
if (!m_context->pushDraws(m_internalDrawBatch.data(),
m_internalDrawBatch.size()))
{
// There wasn't room in the GPU buffers for this path draw. Flush
// and try again.
m_context->logicalFlush();
// Reclaim "draw" because we will use it again on the next
// iteration.
draw = std::move(m_internalDrawBatch.back());
assert(draw != nullptr);
m_internalDrawBatch.pop_back();
continue;
}
// Success!
return;
}
// We failed to process the draw. Release its refs.
fprintf(stderr,
"RiveRenderer::clipAndPushDraw failed. The draw and/or clip stack "
"are too complex.\n");
}
// Used by clipping in clockwiseAtomic mode.
//
// Returns the inverse of a path, meaning, regions that were filled in the old
// path are now empty, and empty regions in the old path are now filled.
//
// NOTE: A true inverse path would expand infinitely in all directions, but this
// function limits it by the provided "bounds".
//
// NOTE: The returned path is always clockwise, even if the given path was not.
// If the given path is not clockwise, we attempt to convert it to clockwise
// based on its dominant winding direction. This may or may not be accurate.
static rcp<RiveRenderPath> invertClockwisePath(const RiveRenderPath* path,
FillRule pathFillRule,
const Mat2D& viewMatrix,
IAABB bounds)
{
auto inversePath = make_rcp<RiveRenderPath>();
inversePath->fillRule(FillRule::clockwise);
Mat2D viewInverseMatrix;
if (viewMatrix.invert(&viewInverseMatrix))
{
// Add the pre-viewMatrix "bounds" rect to the new path.
std::array<Vec2D, 4> boundsVertices = {
Vec2D(bounds.left, bounds.top),
Vec2D(bounds.right, bounds.top),
Vec2D(bounds.right, bounds.bottom),
Vec2D(bounds.left, bounds.bottom),
};
viewInverseMatrix.mapPoints(boundsVertices.data(),
boundsVertices.data(),
4);
inversePath->move(boundsVertices[0]);
if (const float viewMatrixDeterminant =
viewMatrix[0] * viewMatrix[3] - viewMatrix[2] * viewMatrix[1];
viewMatrixDeterminant >= 0)
{
inversePath->line(boundsVertices[1]);
inversePath->line(boundsVertices[2]);
inversePath->line(boundsVertices[3]);
}
else
{
inversePath->line(boundsVertices[3]);
inversePath->line(boundsVertices[2]);
inversePath->line(boundsVertices[1]);
}
// Subtract the given path out of the bounds rect.
if (pathFillRule == FillRule::clockwise || path->getCoarseArea() >= 0)
{
inversePath->addRenderPathBackwards(path, Mat2D());
}
else
{
inversePath->addRenderPath(path, Mat2D());
}
}
return inversePath;
}
RiveRenderer::ApplyClipResult RiveRenderer::applyClip(gpu::Draw* draw)
{
RIVE_PROF_SCOPE_L(3)
auto& renderState = m_renderStateStack.back();
draw->setClipRect(renderState.clipRectInverseMatrix,
renderState.overallClipPixelBounds);
if (draw->clippedPixelBounds().empty() ||
m_context->isOutsideCurrentFrame(draw->clippedPixelBounds()))
{
// Either this draw is outside of the current frame (in which case it's
// not visible) or it was completely clipped by the current overall clip
// bounds.
return ApplyClipResult::fullyClipped;
}
const size_t clipStackHeight = renderState.clipStackHeight;
if (clipStackHeight == 0)
{
assert(draw->clipID() == 0);
return ApplyClipResult::success;
}
// Find which clip element in the stack (if any) is currently rendered to
// the clip buffer.
size_t clipIdxCurrentlyInClipBuffer = -1; // i.e., "none".
if (m_context->getClipContentID() != 0)
{
for (size_t i = clipStackHeight - 1; i != -1; --i)
{
if (m_clipStack[i].clipID == m_context->getClipContentID())
{
clipIdxCurrentlyInClipBuffer = i;
break;
}
}
}
// Draw the necessary updates to the clip buffer (i.e., draw every clip
// element after clipIdxCurrentlyInClipBuffer).
uint32_t parentClipID =
clipIdxCurrentlyInClipBuffer == -1
? 0 // The next clip to be drawn is not nested.
: m_clipStack[clipIdxCurrentlyInClipBuffer].clipID;
if (m_context->frameInterlockMode() ==
gpu::InterlockMode::clockwiseAtomic ||
m_context->frameInterlockMode() == gpu::InterlockMode::depthStencil)
{
if (parentClipID == 0 && m_context->getClipContentID() != 0)
{
// Time for a new stencil clip! Erase the clip currently in the
// stencil buffer before we draw the new one.
auto stencilClipClear =
gpu::DrawUniquePtr(m_context->make<gpu::ClipReset>(
m_context,
m_context->getClipContentID(),
gpu::DrawContents::none,
gpu::ClipReset::ResetAction::clearPreviousClip));
if (!m_context->isOutsideCurrentFrame(
stencilClipClear->pixelBounds()))
{
m_internalDrawBatch.push_back(std::move(stencilClipClear));
}
}
}
for (size_t i = clipIdxCurrentlyInClipBuffer + 1; i < clipStackHeight; ++i)
{
ClipElement& clip = m_clipStack[i];
assert(clip.pathBounds == clip.path->getBounds());
IAABB clipDrawBounds;
RiveRenderPaint clipUpdatePaint;
clipUpdatePaint.clipUpdate(
/*clip THIS clipDraw against:*/ parentClipID);
clipUpdatePaint.feather(clip.feather);
if (clip.stroke.has_value())
{
clipUpdatePaint.stroke(*clip.stroke);
}
clipUpdatePaint.forceClosed(clip.forceClosed);
rcp clipPath = clip.path;
FillRule clipFillRule = clip.fillRule;
std::optional pixelBounds = clip.pixelBounds;
if (m_context->frameInterlockMode() ==
gpu::InterlockMode::clockwiseAtomic &&
parentClipID != 0)
{
// clockwiseAtomic implements nested clips by erasing the inverse
// of the inner path from the outer clip.
clipPath = invertClockwisePath(
clipPath.get(),
clipFillRule,
clip.matrix,
m_context->getClipContentBounds(parentClipID));
clipFillRule = FillRule::clockwise;
// Clear this because the inverted path has different pixel bounds
// (as it now contains the clip content bounds as a box around the
// path)
pixelBounds = std::nullopt;
}
gpu::DrawUniquePtr clipDraw =
gpu::PathDraw::Make(m_context,
clip.matrix,
nullptr, // imageMatrix, unneeded for clips
std::move(clipPath),
clipFillRule,
&clipUpdatePaint,
1.0f, // no modulation for clips
0xFFFFFFFF,
pixelBounds);
// We have already validated that the clip path is within the screen
// bounds, so this should never return null (which is the "this is
// offscreen" result).
assert(clipDraw != nullptr);
clipDrawBounds = clipDraw->pixelBounds();
// Generate a new clipID every time we (re-)render an element to the
// clip buffer. (Each embodiment of the element needs its own
// separate readBounds.)
{
// if we have a parent, use its current adjusted write bounds as its
// outer bounds, otherwise limit it to the screen area.
// TODO: This should take into account any clip rect that might be
// applied.
const auto outerBounds =
(parentClipID != 0)
? m_context->getTightenedClipBounds(parentClipID)
: AABBu16::MakeWH(
m_context->frameDescriptor().renderTargetWidth,
m_context->frameDescriptor().renderTargetHeight);
// Trim the draw bounds to the outer bounds as the initial minimal
// clip bounds (we shouldn't need to write to or read from anywhere
// that is outside of the screen or a parent clip's box, if one
// exists).
const auto tightenedBounds = outerBounds.intersect(clipDrawBounds);
// If there is a parent clip, the next element up the clip stack
// should have its ID.
assert(parentClipID == 0 ||
(i != 0 && m_clipStack[i - 1].clipID == parentClipID));
clip.clipID = m_context->generateClipID(clipDrawBounds,
parentClipID,
tightenedBounds);
}
assert(clip.clipID != m_context->getClipContentID());
if (clip.clipID == 0)
{
return ApplyClipResult::failure; // The context is out of
// clipIDs. We will flush and
// try again.
}
clipDraw->setClipID(clip.clipID);
gpu::DrawContents clipDrawContents = clipDraw->drawContents();
if (!m_context->isOutsideCurrentFrame(clipDrawBounds))
{
m_internalDrawBatch.push_back(std::move(clipDraw));
}
if (parentClipID != 0)
{
if (m_context->frameInterlockMode() ==
gpu::InterlockMode::depthStencil)
{
// When drawing nested stencil clips, we need to intersect them,
// which involves erasing the region of the current clip in the
// stencil buffer that is outside the the one we just drew.
auto stencilClipIntersect =
gpu::DrawUniquePtr(m_context->make<gpu::ClipReset>(
m_context,
parentClipID,
clipDrawContents,
gpu::ClipReset::ResetAction::intersectPreviousClip));
if (!m_context->isOutsideCurrentFrame(
stencilClipIntersect->pixelBounds()))
{
m_internalDrawBatch.push_back(
std::move(stencilClipIntersect));
}
}
}
parentClipID = clip.clipID; // Nest the next clip (if any) inside the
// one we just rendered.
}
assert(parentClipID == m_clipStack[clipStackHeight - 1].clipID);
draw->setClipID(parentClipID);
m_context->setClipContentID(parentClipID);
return ApplyClipResult::success;
}
} // namespace rive