blob: abfc9e233d8b37febcadcbc9f601696d1d2e0038 [file]
/*
* Copyright 2026 Rive
*/
// Paths that tessellate to nothing get dropped before they're sorted into
// drawGroups. An empty clip path clips out everything under it.
#include "common/render_context_null.hpp"
#include "rive/renderer/rive_renderer.hpp"
#include <catch.hpp>
#include <map>
using namespace rive;
using namespace rive::gpu;
namespace
{
class CapturingRenderContext : public RenderContextNULL
{
public:
void flush(const FlushDescriptor& desc) override
{
if (desc.drawList == nullptr)
{
return;
}
for (const DrawBatch& batch : *desc.drawList)
{
++m_batchCounts[batch.drawType];
// A dstBlend barrier that nothing reads would be a waste of a barrier.
if (enums::is_flag_set(batch.barriers, BarrierFlags::dstBlend) &&
batch.drawType != DrawType::renderPassInitialize)
{
CHECK(batch.dstReadList != nullptr);
}
}
}
size_t count(DrawType drawType) const
{
auto it = m_batchCounts.find(drawType);
return it != m_batchCounts.end() ? it->second : 0;
}
size_t total() const
{
size_t n = 0;
for (auto [drawType, count] : m_batchCounts)
{
if (drawType != DrawType::renderPassInitialize &&
drawType != DrawType::renderPassResolve)
{
n += count;
}
}
return n;
}
private:
std::map<DrawType, size_t> m_batchCounts;
};
constexpr uint32_t TargetSize = 256;
struct Frame
{
CapturingRenderContext* capture;
std::unique_ptr<RenderContext> ctx;
};
// Use depthStencil/inkbleed since it's a mode that issues dstRead barriers.
Frame beginDepthStencilFrame()
{
auto impl = std::make_unique<CapturingRenderContext>();
CapturingRenderContext* capture = impl.get();
auto ctx = std::make_unique<RenderContext>(std::move(impl));
ctx->beginFrame({
.renderTargetWidth = TargetSize,
.renderTargetHeight = TargetSize,
.msaaSampleCount = 1,
.clockwiseFillOverride = true,
.inkbleedOverride = true,
});
REQUIRE(ctx->frameInterlockMode() == InterlockMode::depthStencil);
return {capture, std::move(ctx)};
}
void flushFrame(Frame& frame)
{
auto target = frame.capture->makeRenderTarget(TargetSize, TargetSize);
frame.ctx->flush({.renderTarget = target.get()});
}
rcp<RenderPath> makeRect(RenderContext* ctx, float l, float t, float r, float b)
{
auto path = ctx->makeEmptyRenderPath();
path->moveTo(l, t);
path->lineTo(r, t);
path->lineTo(r, b);
path->lineTo(l, b);
path->close();
return path;
}
// Two contours that are each a single point: nonempty bounds (so the path
// survives bounds culling), but nothing to tessellate.
rcp<RenderPath> makeEmpty(RenderContext* ctx)
{
auto path = ctx->makeEmptyRenderPath();
path->moveTo(20, 20);
path->close();
path->moveTo(200, 200);
path->close();
return path;
}
} // namespace
TEST_CASE("empty paths are dropped (depthStencil)", "[RiveRenderer][empty]")
{
Frame frame = beginDepthStencilFrame();
auto paint = frame.ctx->makeRenderPaint();
paint->color(0xff336699);
RiveRenderer renderer(frame.ctx.get());
renderer.drawPath(makeEmpty(frame.ctx.get()).get(), paint.get());
flushFrame(frame);
CHECK(frame.capture->total() == 0);
}
TEST_CASE(
"an empty clip clips out everything (depthStencil)",
"[RiveRenderer][empty]")
{
Frame frame = beginDepthStencilFrame();
auto paint = frame.ctx->makeRenderPaint();
paint->color(0xff336699);
RiveRenderer renderer(frame.ctx.get());
renderer.save();
renderer.clipPath(makeEmpty(frame.ctx.get()).get());
renderer.drawPath(
makeRect(frame.ctx.get(), 20, 20, 200, 200).get(),
paint.get());
renderer.restore();
flushFrame(frame);
CHECK(frame.capture->total() == 0);
}
TEST_CASE(
"an empty advanced blend draw doesn't schedule a dstBlend barrier "
"(depthStencil)",
"[RiveRenderer][empty]")
{
Frame frame = beginDepthStencilFrame();
auto srcOver = frame.ctx->makeRenderPaint();
srcOver->color(0xff336699);
auto multiply = frame.ctx->makeRenderPaint();
multiply->color(0xff996633);
multiply->blendMode(BlendMode::multiply);
RiveRenderer renderer(frame.ctx.get());
// The empty advanced blend draw shares a drawGroup with real srcOver draws.
renderer.drawPath(
makeRect(frame.ctx.get(), 20, 20, 120, 120).get(),
srcOver.get());
renderer.drawPath(makeEmpty(frame.ctx.get()).get(), multiply.get());
renderer.drawPath(
makeRect(frame.ctx.get(), 140, 140, 240, 240).get(),
srcOver.get());
flushFrame(frame);
CHECK(frame.capture->total() > 0);
}