| /* |
| * Copyright 2025 Rive |
| */ |
| |
| #include "gm.hpp" |
| #include "gmutils.hpp" |
| #include "rive/renderer.hpp" |
| #include <set> |
| |
| using namespace rivegm; |
| using namespace rive; |
| |
| constexpr static float R = 490; |
| |
| static void add_circle(PathBuilder&, |
| float x, |
| float y, |
| float radius, |
| rivegm::PathDirection); |
| |
| static void draw_spirograph(Renderer*, |
| RenderPaint*, |
| RenderPath* circles, |
| float2 translate, |
| float2 c, |
| int depth, |
| std::set<std::tuple<int, int>>* drawnCenters); |
| |
| // Stress tests the renderer by trying to fit as many tessellation spans into a |
| // single flush as possible. This will hopefully detect any potential driver |
| // issues associated with tess span overload. |
| class LotsOfTessSpansGM : public GM |
| { |
| public: |
| LotsOfTessSpansGM(RenderPaintStyle style) : GM(1024, 1024), m_style(style) |
| {} |
| |
| private: |
| void onDraw(Renderer* renderer) override |
| { |
| PathBuilder pathBuilder; |
| pathBuilder.fillRule(FillRule::clockwise); |
| for (uint32_t i = (m_style == RenderPaintStyle::fill) ? 25 : 1; i < 50; |
| ++i) |
| { |
| add_circle(pathBuilder, 0, 0, i * 10, rivegm::PathDirection::cw); |
| if (m_style == RenderPaintStyle::fill) |
| { |
| add_circle(pathBuilder, |
| 0, |
| 0, |
| i * 10 - 1, |
| rivegm::PathDirection::ccw); |
| } |
| } |
| Path circles = pathBuilder.detach(); |
| |
| float2 translate = {512, 512}; |
| Paint paint; |
| paint->style(m_style); |
| // Use round joins because they use fewer segments when nearly flat, and |
| // we want to generate as many tessellation *segments* (not spokes) as |
| // possible in a single flush. |
| paint->join(StrokeJoin::round); |
| paint->color((m_style == RenderPaintStyle::fill) ? 0xff7500ff |
| : 0xfffe0089); |
| renderer->drawPath(circles, paint); |
| std::set<std::tuple<int, int>> drawnCenters; |
| draw_spirograph(renderer, |
| paint, |
| circles, |
| translate, |
| float2{-R, 0}, |
| 3, |
| &drawnCenters); |
| draw_spirograph(renderer, |
| paint, |
| circles, |
| translate, |
| float2{0, R}, |
| 3, |
| &drawnCenters); |
| draw_spirograph(renderer, |
| paint, |
| circles, |
| translate, |
| float2{R, 0}, |
| 3, |
| &drawnCenters); |
| draw_spirograph(renderer, |
| paint, |
| circles, |
| translate, |
| float2{0, -R}, |
| 3, |
| &drawnCenters); |
| } |
| |
| const RenderPaintStyle m_style; |
| }; |
| |
| static void add_circle(PathBuilder& pathBuilder, |
| float x, |
| float y, |
| float radius, |
| rivegm::PathDirection dir) |
| { |
| const int n = std::max(static_cast<int>(2 * math::PI * radius / 32), 3); |
| pathBuilder.moveTo(x + radius, y); |
| for (int i = 1; i <= n; ++i) |
| { |
| float c0 = 2 * math::PI * static_cast<float>(i - (2.f / 3.f)) / n; |
| float c1 = 2 * math::PI * static_cast<float>(i - (1.f / 3.f)) / n; |
| float theta = 2 * math::PI * static_cast<float>(i) / n; |
| if (dir == rivegm::PathDirection::ccw) |
| { |
| c0 = -c0; |
| c1 = -c1; |
| theta = -theta; |
| } |
| // NOTE: this is not a true approximation of the arc. The segment is so |
| // tiny that any simple cubic will work for our testing purposes. A true |
| // arc approximation would take more complex math to figure out the |
| // tangents on both ends, and then scale them such that the height of |
| // the cubic matched the arc. |
| pathBuilder.cubicTo(x + radius * cosf(c0), |
| y + radius * sinf(c0), |
| x + radius * cosf(c1), |
| y + radius * sinf(c1), |
| x + radius * cosf(theta), |
| y + radius * sinf(theta)); |
| } |
| pathBuilder.close(); |
| } |
| |
| static void draw_spirograph(Renderer* renderer, |
| RenderPaint* paint, |
| RenderPath* circles, |
| float2 translate, |
| float2 c, |
| int depth, |
| std::set<std::tuple<int, int>>* drawnCenters) |
| { |
| if (depth <= 0) |
| { |
| return; |
| } |
| |
| translate += c; |
| |
| // Only draw the circles once, centered within any given pixel. When we draw |
| // overlapping circles, coverage-based AA darkens but MSAA does not, leading |
| // to divergence in golden images. |
| int2 icenter = simd::cast<int>(simd::floor(translate + .5f)); |
| if (drawnCenters->insert({int(icenter.x), int(icenter.y)}).second) |
| { |
| AutoRestore ar(renderer, /*doSave=*/true); |
| renderer->translate(translate.x, translate.y); |
| renderer->drawPath(circles, paint); |
| } |
| |
| // Next draw circles centered on both points of intersection between the |
| // circle we just drew and the one before it! |
| |
| // y = mx + b |
| float2 c0 = -c; |
| if (fabsf(c.x) > fabsf(c.y)) |
| { |
| c0 = c0.yx; |
| } |
| const float m = -c0.x / c0.y; |
| const float b = simd::dot(c0, c0) / (2 * c0.y); |
| |
| // Ax^2 + Bx + C = 0 |
| const float A = 1 + m * m; |
| const float B = 2 * m * b; |
| const float C = b * b - R * R; |
| |
| float2 left; |
| left.x = (-B + sqrtf(B * B - 4 * A * C)) / (2 * A); |
| left.y = m * left.x + b; |
| |
| float2 right; |
| right.x = (-B - sqrtf(B * B - 4 * A * C)) / (2 * A); |
| right.y = m * right.x + b; |
| |
| if (fabsf(c.x) > fabsf(c.y)) |
| { |
| left = left.yx; |
| right = right.yx; |
| } |
| |
| draw_spirograph(renderer, |
| paint, |
| circles, |
| translate, |
| left, |
| depth - 1, |
| drawnCenters); |
| draw_spirograph(renderer, |
| paint, |
| circles, |
| translate, |
| right, |
| depth - 1, |
| drawnCenters); |
| } |
| |
| GMREGISTER(lots_of_tess_spans_stroke, |
| return new LotsOfTessSpansGM(RenderPaintStyle::stroke);) |
| |
| // TODO: The "_fill" variant is wildly fill-heavy and triggers a |
| // VK_ERROR_DEVICE_LOST on some android devices. The error is probably |
| // appropriate for the capabilities of those devices, so we will need to figure |
| // out how to step around it if we want to use this test. |
| // GMREGISTER(lots_of_tess_spans_fill, |
| // return new LotsOfTessSpansGM(RenderPaintStyle::fill);) |