blob: ed9a90a167220c8bceac5c3211635183bde09200 [file] [log] [blame]
/*
* Copyright 2022 Rive
*/
#include "common/render_context_null.hpp"
#include "rive/renderer/rive_renderer.hpp"
#include "rive/renderer/rive_render_image.hpp"
#include "../src/rive_render_path.hpp"
#include <catch.hpp>
namespace rive::gpu
{
static RenderContext::FrameDescriptor s_frameDescriptor = {
.renderTargetWidth = 100,
.renderTargetHeight = 100,
};
// Ensures that clip contents get reused when we pop and push the same path(s).
TEST_CASE("clip-stack", "RiveRenderer")
{
std::unique_ptr<RenderContext> renderContext =
RenderContextNULL::MakeContext();
auto renderTarget =
renderContext->static_impl_cast<RenderContextNULL>()->makeRenderTarget(
s_frameDescriptor.renderTargetWidth,
s_frameDescriptor.renderTargetHeight);
for (int i = 0; i < 3; ++i)
{
renderContext->beginFrame(s_frameDescriptor);
auto pathA = renderContext->makeEmptyRenderPath();
auto pathB = renderContext->makeEmptyRenderPath();
auto pathC = renderContext->makeEmptyRenderPath();
auto drawablePath = renderContext->makeEmptyRenderPath();
pathA->cubicTo(1, 1, 1, 2, 2, 2);
pathA->lineTo(3, 4);
pathB->cubicTo(5, 5, 5, 6, 6, 6);
pathB->lineTo(7, 8);
pathC->lineTo(9, 10);
pathC->cubicTo(11, 12, 13, 14, 15, 16);
drawablePath->cubicTo(17, 17, 17, 18, 18, 18);
drawablePath->lineTo(19, 20);
auto paint = renderContext->makeRenderPaint();
paint->color(0xffffffff);
RiveRenderer renderer(renderContext.get());
renderer.save();
renderer.clipPath(pathA.get());
renderer.drawPath(drawablePath.get(), paint.get());
renderer.restore();
uint32_t clipID = renderContext->getClipContentID();
CHECK(clipID != 0);
// Pushing the same clip sholdn't cause us to redraw the clipBuffer
// (clipID will stay the same.)
renderer.save();
renderer.clipPath(pathA.get());
renderer.drawPath(drawablePath.get(), paint.get());
renderer.restore();
CHECK(clipID == renderContext->getClipContentID());
// We can't modify paths anymore once they've been pushed to the
// context.
#if 0
// Modifying the path will cause the clip to be regenerated.
renderer.save();
pathA->lineTo(21, 22);
renderer.clipPath(pathA.get());
renderer.drawPath(drawablePath.get(), paint.get());
renderer.restore();
CHECK(clipID != renderContext->getClipContentID());
clipID = renderContext->getClipContentID();
#endif
// Pushing the same (modified) clip sholdn't cause us to redraw the
// clipBuffer, again.
renderer.save();
renderer.clipPath(pathA.get());
renderer.drawPath(drawablePath.get(), paint.get());
renderer.restore();
CHECK(clipID == renderContext->getClipContentID());
// Changing the view matrix will invalidate the clip.
renderer.translate(.5f, 1.5f);
renderer.save();
renderer.clipPath(pathA.get());
renderer.drawPath(drawablePath.get(), paint.get());
renderer.restore();
CHECK(clipID != renderContext->getClipContentID());
clipID = renderContext->getClipContentID();
// Now we should reuse the clip again on the new matrix.
renderer.save();
renderer.clipPath(pathA.get());
renderer.drawPath(drawablePath.get(), paint.get());
renderer.restore();
CHECK(clipID == renderContext->getClipContentID());
// Pushing and popping a nested clip will cause the clip to be
// regenerated.
renderer.save();
renderer.clipPath(pathA.get());
renderer.drawPath(drawablePath.get(), paint.get());
CHECK(clipID == renderContext->getClipContentID());
renderer.clipPath(pathB.get());
renderer.drawPath(drawablePath.get(), paint.get());
CHECK(clipID != renderContext->getClipContentID());
clipID = renderContext->getClipContentID();
renderer.restore();
// Pushing both back will reuse the clip.
renderer.save();
renderer.clipPath(pathA.get());
renderer.clipPath(pathB.get());
renderer.drawPath(drawablePath.get(), paint.get());
renderer.restore();
CHECK(clipID == renderContext->getClipContentID());
// Pushing a new clip and not using it will also not affect the clip
// buffer.
renderer.save();
renderer.clipPath(pathA.get());
renderer.clipPath(pathB.get());
renderer.save();
renderer.clipPath(pathC.get());
renderer.restore();
renderer.drawPath(drawablePath.get(), paint.get());
CHECK(clipID == renderContext->getClipContentID());
renderer.restore();
// Now go three deep, just for fun.
renderer.save();
renderer.clipPath(pathA.get());
renderer.save();
renderer.clipPath(pathB.get());
renderer.clipPath(pathC.get());
renderer.drawPath(drawablePath.get(), paint.get());
CHECK(clipID != renderContext->getClipContentID());
clipID = renderContext->getClipContentID();
renderer.restore();
renderer.clipPath(pathB.get());
renderer.clipPath(pathC.get());
renderer.drawPath(drawablePath.get(), paint.get());
CHECK(clipID == renderContext->getClipContentID());
renderer.restore();
// Adding them back in a different order will regenerate the clip.
renderer.save();
renderer.clipPath(pathA.get());
renderer.clipPath(pathC.get());
renderer.clipPath(pathB.get());
renderer.drawPath(drawablePath.get(), paint.get());
CHECK(clipID != renderContext->getClipContentID());
clipID = renderContext->getClipContentID();
renderer.restore();
// And now the alternate order should stick.
renderer.save();
renderer.clipPath(pathA.get());
renderer.clipPath(pathC.get());
renderer.clipPath(pathB.get());
renderer.drawPath(drawablePath.get(), paint.get());
CHECK(clipID == renderContext->getClipContentID());
renderer.restore();
for (bool logicalFlush : {false, true})
{
if (logicalFlush)
{
renderContext->logicalFlush();
}
else
{
renderContext->flush({.renderTarget = renderTarget.get()});
renderContext->beginFrame(s_frameDescriptor);
}
// The clip content ID gets reset after a flush.
CHECK(renderContext->getClipContentID() == 0);
// The clip gets cleared after a flush, so this should start over
// with a new clip ID.
renderer.save();
renderer.clipPath(pathA.get());
renderer.clipPath(pathC.get());
renderer.clipPath(pathB.get());
renderer.drawPath(drawablePath.get(), paint.get());
CHECK(clipID != renderContext->getClipContentID());
CHECK(renderContext->getClipContentID() == 3);
clipID = renderContext->getClipContentID();
renderer.restore();
// And now that we're in the new flush, doing it again will reuse
// the same clipID.
renderer.save();
renderer.clipPath(pathA.get());
renderer.clipPath(pathC.get());
renderer.clipPath(pathB.get());
renderer.drawPath(drawablePath.get(), paint.get());
renderer.restore();
CHECK(clipID == renderContext->getClipContentID());
// Draw another clip so the clipID isn't 1 anymore.
renderer.save();
renderer.clipPath(pathA.get());
renderer.drawPath(drawablePath.get(), paint.get());
renderer.restore();
CHECK(clipID != renderContext->getClipContentID());
clipID = renderContext->getClipContentID();
}
renderContext->flush({.renderTarget = renderTarget.get()});
}
}
static RawPath& make_oval(const AABB& bounds)
{
static RawPath path;
path.rewind();
path.addOval(bounds);
return path;
};
// Ensures that a flush only invalidates the current clip once, and that we can
// start reusing it again after.
TEST_CASE("clip-flush-clip-clip", "RiveRenderer")
{
std::unique_ptr<RenderContext> renderContext =
RenderContextNULL::MakeContext();
renderContext->beginFrame(s_frameDescriptor);
auto pathA = renderContext->makeRenderPath(make_oval({0, 0, 1, 5}),
FillRule::nonZero);
auto pathB = renderContext->makeRenderPath(make_oval({0, 0, 2, 6}),
FillRule::nonZero);
auto pathC = renderContext->makeRenderPath(make_oval({0, 0, 3, 7}),
FillRule::nonZero);
auto pathD = renderContext->makeRenderPath(make_oval({0, 0, 4, 8}),
FillRule::nonZero);
auto drawablePath = renderContext->makeRenderPath(make_oval({0, 0, 9, 10}),
FillRule::nonZero);
auto paint = renderContext->makeRenderPaint();
paint->color(0xffffffff);
RiveRenderer renderer(renderContext.get());
renderer.save();
renderer.clipPath(pathD.get());
renderer.drawPath(drawablePath.get(), paint.get());
uint32_t clipID = renderContext->getClipContentID();
CHECK(clipID != 0);
renderer.restore();
renderer.clipPath(pathC.get());
renderer.clipPath(pathB.get());
renderer.clipPath(pathA.get());
renderer.drawPath(drawablePath.get(), paint.get());
CHECK(renderContext->getClipContentID() != clipID);
clipID = renderContext->getClipContentID();
// Flushing should invalidate the clip.
renderContext->logicalFlush();
renderer.drawPath(drawablePath.get(), paint.get());
CHECK(renderContext->getClipContentID() != clipID);
clipID = renderContext->getClipContentID();
// The clip should now get reused again for subsequent draws.
for (size_t i = 0; i < 3; ++i)
{
renderer.drawPath(drawablePath.get(), paint.get());
CHECK(renderContext->getClipContentID() == clipID);
}
auto renderTarget =
renderContext->static_impl_cast<RenderContextNULL>()->makeRenderTarget(
s_frameDescriptor.renderTargetWidth,
s_frameDescriptor.renderTargetHeight);
renderContext->flush({.renderTarget = renderTarget.get()});
}
static RawPath& make_cusp(const IAABB& rect)
{
static RawPath path;
path.rewind();
path.moveTo(rect.left, rect.top);
path.lineTo(rect.right, rect.bottom);
path.lineTo(rect.right, rect.top);
path.lineTo(rect.left, rect.bottom);
return path;
}
// Check that clip readBounds and contentBounds get tracked properly.
TEST_CASE("clip-content-read-bounds", "RiveRenderer")
{
std::unique_ptr<RenderContext> renderContext =
RenderContextNULL::MakeContext();
auto renderTarget =
renderContext->static_impl_cast<RenderContextNULL>()->makeRenderTarget(
s_frameDescriptor.renderTargetWidth,
s_frameDescriptor.renderTargetHeight);
RenderContext::FrameDescriptor frameDescriptor = s_frameDescriptor;
frameDescriptor.msaaSampleCount = 4;
renderContext->beginFrame(frameDescriptor);
auto cuspA = static_rcp_cast<RiveRenderPath>(
renderContext->makeRenderPath(make_cusp({0, 0, 1, 5}),
FillRule::nonZero));
auto cuspB = static_rcp_cast<RiveRenderPath>(
renderContext->makeRenderPath(make_cusp({-1, -1, 1, 1}),
FillRule::nonZero));
auto cuspC = static_rcp_cast<RiveRenderPath>(
renderContext->makeRenderPath(make_cusp({0, 0, 5, 1}),
FillRule::nonZero));
auto paint = renderContext->makeRenderPaint();
paint->color(0xffffffff);
// Clip read bounds are affected by the view matrix.
Mat2D xforms[] = {Mat2D(),
Mat2D::fromTranslate(3, 1),
Mat2D::fromScale(3, 2),
Mat2D::fromTranslate(-1, 3).scale({2, 3})};
RiveRenderer renderer(renderContext.get());
for (const Mat2D& m : xforms)
{
renderer.save();
renderer.clipPath(cuspA.get());
renderer.transform(m);
renderer.drawPath(cuspB.get(), paint.get());
uint32_t clipAID = renderContext->getClipContentID();
REQUIRE(clipAID != 0);
// clipA is not transformed.
auto clipAContentBounds = cuspA->getBounds().roundOut();
CHECK(renderContext->getClipContentBounds(clipAID) ==
clipAContentBounds);
CHECK(renderContext->getClipReadBounds(clipAID) ==
m.mapBoundingBox({-1, -1, 1, 1}).roundOut());
renderer.clipPath(cuspC.get());
renderer.drawPath(cuspB.get(), paint.get());
uint32_t clipCID = renderContext->getClipContentID();
REQUIRE(clipCID != 0);
REQUIRE(clipCID != clipAID);
auto clipCContentBounds =
m.mapBoundingBox(cuspC->getBounds().roundOut()).roundOut();
CHECK(renderContext->getClipContentBounds(clipCID) ==
clipCContentBounds);
CHECK(renderContext->getClipReadBounds(clipCID) ==
m.mapBoundingBox({-1, -1, 1, 1}).roundOut());
// clipAID read bounds should have expanded from the nested clipping.
CHECK(renderContext->getClipContentBounds(clipAID) ==
clipAContentBounds);
CHECK(renderContext->getClipReadBounds(clipAID) ==
m.mapBoundingBox({-1, -1, 5, 1}).roundOut());
// Each nested clip is read only by the one directly below it.
auto cusp6 = static_rcp_cast<RiveRenderPath>(
renderContext->makeRenderPath(make_cusp({0, 0, 1, 6}),
FillRule::nonZero));
auto cusp7 = static_rcp_cast<RiveRenderPath>(
renderContext->makeRenderPath(make_cusp({0, 0, 1, 7}),
FillRule::nonZero));
auto cusp8 = static_rcp_cast<RiveRenderPath>(
renderContext->makeRenderPath(make_cusp({0, 0, 1, 8}),
FillRule::nonZero));
auto cusp9 = static_rcp_cast<RiveRenderPath>(
renderContext->makeRenderPath(make_cusp({0, 0, 1, 9}),
FillRule::nonZero));
renderer.clipPath(cusp9.get());
renderer.drawPath(cuspB.get(), paint.get());
uint32_t clip9ID = renderContext->getClipContentID();
renderer.clipPath(cusp8.get());
renderer.drawPath(cuspB.get(), paint.get());
uint32_t clip8ID = renderContext->getClipContentID();
renderer.save();
renderer.clipPath(cusp7.get());
renderer.drawPath(cuspB.get(), paint.get());
uint32_t clip7ID = renderContext->getClipContentID();
renderer.clipPath(cusp6.get());
renderer.drawPath(cuspC.get(), paint.get());
uint32_t clip6ID = renderContext->getClipContentID();
// clipA bounds should not have been affected by deeper nested clips.
CHECK(renderContext->getClipContentBounds(clipAID) ==
clipAContentBounds);
CHECK(renderContext->getClipReadBounds(clipAID) ==
m.mapBoundingBox({-1, -1, 5, 1}).roundOut());
// Each nested clip is read only by the one directly below it. Outer
// clips are not read by the draw either.
CHECK(renderContext->getClipContentBounds(clipCID) ==
clipCContentBounds);
CHECK(renderContext->getClipReadBounds(clipCID) ==
m.mapBoundingBox({-1, -1, 1, 9}).roundOut());
auto clip9ContentBounds =
m.mapBoundingBox(cusp9->getBounds().roundOut()).roundOut();
CHECK(renderContext->getClipContentBounds(clip9ID) ==
clip9ContentBounds);
CHECK(renderContext->getClipReadBounds(clip9ID) ==
m.mapBoundingBox({-1, -1, 1, 8}).roundOut());
auto clip8ContentBounds =
m.mapBoundingBox(cusp8->getBounds().roundOut()).roundOut();
CHECK(renderContext->getClipContentBounds(clip8ID) ==
clip8ContentBounds);
CHECK(renderContext->getClipReadBounds(clip8ID) ==
m.mapBoundingBox({-1, -1, 1, 7}).roundOut());
auto clip7ContentBounds =
m.mapBoundingBox(cusp7->getBounds().roundOut()).roundOut();
CHECK(renderContext->getClipContentBounds(clip7ID) ==
clip7ContentBounds);
CHECK(renderContext->getClipReadBounds(clip7ID) ==
m.mapBoundingBox({-1, -1, 1, 6}).roundOut());
auto clip6ContentBounds =
m.mapBoundingBox(cusp6->getBounds().roundOut()).roundOut();
CHECK(renderContext->getClipContentBounds(clip6ID) ==
clip6ContentBounds);
CHECK(renderContext->getClipReadBounds(clip6ID) ==
m.mapBoundingBox({0, 0, 5, 1}).roundOut());
// Pop back and do some more reading from clip8.
renderer.restore();
renderer.drawPath(cuspC.get(), paint.get());
uint32_t secondClip8ID = renderContext->getClipContentID();
// Since clip8 got obliterated and redrawn, this next read shouldn't
// affect it.
CHECK(secondClip8ID != clip8ID);
CHECK(renderContext->getClipContentBounds(clip8ID) ==
clip8ContentBounds);
CHECK(renderContext->getClipReadBounds(clip8ID) ==
m.mapBoundingBox({-1, -1, 1, 7}).roundOut());
// ... But should affect the clip currently in the clip buffer.
CHECK(renderContext->getClipContentBounds(secondClip8ID) ==
clip8ContentBounds);
CHECK(renderContext->getClipReadBounds(secondClip8ID) ==
m.mapBoundingBox({0, 0, 5, 1}).roundOut());
renderer.clipPath(cuspB.get());
renderer.drawPath(cuspC.get(), paint.get());
CHECK(renderContext->getClipContentBounds(clip8ID) ==
clip8ContentBounds);
CHECK(renderContext->getClipReadBounds(clip8ID) ==
m.mapBoundingBox({-1, -1, 1, 7}).roundOut());
CHECK(renderContext->getClipContentBounds(secondClip8ID) ==
clip8ContentBounds);
CHECK(renderContext->getClipReadBounds(secondClip8ID) ==
m.mapBoundingBox({-1, -1, 5, 1}).roundOut());
uint32_t clipBID = renderContext->getClipContentID();
auto clipBContentBounds =
m.mapBoundingBox(cuspB->getBounds().roundOut()).roundOut();
CHECK(renderContext->getClipContentBounds(clipBID) ==
clipBContentBounds);
CHECK(renderContext->getClipReadBounds(clipBID) ==
m.mapBoundingBox({0, 0, 5, 1}).roundOut());
renderer.restore();
// Clip content bounds should never change.
CHECK(renderContext->getClipContentBounds(clipAID) ==
clipAContentBounds);
CHECK(renderContext->getClipContentBounds(clipCID) ==
clipCContentBounds);
CHECK(renderContext->getClipContentBounds(clip9ID) ==
clip9ContentBounds);
CHECK(renderContext->getClipContentBounds(clip8ID) ==
clip8ContentBounds);
CHECK(renderContext->getClipContentBounds(clip7ID) ==
clip7ContentBounds);
CHECK(renderContext->getClipContentBounds(clip6ID) ==
clip6ContentBounds);
CHECK(renderContext->getClipContentBounds(secondClip8ID) ==
clip8ContentBounds);
CHECK(renderContext->getClipContentBounds(clipBID) ==
clipBContentBounds);
}
renderContext->flush({.renderTarget = renderTarget.get()});
}
static RawPath& make_rect(const AABB& rect)
{
static RawPath path;
path.rewind();
path.addRect(rect);
return path;
};
#define CHECK_AABB_NEARLY_EQUAL(AABB, L, T, R, B) \
{ \
CHECK(AABB.left() == Approx(L)); \
CHECK(AABB.top() == Approx(T)); \
CHECK(AABB.right() == Approx(R)); \
CHECK(AABB.bottom() == Approx(B)); \
}
// Ensures that rectangular clips are handled as "clipRects" where possible,
// instead of going to the clip buffer.
TEST_CASE("clip-rects", "RiveRenderer")
{
std::unique_ptr<RenderContext> renderContext =
RenderContextNULL::MakeContext();
renderContext->beginFrame(s_frameDescriptor);
auto rectA = renderContext->makeRenderPath(make_rect({0, 0, 1, 5}),
FillRule::nonZero);
auto rectB = renderContext->makeRenderPath(make_rect({1, -1, 2, 4}),
FillRule::nonZero);
auto rectC = renderContext->makeRenderPath(make_rect({0, 0, 3, 6}),
FillRule::nonZero);
auto ovalA = renderContext->makeRenderPath(make_oval({0, 0, 1, 4}),
FillRule::nonZero);
auto ovalB = renderContext->makeRenderPath(make_oval({0, 0, 2, 5}),
FillRule::nonZero);
auto ovalC = renderContext->makeRenderPath(make_oval({0, 0, 3, 6}),
FillRule::nonZero);
auto drawablePath = renderContext->makeRenderPath(make_oval({0, 0, 7, 8}),
FillRule::nonZero);
auto paint = renderContext->makeRenderPaint();
paint->color(0xffffffff);
RiveRenderer renderer(renderContext.get());
// A single rect is always handled as a clipRect.
renderer.save();
renderer.clipPath(rectA.get());
CHECK(renderContext->getClipContentID() == 0);
renderer.drawPath(drawablePath.get(), paint.get());
CHECK(renderContext->getClipContentID() == 0);
renderer.restore();
renderer.save();
// The nested rect is always handled as a clipRect, even if it's nested.
renderer.clipPath(ovalA.get());
renderer.drawPath(drawablePath.get(), paint.get());
uint32_t clipID = renderContext->getClipContentID();
CHECK(clipID != 0);
CHECK(!renderer.hasClipRect());
renderer.clipPath(rectA.get());
renderer.drawPath(drawablePath.get(), paint.get());
CHECK(renderContext->getClipContentID() == clipID);
CHECK(renderer.hasClipRect());
CHECK_AABB_NEARLY_EQUAL(renderer.getClipRect(), 0, 0, 1, 5);
CHECK(renderer.getClipRectMatrix() == Mat2D());
// Doubly nested rectangles get combined into a single clipRect.
renderer.clipPath(ovalB.get());
renderer.drawPath(drawablePath.get(), paint.get());
CHECK(renderContext->getClipContentID() != clipID);
clipID = renderContext->getClipContentID();
renderer.clipPath(rectB.get());
renderer.drawPath(drawablePath.get(), paint.get());
CHECK(renderContext->getClipContentID() == clipID);
CHECK(renderer.hasClipRect());
CHECK_AABB_NEARLY_EQUAL(renderer.getClipRect(), 1, 0, 1, 4);
CHECK(renderer.getClipRectMatrix() == Mat2D());
// Nested rectangles don't get combined if their matrices are incompatible.
renderer.save();
renderer.rotate(1);
renderer.clipPath(ovalA.get());
renderer.drawPath(drawablePath.get(), paint.get());
CHECK(renderContext->getClipContentID() != clipID);
clipID = renderContext->getClipContentID();
renderer.clipPath(rectB.get());
renderer.drawPath(drawablePath.get(), paint.get());
CHECK(renderContext->getClipContentID() != clipID);
clipID = renderContext->getClipContentID();
renderer.restore();
CHECK(renderer.hasClipRect());
CHECK_AABB_NEARLY_EQUAL(renderer.getClipRect(), 1, 0, 1, 4);
CHECK(renderer.getClipRectMatrix() == Mat2D());
// Nested rectangles DO get combined if their matrices ARE compatible.
renderer.save();
renderer.scale(1, 2);
renderer.translate(3, 4);
renderer.clipPath(ovalC.get());
renderer.drawPath(drawablePath.get(), paint.get());
CHECK(renderContext->getClipContentID() != clipID);
clipID = renderContext->getClipContentID();
renderer.clipPath(rectC.get());
renderer.drawPath(drawablePath.get(), paint.get());
CHECK(renderContext->getClipContentID() == clipID);
CHECK(renderer.hasClipRect());
CHECK_AABB_NEARLY_EQUAL(renderer.getClipRect(), 3, 8, 1, 4);
CHECK(renderer.getClipRectMatrix() == Mat2D());
renderer.restore();
renderer.restore();
renderContext->logicalFlush();
renderer.save();
// Scaling and transforming within the same baseline space should still
// allow for clipRects to be combined.
Mat2D m = Mat2D::fromRotation(1) * Mat2D{.3f, .3f, .9f, -.7f, .4f, .2f};
renderer.transform(m);
renderer.clipPath(rectA.get());
renderer.drawPath(drawablePath.get(), paint.get());
CHECK(renderContext->getClipContentID() == 0);
CHECK(renderer.hasClipRect());
CHECK(renderer.getClipRect() == AABB{0, 0, 1, 5});
CHECK(renderer.getClipRectMatrix() == m);
renderer.clipPath(rectB.get());
renderer.drawPath(drawablePath.get(), paint.get());
CHECK(renderContext->getClipContentID() == 0);
renderer.translate(.9f, -.1f);
renderer.clipPath(rectB.get());
renderer.drawPath(drawablePath.get(), paint.get());
CHECK(renderContext->getClipContentID() == 0);
CHECK(renderer.hasClipRect());
CHECK_AABB_NEARLY_EQUAL(renderer.getClipRect(), 1.9f, 0, 1, 3.9f);
CHECK(renderer.getClipRectMatrix() == m);
renderer.scale(10, 2);
renderer.clipPath(rectB.get());
renderer.drawPath(drawablePath.get(), paint.get());
CHECK(renderContext->getClipContentID() == 0);
// Even 90 degree rotations work!
renderer.rotate(math::PI / 2);
renderer.clipPath(rectB.get());
renderer.drawPath(drawablePath.get(), paint.get());
CHECK(renderer.hasClipRect());
CHECK_AABB_NEARLY_EQUAL(renderer.getClipRect(), 10.9f, 1.9f, 1, 3.9f);
CHECK(renderer.getClipRectMatrix() == m);
CHECK(renderContext->getClipContentID() == 0);
renderer.rotate(math::PI);
renderer.translate(-10, 0);
renderer.clipPath(rectB.get());
renderer.drawPath(drawablePath.get(), paint.get());
CHECK(renderer.hasClipRect());
CHECK_AABB_NEARLY_EQUAL(renderer.getClipRect(), 10.9f, 15.9f, 1, 3.9f);
CHECK(renderer.getClipRectMatrix() == m);
CHECK(renderContext->getClipContentID() == 0);
renderer.rotate(-math::PI / 2);
renderer.clipPath(rectB.get());
renderer.drawPath(drawablePath.get(), paint.get());
CHECK(renderContext->getClipContentID() == 0);
CHECK(renderer.hasClipRect());
CHECK_AABB_NEARLY_EQUAL(renderer.getClipRect(), 10.9f, 15.9f, -9.1f, 3.9f);
CHECK(renderer.getClipRectMatrix() == m);
// And flips!
renderer.scale(1, -1);
renderer.clipPath(rectB.get());
renderer.drawPath(drawablePath.get(), paint.get());
CHECK(renderContext->getClipContentID() == 0);
CHECK(renderer.hasClipRect());
CHECK_AABB_NEARLY_EQUAL(renderer.getClipRect(), 10.9f, 17.9f, -9.1f, 3.9f);
CHECK(renderer.getClipRectMatrix() == m);
renderer.scale(-1, -1);
renderer.translate(2, 0);
renderer.clipPath(rectB.get());
renderer.drawPath(drawablePath.get(), paint.get());
CHECK(renderContext->getClipContentID() == 0);
CHECK(renderer.hasClipRect());
CHECK_AABB_NEARLY_EQUAL(renderer.getClipRect(), 30.9f, 17.9f, -9.1f, 3.9f);
CHECK(renderer.getClipRectMatrix() == m);
// ... but once the matrices become incompatible, we can't intersect with
// the clipRect anymore.
renderer.transform(Mat2D{1, .1f, 0, 1, 0, 0});
renderer.clipPath(rectB.get());
renderer.drawPath(drawablePath.get(), paint.get());
CHECK(renderContext->getClipContentID() != 0);
CHECK_AABB_NEARLY_EQUAL(renderer.getClipRect(), 30.9f, 17.9f, -9.1f, 3.9f);
CHECK(renderer.getClipRectMatrix() == m);
renderer.restore();
auto renderTarget =
renderContext->static_impl_cast<RenderContextNULL>()->makeRenderTarget(
s_frameDescriptor.renderTargetWidth,
s_frameDescriptor.renderTargetHeight);
renderContext->flush({.renderTarget = renderTarget.get()});
}
// This test passes by not triggering assertions within RiveRenderer and
// RenderContext due to drawing empty paths.
TEST_CASE("empty-paths", "RiveRenderer")
{
std::unique_ptr<RenderContext> renderContext =
RenderContextNULL::MakeContext();
renderContext->beginFrame(s_frameDescriptor);
RiveRenderer renderer(renderContext.get());
// Draw an empty path.
auto paint = renderContext->makeRenderPaint();
auto path = renderContext->makeEmptyRenderPath();
renderer.drawPath(path.get(), paint.get());
// Draw another empty path.
path = renderContext->makeEmptyRenderPath();
path->moveTo(1, 1);
path->moveTo(2, 2);
path->lineTo(2, 2);
renderer.drawPath(path.get(), paint.get());
// Draw a path with tons of vertices that definitely trigger a first-draw
// realloc.
path = renderContext->makeEmptyRenderPath();
auto rand100 = []() {
return static_cast<float>(rand()) * 100.f /
static_cast<float>(RAND_MAX);
};
for (size_t i = 0; i < 100000; ++i)
{
path->cubicTo(rand100(),
rand100(),
rand100(),
rand100(),
rand100(),
rand100());
}
renderer.drawPath(path.get(), paint.get());
// Draw one more empty path.
renderer.drawPath(renderContext->makeEmptyRenderPath().get(), paint.get());
auto renderTarget =
renderContext->static_impl_cast<RenderContextNULL>()->makeRenderTarget(
s_frameDescriptor.renderTargetWidth,
s_frameDescriptor.renderTargetHeight);
renderContext->flush({.renderTarget = renderTarget.get()});
}
// This test passes by not triggering assertions within RiveRenderer and
// RenderContext due to drawing empty paths composed of nothing but moves.
TEST_CASE("empty-paths2", "RiveRenderer")
{
std::unique_ptr<RenderContext> renderContext =
RenderContextNULL::MakeContext();
renderContext->beginFrame(s_frameDescriptor);
RiveRenderer renderer(renderContext.get());
auto paint = renderContext->makeRenderPaint();
// Draw path composed of nothing but moves.
auto path = renderContext->makeEmptyRenderPath();
path->moveTo(0, 0);
path->moveTo(1, 2);
path->moveTo(3, 4);
path->moveTo(5, 6);
renderer.drawPath(path.get(), paint.get());
// Draw a large path (that triggers the triangulator) composed of nothing
// but moves.
path = renderContext->makeEmptyRenderPath();
path->moveTo(0, 0);
path->moveTo(1000, 2000);
path->moveTo(3000, 4000);
path->moveTo(5000, 6000);
path->moveTo(7000, 8000);
renderer.drawPath(path.get(), paint.get());
auto renderTarget =
renderContext->static_impl_cast<RenderContextNULL>()->makeRenderTarget(
s_frameDescriptor.renderTargetWidth,
s_frameDescriptor.renderTargetHeight);
renderContext->flush({.renderTarget = renderTarget.get()});
}
TEST_CASE("IsAABB", "RiveRenderer")
{
AABB rect;
for (bool doClose : {true, false})
{
RawPath path;
if (doClose)
{
path.addRect(AABB{0, 0, 10, 10}, PathDirection::ccw);
}
else
{
path.lineTo(10, 0);
path.lineTo(10, 10);
path.lineTo(0, 10);
}
CHECK(RiveRenderer::IsAABB(path, &rect));
CHECK(rect == AABB{0, 0, 10, 10});
path.reset();
if (doClose)
{
path.addRect(AABB{1, 2, -1, 5}, PathDirection::cw);
}
else
{
path.moveTo(1, 2);
path.lineTo(-1, 2);
path.lineTo(-1, 5);
path.lineTo(1, 5);
}
CHECK(RiveRenderer::IsAABB(path, &rect));
CHECK(rect == AABB{-1, 2, 1, 5});
path.reset();
if (doClose)
{
path.addRect(AABB{0, 0, 10, 10}, PathDirection::ccw);
path.addRect(AABB{1, 2, -1, 5}, PathDirection::ccw);
}
else
{
path.lineTo(10, 0);
path.lineTo(10, 10);
path.lineTo(0, 10);
path.lineTo(0, 0);
path.lineTo(10, 0);
}
CHECK(!RiveRenderer::IsAABB(path, &rect));
}
RawPath path;
path.moveTo(1, 0);
CHECK(!RiveRenderer::IsAABB(path, &rect));
path.lineTo(0, 0);
CHECK(!RiveRenderer::IsAABB(path, &rect));
path.lineTo(0, 1);
CHECK(!RiveRenderer::IsAABB(path, &rect));
path.lineTo(1, 1);
CHECK(RiveRenderer::IsAABB(path, &rect));
path.close();
CHECK(RiveRenderer::IsAABB(path, &rect));
CHECK(rect == AABB{0, 0, 1, 1});
path.reset();
path.moveTo(0, 0);
path.lineTo(1, 0);
path.lineTo(1, 1);
path.lineTo(0, 1.1f);
path.close();
CHECK(!RiveRenderer::IsAABB(path, &rect));
// Accept additional verbs and points after the AABB as long as every point
// after is equal to p0.
path.reset();
path.moveTo(0, 0);
path.lineTo(1, 0);
path.lineTo(1, 1);
path.lineTo(0, 1);
CHECK(RiveRenderer::IsAABB(path, &rect));
CHECK(rect == AABB{0, 0, 1, 1});
path.close();
CHECK(RiveRenderer::IsAABB(path, &rect));
CHECK(rect == AABB{0, 0, 1, 1});
path.reset();
path.moveTo(1, 1);
path.lineTo(0, 1);
path.lineTo(0, 0);
path.lineTo(1, 0);
CHECK(RiveRenderer::IsAABB(path, &rect));
CHECK(rect == AABB{0, 0, 1, 1});
path.lineTo(1, 1);
CHECK(RiveRenderer::IsAABB(path, &rect));
CHECK(rect == AABB{0, 0, 1, 1});
path.close();
CHECK(RiveRenderer::IsAABB(path, &rect));
CHECK(rect == AABB{0, 0, 1, 1});
path.close();
CHECK(RiveRenderer::IsAABB(path, &rect));
CHECK(rect == AABB{0, 0, 1, 1});
path.cubicTo(1, 1, 1, 1, 1, 1);
CHECK(RiveRenderer::IsAABB(path, &rect));
CHECK(rect == AABB{0, 0, 1, 1});
path.quadTo(1, 1, 1, 1);
CHECK(RiveRenderer::IsAABB(path, &rect));
CHECK(rect == AABB{0, 0, 1, 1});
}
// Check that paths with NaN vertices don't crash.
TEST_CASE("nan-render-path", "RiveRenderer")
{
std::unique_ptr<RenderContext> renderContext =
RenderContextNULL::MakeContext();
RiveRenderer renderer(renderContext.get());
auto paint = renderContext->makeRenderPaint();
auto finitePath = renderContext->makeEmptyRenderPath();
finitePath->moveTo(1, 2);
finitePath->lineTo(3, 4);
finitePath->cubicTo(5, 6, 7, 8, 9, 10);
finitePath->cubicTo(11, 12, 13, 14, 15, 16);
finitePath->lineTo(17, 18);
auto nan = std::numeric_limits<float>::quiet_NaN();
auto nanPath = renderContext->makeEmptyRenderPath();
nanPath->moveTo(nan, nan);
nanPath->lineTo(nan, nan);
nanPath->cubicTo(nan, nan, nan, nan, nan, nan);
nanPath->cubicTo(nan, nan, nan, nan, nan, nan);
nanPath->lineTo(nan, nan);
renderContext->beginFrame(s_frameDescriptor);
renderer.drawPath(nanPath.get(), paint.get());
renderer.save();
renderer.clipPath(nanPath.get());
renderer.drawPath(finitePath.get(), paint.get());
renderer.restore();
renderer.save();
renderer.transform({nan, nan, nan, nan, nan, nan});
renderer.drawPath(finitePath.get(), paint.get());
renderer.clipPath(finitePath.get());
renderer.drawPath(nanPath.get(), paint.get());
renderer.restore();
CHECK(renderContext->getClipContentID() == 0);
renderer.save();
renderer.clipPath(nanPath.get());
renderer.drawPath(finitePath.get(), paint.get());
// NaN paths cause an immediatedly empty clip stack that just discards
// elements.
CHECK(renderContext->getClipContentID() == 0);
renderer.clipPath(finitePath.get());
renderer.drawPath(finitePath.get(), paint.get());
CHECK(renderContext->getClipContentID() == 0);
renderer.restore();
renderer.save();
renderer.clipPath(finitePath.get());
renderer.drawPath(finitePath.get(), paint.get());
// Non-NaN paths update the clip content ID.
CHECK(renderContext->getClipContentID() != 0);
renderer.restore();
// move(nan, nan), close() found a crash.
auto moveNaN = renderContext->makeEmptyRenderPath();
moveNaN->moveTo(nan, nan);
moveNaN->close();
renderer.drawPath(moveNaN.get(), paint.get());
auto renderTarget =
renderContext->static_impl_cast<RenderContextNULL>()->makeRenderTarget(
s_frameDescriptor.renderTargetWidth,
s_frameDescriptor.renderTargetHeight);
renderContext->flush({.renderTarget = renderTarget.get()});
}
// Check that edge cases in stroke thickness don't crash or assert.
TEST_CASE("stroke-thickness", "RiveRenderer")
{
std::unique_ptr<RenderContext> renderContext =
RenderContextNULL::MakeContext();
renderContext->beginFrame(s_frameDescriptor);
auto path = renderContext->makeEmptyRenderPath();
path->lineTo(100, 100);
path->lineTo(100, 0);
auto paint = renderContext->makeRenderPaint();
paint->style(RenderPaintStyle::stroke);
RiveRenderer renderer(renderContext.get());
// Check a stroke thickness that truncates to 0 when converted to a radius
// (i.e., when multiplied by .5).
paint->thickness(math::bit_cast<float>(1u));
renderer.drawPath(path.get(), paint.get());
// Check NaN stroke thickness.
paint->thickness(std::numeric_limits<float>::quiet_NaN());
renderer.drawPath(path.get(), paint.get());
// Check infinite stroke thickness.
paint->thickness(std::numeric_limits<float>::infinity());
renderer.drawPath(path.get(), paint.get());
// Check -infinite stroke thickness.
paint->thickness(-std::numeric_limits<float>::infinity());
renderer.drawPath(path.get(), paint.get());
auto renderTarget =
renderContext->static_impl_cast<RenderContextNULL>()->makeRenderTarget(
s_frameDescriptor.renderTargetWidth,
s_frameDescriptor.renderTargetHeight);
renderContext->flush({.renderTarget = renderTarget.get()});
}
// When AABB::height() evaluates inf - inf, the result is nan.
TEST_CASE("nan-testcase", "RiveRenderer")
{
std::unique_ptr<RenderContext> nullContext =
RenderContextNULL::MakeContext();
nullContext->beginFrame({
.renderTargetWidth = 2279,
.renderTargetHeight = 710,
});
auto path = nullContext->makeEmptyRenderPath();
path->moveTo(0, std::numeric_limits<float>::infinity());
path->lineTo(1, std::numeric_limits<float>::infinity());
auto paint = nullContext->makeRenderPaint();
RiveRenderer renderer(nullContext.get());
renderer.save();
renderer.align(Fit::fill,
Alignment::center,
{144, 77, 885, 168},
{0, 0, 100, 100});
renderer.drawPath(path.get(), paint.get());
renderer.restore();
auto renderTarget =
nullContext->static_impl_cast<RenderContextNULL>()->makeRenderTarget(
2279,
710);
nullContext->flush({.renderTarget = renderTarget.get()});
}
// This path was found by the fuzzer and triggered an assertion.
TEST_CASE("nan-testcase2", "RiveRenderer")
{
std::unique_ptr<RenderContext> nullContext =
RenderContextNULL::MakeContext();
nullContext->beginFrame({
.renderTargetWidth = 2279,
.renderTargetHeight = 710,
});
auto inf = std::numeric_limits<float>::infinity();
auto nan = std::numeric_limits<float>::quiet_NaN();
auto path = nullContext->makeEmptyRenderPath();
path->lineTo(1, -inf);
path->moveTo(nan, -inf);
auto paint = nullContext->makeRenderPaint();
RiveRenderer renderer(nullContext.get());
renderer.save();
renderer.align(Fit::fill,
{0.260523f, 0.803141f},
{144.649948f, 77.102341f, 885.225891f, 168.455017f},
{0, 0, 100, 100});
renderer.drawPath(path.get(), paint.get());
renderer.restore();
auto renderTarget =
nullContext->static_impl_cast<RenderContextNULL>()->makeRenderTarget(
2279,
710);
nullContext->flush({.renderTarget = renderTarget.get()});
}
// Attempting to create a gradient with invalid stops returns null.
TEST_CASE("invalid-gradient-stops", "RiveRenderer")
{
std::vector<ColorInt> colors(100);
std::unique_ptr<RenderContext> nullContext =
RenderContextNULL::MakeContext();
auto checkGradients = [&](std::vector<float> stops, bool shouldSucceed) {
if (stops.size() > colors.size())
{
return false;
}
if (static_cast<bool>(nullContext->makeLinearGradient(0,
0,
0,
0,
colors.data(),
stops.data(),
stops.size())) !=
shouldSucceed)
{
return false;
}
if (static_cast<bool>(nullContext->makeRadialGradient(0,
0,
0,
colors.data(),
stops.data(),
stops.size())) !=
shouldSucceed)
{
return false;
}
return true;
};
CHECK(checkGradients({0, .5, 1}, true));
auto inf = std::numeric_limits<float>::infinity();
auto nan = std::numeric_limits<float>::quiet_NaN();
// Empty gradients are invalid.
CHECK(checkGradients({0}, true));
CHECK(checkGradients({1}, true));
CHECK(checkGradients({}, false));
// Gradients outside 0..1 are invalid
CHECK(checkGradients({0, .5f, 1.1f}, false));
CHECK(checkGradients({0, .5f, inf}, false));
CHECK(checkGradients({0, .5f, nan}, false));
CHECK(checkGradients({-.1f, .5f, 1}, false));
CHECK(checkGradients({-inf, .5f, 1}, false));
CHECK(checkGradients({nan, .5f, 1}, false));
// Unordered gradients are invalid.
CHECK(checkGradients({0, .4F, .5F, 1}, true));
CHECK(checkGradients({0, .5F, .4F, 1}, false));
CHECK(checkGradients({.4f, 0, .5f, 1}, false));
CHECK(checkGradients({0, .4f, 1, .5f}, false));
CHECK(checkGradients({nan, .5f, 1}, false));
CHECK(checkGradients({0, nan, 1}, false));
CHECK(checkGradients({0, .5f, nan}, false));
}
// Drawing round caps with tiny, negative, and non-finite stroke thickness
// values should not crash.
TEST_CASE("round-cap-edge-values", "RiveRenderer")
{
std::unique_ptr<RenderContext> renderContext =
RenderContextNULL::MakeContext();
renderContext->beginFrame(s_frameDescriptor);
auto path = renderContext->makeEmptyRenderPath();
path->moveTo(25, 25);
path->lineTo(75, 75);
auto emptyPath = renderContext->makeEmptyRenderPath();
emptyPath->close();
emptyPath->moveTo(50, 50);
emptyPath->close();
RiveRenderer renderer(renderContext.get());
auto drawStrokes = [&](float thickness) {
auto stroke = renderContext->makeRenderPaint();
stroke->style(RenderPaintStyle::stroke);
stroke->thickness(thickness);
stroke->cap(StrokeCap::round);
stroke->join(StrokeJoin::round);
renderer.drawPath(path.get(), stroke.get());
renderer.drawPath(emptyPath.get(), stroke.get());
};
for (float thickness = 1; thickness > 0; thickness /= 2)
{
drawStrokes(thickness);
drawStrokes(-thickness);
}
drawStrokes(math::bit_cast<float>(1u));
drawStrokes(std::numeric_limits<float>::infinity());
drawStrokes(-std::numeric_limits<float>::infinity());
drawStrokes(std::numeric_limits<float>::quiet_NaN());
auto renderTarget =
renderContext->static_impl_cast<RenderContextNULL>()->makeRenderTarget(
s_frameDescriptor.renderTargetWidth,
s_frameDescriptor.renderTargetHeight);
renderContext->flush({.renderTarget = renderTarget.get()});
}
// Infinite draw bounds cast to {min32i, min32i, max32i, max32i}.
// Make sure these bounds don't overflow and cause assertions when working with
// the intersection board.
TEST_CASE("infinite-atomic-path", "RiveRenderer")
{
std::unique_ptr<RenderContext> renderContext =
RenderContextNULL::MakeContext();
auto desc = s_frameDescriptor;
desc.disableRasterOrdering = true;
renderContext->beginFrame(desc);
RiveRenderer renderer(renderContext.get());
auto paint = renderContext->makeRenderPaint();
auto path = renderContext->makeEmptyRenderPath();
path->moveTo(-std::numeric_limits<float>::infinity(),
-std::numeric_limits<float>::infinity());
path->lineTo(std::numeric_limits<float>::infinity(),
std::numeric_limits<float>::infinity());
renderer.drawPath(path.get(), paint.get());
auto renderTarget =
renderContext->static_impl_cast<RenderContextNULL>()->makeRenderTarget(
desc.renderTargetWidth,
desc.renderTargetHeight);
renderContext->flush({.renderTarget = renderTarget.get()});
}
#define filter(a) rive::ImageSampler::GetFilterOptionFromKey(a)
#define wrapx(a) rive::ImageSampler::GetWrapXOptionFromKey(a)
#define wrapy(a) rive::ImageSampler::GetWrapYOptionFromKey(a)
TEST_CASE("image-sample-option-conversions", "RiveRenderer")
{
rive::ImageSampler defaultOptions = rive::ImageSampler::LinearClamp();
rive::ImageSampler repeatNearestOptions = {rive::ImageWrap::repeat,
rive::ImageWrap::repeat,
rive::ImageFilter::nearest};
auto defaultKey = defaultOptions.asKey();
CHECK(defaultKey == 0);
auto repeatNearestOptionsKey = repeatNearestOptions.asKey();
CHECK(defaultKey != repeatNearestOptionsKey);
CHECK(wrapx(repeatNearestOptionsKey) == rive::ImageWrap::repeat);
CHECK(wrapy(repeatNearestOptionsKey) == rive::ImageWrap::repeat);
CHECK(filter(repeatNearestOptionsKey) == rive::ImageFilter::nearest);
rive::ImageSampler clampMirrorLinearOptions = {rive::ImageWrap::clamp,
rive::ImageWrap::mirror,
rive::ImageFilter::bilinear};
auto clampMirrorLinearOptionsKey = clampMirrorLinearOptions.asKey();
CHECK(wrapx(clampMirrorLinearOptionsKey) == rive::ImageWrap::clamp);
CHECK(wrapy(clampMirrorLinearOptionsKey) == rive::ImageWrap::mirror);
CHECK(filter(clampMirrorLinearOptionsKey) == rive::ImageFilter::bilinear);
CHECK(clampMirrorLinearOptions != repeatNearestOptions);
rive::ImageSampler repeatClampMipLinearOptions = {
rive::ImageWrap::repeat,
rive::ImageWrap::clamp,
rive::ImageFilter::bilinear};
auto repearClampMipLinearOptionsKey = repeatClampMipLinearOptions.asKey();
CHECK(wrapx(repearClampMipLinearOptionsKey) == rive::ImageWrap::repeat);
CHECK(wrapy(repearClampMipLinearOptionsKey) == rive::ImageWrap::clamp);
CHECK(filter(repearClampMipLinearOptionsKey) ==
rive::ImageFilter::bilinear);
rive::ImageSampler clampRepeatMipNearestOptions = {
rive::ImageWrap::clamp,
rive::ImageWrap::repeat,
rive::ImageFilter::bilinear};
auto clampRepeatMipNearestOptionsKey = clampRepeatMipNearestOptions.asKey();
CHECK(wrapx(clampRepeatMipNearestOptionsKey) == rive::ImageWrap::clamp);
CHECK(wrapy(clampRepeatMipNearestOptionsKey) == rive::ImageWrap::repeat);
CHECK(filter(clampRepeatMipNearestOptionsKey) ==
rive::ImageFilter::bilinear);
rive::ImageSampler mirrorClampMipNearestOptions = {
rive::ImageWrap::mirror,
rive::ImageWrap::clamp,
rive::ImageFilter::nearest};
auto mirrorClampMipNearestOptionsKey = mirrorClampMipNearestOptions.asKey();
CHECK(wrapx(mirrorClampMipNearestOptionsKey) == rive::ImageWrap::mirror);
CHECK(wrapy(mirrorClampMipNearestOptionsKey) == rive::ImageWrap::clamp);
CHECK(filter(mirrorClampMipNearestOptionsKey) ==
rive::ImageFilter::nearest);
}
// Ensure the renderer gracefully handles a null texture within a
// RiveRenderImage.
TEST_CASE("null-render-texture", "RiveRenderer")
{
std::unique_ptr<RenderContext> renderContext =
RenderContextNULL::MakeContext();
auto desc = s_frameDescriptor;
desc.disableRasterOrdering = true;
renderContext->beginFrame(desc);
RiveRenderer renderer(renderContext.get());
class NullTextureImage : public RiveRenderImage
{
public:
NullTextureImage(int width, int height) : RiveRenderImage(width, height)
{
// Never call resetTexture() so that our texture is null.
}
};
auto nullTextureImage = make_rcp<NullTextureImage>(100, 100);
CHECK(nullTextureImage->getTexture() == nullptr);
renderer.drawImage(nullTextureImage.get(),
ImageSampler::LinearClamp(),
BlendMode::screen,
.5f);
renderer.drawImageMesh(
nullTextureImage.get(),
ImageSampler::LinearClamp(),
renderContext->makeRenderBuffer(RenderBufferType::vertex,
RenderBufferFlags::none,
80),
renderContext->makeRenderBuffer(RenderBufferType::vertex,
RenderBufferFlags::none,
80),
renderContext->makeRenderBuffer(RenderBufferType::index,
RenderBufferFlags::none,
100),
10,
50,
BlendMode::colorBurn,
.5f);
auto renderTarget =
renderContext->static_impl_cast<RenderContextNULL>()->makeRenderTarget(
desc.renderTargetWidth,
desc.renderTargetHeight);
renderContext->flush({.renderTarget = renderTarget.get()});
// If we don't crash, the test passed.
CHECK(nullTextureImage->getTexture() == nullptr);
}
} // namespace rive::gpu