blob: 709e4796f631de26bac577a780c4fdb90b1c2f3e [file]
/*
* Copyright 2026 Rive
*/
// The GPU census walks the replayer's resident tables. What it has to get
// right to be usable as evidence: it counts what is live and not what was
// freed, it scales with the resources actually resident, and it is a level so
// reading it twice gives the same answer.
#include "rive/renderer/cmd/deferred_replayer.hpp"
#include "rive/renderer/cmd/deferred_session.hpp"
#include "rive/renderer/cmd/gpu_census.hpp"
#include "deferred_test_sink.hpp"
#include <catch.hpp>
using namespace rive;
using deferred_test::TestSink;
namespace
{
// Replay one session frame and hand back what stayed resident.
cmd::GpuCensus replayAndCensus(cmd::DeferredSession& session,
cmd::DeferredReplayer& replayer,
TestSink& sink)
{
cmd::DeferredFrame frame = cmd::takeFrame(session);
replayer.replayFrame(frame, sink);
return replayer.gpuCensus();
}
} // namespace
TEST_CASE("the census counts what replay left resident", "[deferred][census]")
{
cmd::DeferredSession session(nullptr);
cmd::DeferredReplayer replayer;
TestSink sink;
auto* screen = session.screenRenderer();
auto paint = session.makeRenderPaint();
auto path = session.makeEmptyRenderPath();
screen->drawPath(path.get(), paint.get());
cmd::GpuCensus c = replayAndCensus(session, replayer, sink);
CHECK(c.paths == 1);
CHECK(c.paints == 1);
// Nothing sized was recorded, so the byte total has to be zero rather than
// some incidental nonzero from the count tables.
CHECK(c.totalBytes() == 0);
// A level, not a running total: the same walk twice is the same answer.
CHECK(replayer.gpuCensus().totalBytes() == c.totalBytes());
CHECK(replayer.gpuCensus().liveObjects() == c.liveObjects());
}
TEST_CASE("census bytes scale with the resources resident",
"[deferred][census]")
{
cmd::DeferredSession session(nullptr);
cmd::DeferredReplayer replayer;
TestSink sink;
auto* screen = session.screenRenderer();
auto paint = session.makeRenderPaint();
auto buffer = session.makeRenderBuffer(RenderBufferType::vertex,
RenderBufferFlags::none,
1024);
// Touch it so the draw keeps the recording honest about a live buffer.
auto path = session.makeEmptyRenderPath();
screen->drawPath(path.get(), paint.get());
cmd::GpuCensus one = replayAndCensus(session, replayer, sink);
CHECK(one.buffers == 1);
CHECK(one.bufferBytes == 1024);
CHECK(one.totalBytes() == 1024);
// A second buffer of the same size doubles the sized total, and the
// unsized counts stay put.
auto buffer2 = session.makeRenderBuffer(RenderBufferType::vertex,
RenderBufferFlags::none,
1024);
screen->drawPath(path.get(), paint.get());
cmd::GpuCensus two = replayAndCensus(session, replayer, sink);
CHECK(two.buffers == 2);
CHECK(two.bufferBytes == 2048);
CHECK(two.paths == one.paths);
CHECK(two.paints == one.paints);
}
TEST_CASE("a destroyed resource leaves the census but keeps its slot",
"[deferred][census]")
{
cmd::DeferredSession session(nullptr);
cmd::DeferredReplayer replayer;
TestSink sink;
auto* screen = session.screenRenderer();
auto paint = session.makeRenderPaint();
auto path = session.makeEmptyRenderPath();
{
auto doomed = session.makeRenderBuffer(RenderBufferType::vertex,
RenderBufferFlags::none,
4096);
screen->drawPath(path.get(), paint.get());
cmd::GpuCensus live = replayAndCensus(session, replayer, sink);
CHECK(live.bufferBytes == 4096);
CHECK(live.slots2d >= live.liveObjects());
}
// The rcp died, so the next frame carries the destroy record.
session.commandBuffer().drainDestroys();
screen->drawPath(path.get(), paint.get());
cmd::GpuCensus after = replayAndCensus(session, replayer, sink);
CHECK(after.buffers == 0);
CHECK(after.bufferBytes == 0);
// The tables never compact, so the freed slot is still counted as minted.
CHECK(after.slots2d >= 1);
}
TEST_CASE("reset empties the census", "[deferred][census]")
{
cmd::DeferredSession session(nullptr);
cmd::DeferredReplayer replayer;
TestSink sink;
auto* screen = session.screenRenderer();
auto paint = session.makeRenderPaint();
auto path = session.makeEmptyRenderPath();
auto buffer = session.makeRenderBuffer(RenderBufferType::vertex,
RenderBufferFlags::none,
2048);
screen->drawPath(path.get(), paint.get());
CHECK(replayAndCensus(session, replayer, sink).totalBytes() == 2048);
replayer.reset();
cmd::GpuCensus empty = replayer.gpuCensus();
CHECK(empty.totalBytes() == 0);
CHECK(empty.liveObjects() == 0);
CHECK(empty.slots2d == 0);
CHECK(empty.slotsOre == 0);
}
TEST_CASE("ore texture sizing covers mips, layers and samples",
"[deferred][census]")
{
// No GPU here, so size the arithmetic directly against the format table
// rather than through a real texture.
using rive::ore::TextureFormat;
CHECK(ore::textureFormatBytesPerTexel(TextureFormat::rgba8unorm) == 4);
CHECK(ore::textureFormatBytesPerTexel(TextureFormat::r8unorm) == 1);
// rgba32float is 16 bytes, so a 4x4 single level is 256.
CHECK(ore::textureFormatBytesPerTexel(TextureFormat::rgba32float) == 16);
}