| /* | |
| * Copyright 2022 Rive | |
| */ | |
| #include "rive/renderer/rive_renderer.hpp" | |
| #include "gradient.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/renderer/rive_render_paint.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); | |
| if (m_context->frameInterlockMode() != gpu::InterlockMode::depthStencil) | |
| { | |
| // For non-depthStencil modes, there will be a gap between this outer | |
| // stroke and the same shape if filled normally, so expand this paint | |
| // by a full texel to cover the gap. | |
| fillPaint->additionalOutset(1.0f); | |
| } | |
| // 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()); | |
| } | |
| 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, | |
| paint->getInverseGradientTransform()))); | |
| 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(¤tToNew)) | |
| { | |
| 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); | |
| // ImageRectDraw below takes the blend mode directly rather than through a | |
| // RiveRenderPaint, so fold additive here too. | |
| blendMode = foldAdditiveToSrcOver(blendMode); | |
| 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 | |
| Mat2D{}))); // inverseGradientTransform | |
| } | |
| } | |
| 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::applyLayerMask( | |
| const RenderImage* renderImage, | |
| ImageSampler imageSampler, | |
| LayerMaskMode mode) | |
| { | |
| RIVE_PROF_SCOPE_L(2) | |
| if (!m_context->frameSupportsLayerMask()) | |
| { | |
| // Leaves the layer unmasked. The alternative on a mode whose blend step | |
| // has not been taught the op is worse than unmasked: find_paint_color | |
| // would hand back the coverage texel and src-over would paint it over | |
| // the content. | |
| return; | |
| } | |
| LITE_RTTI_CAST_OR_RETURN(image, const RiveRenderImage*, renderImage); | |
| rcp<gpu::Texture> maskTexture = image->refTexture(); | |
| if (maskTexture == nullptr) | |
| { | |
| return; | |
| } | |
| if (m_renderStateStack.back().overallClipPixelBounds.empty()) | |
| { | |
| return; | |
| } | |
| // Same shape as the image-paint branch of drawImage: a unit rect scaled to | |
| // the mask's pixel extent. The caller has already put the renderer in the | |
| // canvas's own space, so this covers exactly the target. | |
| if (m_unitRectPath == nullptr) | |
| { | |
| m_unitRectPath = make_rcp<RiveRenderPath>(); | |
| m_unitRectPath->line({1, 0}); | |
| m_unitRectPath->line({1, 1}); | |
| m_unitRectPath->line({0, 1}); | |
| } | |
| save(); | |
| scale( | |
| static_cast<float>(image->width()), | |
| static_cast<float>(image->height())); | |
| RiveRenderPaint paint; | |
| paint.layerMask(std::move(maskTexture), mode); | |
| 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); | |
| // ImageMeshDraw below takes the blend mode directly rather than through a | |
| // RiveRenderPaint, so fold additive here too. | |
| blendMode = foldAdditiveToSrcOver(blendMode); | |
| 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), | |
| Vec2D{0.0f, 0.0f}, | |
| Vec2D{1.0f, 1.0f}))); | |
| } | |
| void RiveRenderer::drawImageMeshInstanced( | |
| const RenderImage* renderImage, | |
| ImageSampler imageSampler, | |
| rcp<RenderBuffer> vertices_f32, | |
| rcp<RenderBuffer> uvCoords_f32, | |
| rcp<RenderBuffer> indices_u16, | |
| uint32_t vertexCount, | |
| uint32_t indexCount, | |
| rcp<ImageMeshInstances> instances) | |
| { | |
| 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 (instances == nullptr || instances->count() == 0) | |
| { | |
| return; | |
| } | |
| if (m_renderStateStack.back().overallClipPixelBounds.empty()) | |
| { | |
| return; | |
| } | |
| if (m_context->frameInterlockMode() == gpu::InterlockMode::atomics) | |
| { | |
| // If we're in atomic mode, meshes can't self-overlap, so we can't | |
| // safely issue instanced calls without barriers since the individual | |
| // instances may overlap. As a workaround, issue individual draw calls | |
| // for each instance instead. This is slow, and I'm only doing it this | |
| // way because atomic mode is soon going away. | |
| for (const auto& instanceData : instances->instanceData()) | |
| { | |
| float finalOpacity = std::max( | |
| 0.0f, | |
| instanceData.opacity * m_renderStateStack.back().modulatedOpacity); | |
| clipAndPushDraw(gpu::DrawUniquePtr(m_context->make<gpu::ImageMeshDraw>( | |
| gpu::Draw::FULLSCREEN_PIXEL_BOUNDS, | |
| m_renderStateStack.back().matrix * instanceData.transform, | |
| BlendMode::srcOver, | |
| instanceData.additiveness, | |
| imageTexture, | |
| imageSampler, | |
| vertices_f32, | |
| uvCoords_f32, | |
| indices_u16, | |
| indexCount, | |
| modulated(0xFFFFFFFF, finalOpacity), | |
| instanceData.uvTranslate, | |
| instanceData.uvScale))); | |
| } | |
| } | |
| else | |
| { | |
| // Unlike in drawImageMesh, we wait to resolve the modulated | |
| // color/opacity because each instance has its own opacity, so we avoid | |
| // precision loss if we do the float -> u8 conversion after it's been | |
| // multiplied in. | |
| clipAndPushDraw( | |
| gpu::DrawUniquePtr(m_context->make<gpu::ImageMeshInstancedDraw>( | |
| gpu::Draw::FULLSCREEN_PIXEL_BOUNDS, | |
| m_renderStateStack.back().matrix, | |
| std::move(imageTexture), | |
| imageSampler, | |
| std::move(vertices_f32), | |
| std::move(uvCoords_f32), | |
| std::move(indices_u16), | |
| indexCount, | |
| std::move(instances), | |
| m_renderStateStack.back().modulatedColor, | |
| m_renderStateStack.back().modulatedOpacity))); | |
| } | |
| } | |
| 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, as well as outer strokes. | |
| // | |
| // 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 null means the clip path is empty and clips out everything. | |
| if (clipDraw == nullptr) | |
| { | |
| return ApplyClipResult::fullyClipped; | |
| } | |
| 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->combinedDrawContents(); | |
| 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 |