| /* |
| * Copyright 2022 Rive |
| */ |
| |
| #include "rive/renderer/rive_renderer.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 |
| { |
| bool RiveRenderer::IsAABB(const RawPath& path, AABB* result) |
| { |
| RIVE_PROF_SCOPE() |
| // 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_) |
| { |
| reset(matrix_, path_, fillRule_); |
| } |
| |
| RiveRenderer::ClipElement::~ClipElement() {} |
| |
| void RiveRenderer::ClipElement::reset(const Mat2D& matrix_, |
| const RiveRenderPath* path_, |
| FillRule fillRule_) |
| { |
| matrix = matrix_; |
| rawPathMutationID = path_->getRawPathMutationID(); |
| pathBounds = path_->getBounds(); |
| path = ref_rcp(path_); |
| fillRule = fillRule_; |
| clipID = 0; // This gets initialized lazily. |
| } |
| |
| bool RiveRenderer::ClipElement::isEquivalent(const Mat2D& matrix_, |
| const RiveRenderPath* path_) const |
| { |
| return matrix_ == matrix && |
| path_->getRawPathMutationID() == rawPathMutationID && |
| path_->getFillRule() == fillRule; |
| } |
| |
| 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_stack.back(); |
| m_stack.push_back(copy); |
| } |
| |
| void RiveRenderer::restore() |
| { |
| assert(m_stack.size() > 1); |
| assert(m_stack.back().clipStackHeight >= |
| m_stack[m_stack.size() - 2].clipStackHeight); |
| m_stack.pop_back(); |
| } |
| |
| void RiveRenderer::transform(const Mat2D& matrix) |
| { |
| m_stack.back().matrix = m_stack.back().matrix * matrix; |
| } |
| |
| void RiveRenderer::drawPath(RenderPath* renderPath, RenderPaint* renderPaint) |
| { |
| RIVE_PROF_SCOPE() |
| 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_stack.back().clipIsEmpty) |
| { |
| return; |
| } |
| |
| 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_stack.back().matrix.findMaxScale(); |
| if (paint->getFeather() * matrixMaxScale > 1) |
| { |
| clipAndPushDraw(gpu::PathDraw::Make( |
| m_context, |
| m_stack.back().matrix, |
| path->makeSoftenedCopyForFeathering(paint->getFeather(), |
| matrixMaxScale), |
| path->getFillRule(), |
| paint, |
| &m_scratchPath)); |
| return; |
| } |
| } |
| |
| clipAndPushDraw(gpu::PathDraw::Make(m_context, |
| m_stack.back().matrix, |
| ref_rcp(path), |
| path->getFillRule(), |
| paint, |
| &m_scratchPath)); |
| } |
| |
| void RiveRenderer::clipPath(RenderPath* renderPath) |
| { |
| RIVE_PROF_SCOPE() |
| LITE_RTTI_CAST_OR_RETURN(path, RiveRenderPath*, renderPath); |
| |
| if (m_context->frameInterlockMode() == gpu::InterlockMode::clockwiseAtomic) |
| { |
| // Just discard clips in clockwiseAtomic mode for now. |
| // TODO: Implement clipping in clockwiseAtomic mode. |
| return; |
| } |
| |
| if (m_stack.back().clipIsEmpty) |
| { |
| return; |
| } |
| |
| if (path->getRawPath().empty()) |
| { |
| m_stack.back().clipIsEmpty = true; |
| 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); |
| } |
| } |
| |
| // 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() |
| bool hasClipRect = m_stack.back().clipRectInverseMatrix != nullptr; |
| if (rect.isEmptyOrNaN()) |
| { |
| m_stack.back().clipIsEmpty = true; |
| 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, |
| m_stack.back().matrix, |
| m_stack.back().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! |
| m_stack.back().clipRect = rect; |
| m_stack.back().clipRectMatrix = m_stack.back().matrix; |
| } |
| else |
| { |
| // Both rects are in the same space now. Intersect the two |
| // geometrically. |
| float4 a = simd::load4f(&m_stack.back().clipRect); |
| float4 b = simd::load4f(&rect); |
| float4 intersection = |
| simd::join(simd::max(a.xy, b.xy), simd::min(a.zw, b.zw)); |
| simd::store(&m_stack.back().clipRect, intersection); |
| } |
| |
| m_stack.back().clipRectInverseMatrix = |
| m_context->make<gpu::ClipRectInverseMatrix>( |
| m_stack.back().clipRectMatrix, |
| m_stack.back().clipRect); |
| } |
| |
| void RiveRenderer::clipPathImpl(const RiveRenderPath* path) |
| { |
| RIVE_PROF_SCOPE() |
| if (path->getBounds().isEmptyOrNaN()) |
| { |
| m_stack.back().clipIsEmpty = true; |
| 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 = m_stack.back().clipStackHeight; |
| assert(m_clipStack.size() >= clipStackHeight); |
| if (m_clipStack.size() == clipStackHeight || |
| !m_clipStack[clipStackHeight].isEquivalent(m_stack.back().matrix, path)) |
| { |
| m_clipStack.resize(clipStackHeight); |
| m_clipStack.emplace_back(m_stack.back().matrix, |
| path, |
| path->getFillRule()); |
| } |
| m_stack.back().clipStackHeight = clipStackHeight + 1; |
| } |
| |
| void RiveRenderer::drawImage(const RenderImage* renderImage, |
| ImageSampler imageSampler, |
| BlendMode blendMode, |
| float opacity) |
| { |
| RIVE_PROF_SCOPE() |
| 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; |
| } |
| |
| // 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_stack.back().clipIsEmpty) |
| { |
| const Mat2D& m = m_stack.back().matrix; |
| clipAndPushDraw( |
| gpu::DrawUniquePtr(m_context->make<gpu::ImageRectDraw>( |
| m_context, |
| m.mapBoundingBox(AABB{0, 0, 1, 1}).roundOut(), |
| m, |
| blendMode, |
| std::move(imageTexture), |
| imageSampler, |
| opacity))); |
| } |
| } |
| 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), opacity); |
| paint.blendMode(blendMode); |
| 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) |
| { |
| RIVE_PROF_SCOPE() |
| 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_stack.back().clipIsEmpty) |
| { |
| return; |
| } |
| |
| clipAndPushDraw(gpu::DrawUniquePtr( |
| m_context->make<gpu::ImageMeshDraw>(gpu::Draw::FULLSCREEN_PIXEL_BOUNDS, |
| m_stack.back().matrix, |
| blendMode, |
| std::move(imageTexture), |
| imageSampler, |
| std::move(vertices_f32), |
| std::move(uvCoords_f32), |
| std::move(indices_u16), |
| indexCount, |
| opacity))); |
| } |
| |
| void RiveRenderer::clipAndPushDraw(gpu::DrawUniquePtr draw) |
| { |
| RIVE_PROF_SCOPE() |
| assert(!m_stack.back().clipIsEmpty); |
| 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::clipEmpty) |
| { |
| 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"); |
| } |
| |
| RiveRenderer::ApplyClipResult RiveRenderer::applyClip(gpu::Draw* draw) |
| { |
| RIVE_PROF_SCOPE() |
| if (m_stack.back().clipIsEmpty) |
| { |
| return ApplyClipResult::clipEmpty; |
| } |
| draw->setClipRect(m_stack.back().clipRectInverseMatrix); |
| |
| const size_t clipStackHeight = m_stack.back().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 lastClipID = |
| clipIdxCurrentlyInClipBuffer == -1 |
| ? 0 // The next clip to be drawn is not nested. |
| : m_clipStack[clipIdxCurrentlyInClipBuffer].clipID; |
| if (m_context->frameInterlockMode() == gpu::InterlockMode::msaa) |
| { |
| if (lastClipID == 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::StencilClipReset>( |
| m_context, |
| m_context->getClipContentID(), |
| gpu::DrawContents::none, |
| gpu::StencilClipReset::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:*/ lastClipID); |
| |
| gpu::DrawUniquePtr clipDraw = gpu::PathDraw::Make(m_context, |
| clip.matrix, |
| clip.path, |
| clip.fillRule, |
| &clipUpdatePaint, |
| &m_scratchPath); |
| |
| if (clipDraw == nullptr) |
| { |
| return ApplyClipResult::clipEmpty; |
| } |
| 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.) |
| clip.clipID = m_context->generateClipID(clipDrawBounds); |
| 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 (lastClipID != 0) |
| { |
| m_context->addClipReadBounds(lastClipID, clipDrawBounds); |
| if (m_context->frameInterlockMode() == gpu::InterlockMode::msaa) |
| { |
| // 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::StencilClipReset>( |
| m_context, |
| lastClipID, |
| clipDrawContents, |
| gpu::StencilClipReset::ResetAction:: |
| intersectPreviousClip)); |
| if (!m_context->isOutsideCurrentFrame( |
| stencilClipIntersect->pixelBounds())) |
| { |
| m_internalDrawBatch.push_back( |
| std::move(stencilClipIntersect)); |
| } |
| } |
| } |
| |
| lastClipID = clip.clipID; // Nest the next clip (if any) inside the one |
| // we just rendered. |
| } |
| assert(lastClipID == m_clipStack[clipStackHeight - 1].clipID); |
| draw->setClipID(lastClipID); |
| m_context->addClipReadBounds(lastClipID, draw->pixelBounds()); |
| m_context->setClipContentID(lastClipID); |
| return ApplyClipResult::success; |
| } |
| } // namespace rive |