blob: 2a58333230f1bfedd85c2f6e39f21b42e7930c76 [file]
/*
* Copyright 2025 Rive
*/
#include <catch.hpp>
#include "rive/renderer/range_chunker.hpp"
#include <algorithm>
#include <iterator>
#include <vector>
using namespace rive;
using namespace rive::gpu;
TEST_CASE("maxPerDrawCommand==2, odd", "[RangeChunker]")
{
RangeChunker c(5, 0, 2);
auto it = c.begin();
CHECK((*it).first == 0);
CHECK((*it).count == 2);
CHECK(it != c.end());
++it;
CHECK((*it).first == 2);
CHECK((*it).count == 2);
CHECK(it != c.end());
++it;
CHECK((*it).first == 4);
CHECK((*it).count == 1);
CHECK(it != c.end());
for (size_t i = 0; i < 10; ++i)
{
++it;
CHECK((*it).first == 5);
CHECK((*it).count == 0);
CHECK(it == c.end());
}
}
TEST_CASE("maxPerDrawCommand==2, even", "[RangeChunker]")
{
RangeChunker c(6, 0, 2);
auto it = c.begin();
CHECK((*it).first == 0);
CHECK((*it).count == 2);
CHECK(it != c.end());
++it;
CHECK((*it).first == 2);
CHECK((*it).count == 2);
CHECK(it != c.end());
++it;
CHECK((*it).first == 4);
CHECK((*it).count == 2);
CHECK(it != c.end());
for (size_t i = 0; i < 10; ++i)
{
++it;
CHECK((*it).first == 6);
CHECK((*it).count == 0);
CHECK(it == c.end());
}
}
TEST_CASE("maxPerDrawCommand==0xffffffff", "[RangeChunker]")
{
RangeChunker c(5, 0, 0xffffffff);
auto it = c.begin();
CHECK((*it).first == 0);
CHECK((*it).count == 5);
CHECK(it != c.end());
for (size_t i = 0; i < 10; ++i)
{
++it;
CHECK((*it).first == 5);
CHECK((*it).count == 0);
CHECK(it == c.end());
}
}
TEST_CASE("maxPerDrawCommand==0", "[RangeChunker]")
{
RangeChunker c(5, 0, 0);
auto it = c.begin();
for (size_t i = 0; i < 10; ++i)
{
CHECK(it != c.end());
CHECK((*it).first == 0);
CHECK((*it).count == 0);
++it;
}
}
TEST_CASE("empty", "[RangeChunker]")
{
RangeChunker c(0, 0, 5);
auto it = c.begin();
for (size_t i = 0; i < 10; ++i)
{
CHECK(it == c.end());
++it;
}
}
TEST_CASE("empty,maxPerDrawCommand==0", "[RangeChunker]")
{
RangeChunker c(0, 0, 0);
auto it = c.begin();
for (size_t i = 0; i < 10; ++i)
{
CHECK(it == c.end());
++it;
}
}
// DSIndexRangeChunker turns a depthStencil batch into draw arguments. The four
// index patterns differ in index count, patch stride, reps per buffer, and
// flags, so every case runs all of them: transposing any one inside the
// iterator would still compile.
constexpr static DrawType DSDrawTypes[] = {
DrawType::stencilMidpointFans,
DrawType::stencilOuterCubics,
DrawType::depthStrokes,
DrawType::depthAAStrokes,
};
static uint32_t patchIndexCount(DrawType drawType)
{
switch (drawType)
{
case DrawType::stencilMidpointFans:
case DrawType::stencilDynamicMidpointFans:
return DSMidpointFanFillPatchIndexCount;
case DrawType::stencilOuterCubics:
case DrawType::stencilDynamicOuterCubics:
return DSOuterCubicFillPatchIndexCount;
case DrawType::depthStrokes:
return DSStrokePatchIndexCount;
case DrawType::depthAAStrokes:
return DSAAStrokePatchIndexCount;
default:
RIVE_UNREACHABLE();
}
}
static int32_t patchFlags(DrawType drawType)
{
switch (drawType)
{
case DrawType::stencilOuterCubics:
case DrawType::stencilDynamicOuterCubics:
return DSVertexFlag_OuterCubicFill;
case DrawType::depthAAStrokes:
return DSVertexFlag_AAPolarStroke;
default:
return 0;
}
}
// depthAAStrokes draw twice: depth only, then color.
static std::vector<int32_t> passFlags(DrawType drawType)
{
if (drawType == DrawType::depthAAStrokes)
{
return {DSVertexFlag_StrokeDepthPass | DSVertexFlag_DisableColorWrite, 0};
}
return {0};
}
// RenderContext premultiplies a depthStencil batch's baseElement into a base
// vertex with the patch flags ORed in, and fills in the index pattern.
static DrawBatch makeBatch(
DrawType drawType,
uint32_t patchCount,
uint32_t firstPatch)
{
DrawBatch batch(
drawType,
ShaderMiscFlags::none,
DrawContents::none,
patchCount,
(firstPatch << dsPatchStrideLog2(drawType)) | patchFlags(drawType),
BlendMode::srcOver,
ImageSampler::LinearClamp(),
BarrierFlags::none);
batch.indexCountPerInstance = patchIndexCount(drawType);
return batch;
}
// A range appended to a batch's list (base premultiplied like makeBatch()).
static DrawBatch::ElementRange makeRange(
DrawType drawType,
uint32_t patchCount,
uint32_t firstPatch)
{
return DrawBatch::ElementRange(
patchCount,
(firstPatch << dsPatchStrideLog2(drawType)) | patchFlags(drawType));
}
static void checkDraw(
DSIndexRangeChunker::Draw draw,
DrawType drawType,
uint32_t patchCount,
uint32_t firstPatch,
int32_t extraFlags = 0)
{
CHECK(draw.indexCount == patchCount * patchIndexCount(drawType));
CHECK(
draw.baseVertex ==
((static_cast<int32_t>(firstPatch) << dsPatchStrideLog2(drawType)) |
patchFlags(drawType) | extraFlags));
}
TEST_CASE("one chunk", "[DSIndexRangeChunker]")
{
for (DrawType drawType : DSDrawTypes)
{
const DrawBatch batch = makeBatch(drawType, 3, 7);
auto iter = DSIndexRangeChunker::drawsForBatch(batch);
auto it = iter.begin();
for (int32_t flags : passFlags(drawType))
{
REQUIRE(it != iter.end());
checkDraw(*it, drawType, 3, 7, flags);
++it;
}
CHECK(it == iter.end());
}
}
TEST_CASE("base patch zero", "[DSIndexRangeChunker]")
{
for (DrawType drawType : DSDrawTypes)
{
const DrawBatch batch = makeBatch(drawType, 1, 0);
auto iter = DSIndexRangeChunker::drawsForBatch(batch);
auto it = iter.begin();
for (int32_t flags : passFlags(drawType))
{
REQUIRE(it != iter.end());
// Nothing but flags, so a midpoint fan draws from 0.
CHECK((*it).baseVertex == (patchFlags(drawType) | flags));
checkDraw(*it, drawType, 1, 0, flags);
++it;
}
CHECK(it == iter.end());
}
}
TEST_CASE("empty batch", "[DSIndexRangeChunker]")
{
for (DrawType drawType : DSDrawTypes)
{
const DrawBatch batch = makeBatch(drawType, 0, 4);
auto iter = DSIndexRangeChunker::drawsForBatch(batch);
CHECK(iter.begin() == iter.end());
}
}
TEST_CASE("splits at the buffer's rep count", "[DSIndexRangeChunker]")
{
for (DrawType drawType : DSDrawTypes)
{
const uint32_t maxReps = dsPatchMaxReps(drawType);
const uint32_t tail = 13;
const DrawBatch batch = makeBatch(drawType, maxReps + tail, 0);
auto iter = DSIndexRangeChunker::drawsForBatch(batch);
auto it = iter.begin();
for (int32_t flags : passFlags(drawType))
{
REQUIRE(it != iter.end());
checkDraw(*it, drawType, maxReps, 0, flags);
++it;
REQUIRE(it != iter.end());
// The second draw restarts at the buffer's first patch; only
// baseVertex advances.
checkDraw(*it, drawType, tail, maxReps, flags);
++it;
}
CHECK(it == iter.end());
}
}
TEST_CASE(
"dynamic fill subpasses carry the color-write flag",
"[DSIndexRangeChunker]")
{
for (DrawType drawType :
{DrawType::stencilDynamicMidpointFans,
DrawType::stencilDynamicOuterCubics})
{
const DrawBatch batch = makeBatch(drawType, 2, 5);
for (bool colorWriteEnabled : {false, true})
{
PipelineState subpassState;
subpassState.colorWriteEnabled = colorWriteEnabled;
auto iter = DSIndexRangeChunker::drawsForDynamicFillSubpass(
batch,
batch.elementRange,
subpassState);
auto it = iter.begin();
REQUIRE(it != iter.end());
checkDraw(
*it,
drawType,
2,
5,
colorWriteEnabled ? 0 : DSVertexFlag_DisableColorWrite);
++it;
CHECK(it == iter.end());
}
}
}
TEST_CASE("AA strokes draw depth before color", "[DSIndexRangeChunker]")
{
const DrawBatch batch = makeBatch(DrawType::depthAAStrokes, 4, 2);
auto iter = DSIndexRangeChunker::drawsForBatch(batch);
auto it = iter.begin();
REQUIRE(it != iter.end());
CHECK(((*it).baseVertex & DSVertexFlag_StrokeDepthPass) != 0);
CHECK(((*it).baseVertex & DSVertexFlag_DisableColorWrite) != 0);
++it;
REQUIRE(it != iter.end());
CHECK(((*it).baseVertex & DSVertexFlag_StrokeDepthPass) == 0);
CHECK(((*it).baseVertex & DSVertexFlag_DisableColorWrite) == 0);
++it;
CHECK(it == iter.end());
}
TEST_CASE(
"range-for covers every patch exactly once per pass",
"[DSIndexRangeChunker]")
{
for (DrawType drawType : DSDrawTypes)
{
const uint32_t maxReps = dsPatchMaxReps(drawType);
const uint32_t patchCount = maxReps * 2 + 3;
const size_t passCount = passFlags(drawType).size();
uint32_t totalIndices = 0;
uint32_t draws = 0;
// The chunker points into the batch, so the batch must outlive the loop.
const DrawBatch batch = makeBatch(drawType, patchCount, 0);
for (auto [indexCount, baseVertex] :
DSIndexRangeChunker::drawsForBatch(batch))
{
totalIndices += indexCount;
++draws;
}
CHECK(draws == 3 * passCount);
CHECK(totalIndices == passCount * patchCount * patchIndexCount(drawType));
}
}
// Range lists only exist for strokes, hairlines, and dynamic fills.
constexpr static DrawType DSListDrawTypes[] = {
DrawType::depthStrokes,
DrawType::depthAAStrokes,
};
TEST_CASE(
"each range runs every pass before the next range",
"[DSIndexRangeChunker]")
{
for (DrawType drawType : DSListDrawTypes)
{
const uint32_t maxReps = dsPatchMaxReps(drawType);
// {patchCount, firstPatch}: empty ranges at the front, middle, and end,
// and one range that spans two chunks.
const uint32_t spec[][2] = {
{0, 1},
{3, 10},
{0, 50},
{maxReps + 5, 100},
{2, 7},
{0, 9},
};
DrawBatch batch = makeBatch(drawType, spec[0][0], spec[0][1]);
std::vector<DrawBatch::ElementRange> ranges;
ranges.reserve(std::size(spec) - 1);
DrawBatch::ElementRange* tail = &batch.elementRange;
for (size_t i = 1; i < std::size(spec); ++i)
{
ranges.push_back(makeRange(drawType, spec[i][0], spec[i][1]));
tail->next = &ranges.back();
tail = tail->next;
}
auto iter = DSIndexRangeChunker::drawsForBatch(batch);
auto it = iter.begin();
for (const auto& [patchCount, firstPatch] : spec)
{
for (int32_t flags : passFlags(drawType))
{
for (uint32_t done = 0; done < patchCount;)
{
const uint32_t chunk = std::min(patchCount - done, maxReps);
REQUIRE(it != iter.end());
checkDraw(*it, drawType, chunk, firstPatch + done, flags);
++it;
done += chunk;
}
}
}
CHECK(it == iter.end());
}
}
TEST_CASE(
"a dynamic fill subpass stops at the next range",
"[DSIndexRangeChunker]")
{
for (DrawType drawType :
{DrawType::stencilDynamicMidpointFans,
DrawType::stencilDynamicOuterCubics})
{
const uint32_t maxReps = dsPatchMaxReps(drawType);
DrawBatch batch = makeBatch(drawType, 2, 5);
DrawBatch::ElementRange second = makeRange(drawType, maxReps + 1, 40);
batch.elementRange.next = &second;
const PipelineState subpassState;
auto first = DSIndexRangeChunker::drawsForDynamicFillSubpass(
batch,
batch.elementRange,
subpassState);
auto it = first.begin();
REQUIRE(it != first.end());
checkDraw(*it, drawType, 2, 5);
++it;
CHECK(it == first.end());
auto rest = DSIndexRangeChunker::drawsForDynamicFillSubpass(
batch,
second,
subpassState);
it = rest.begin();
REQUIRE(it != rest.end());
checkDraw(*it, drawType, maxReps, 40);
++it;
REQUIRE(it != rest.end());
checkDraw(*it, drawType, 1, 40 + maxReps);
++it;
CHECK(it == rest.end());
}
}