Files
EVEngine/test/renderflow.cpp
T

454 lines
16 KiB
C++

#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include "devtools/RenderFlow.hpp"
#include "common/RenderTrace.h"
#include "common/Exception.h"
#include <string>
#include <vector>
using namespace eve::dev;
static bool pathHas(const RenderSliceResult& s, RenderEventKind kind, const char* nameOrDetail) {
for (const auto& e : s.path) {
if (e.kind != kind) continue;
if (e.name == nameOrDetail || e.detail == nameOrDetail) return true;
}
return false;
}
// ---------------------------------------------------------------------------
// Core slicing
// ---------------------------------------------------------------------------
TEST_CASE("devtools.renderflow.passAndBindSlice") {
RenderFlow flow;
flow.frameBegin();
flow.passBegin("RenderSystem2D");
flow.target("screen");
flow.bind("texture", "hero");
flow.draw("drawTexturedRectShaderUV", "hero");
flow.error("draw failed: bad descriptor");
flow.passEnd("RenderSystem2D");
flow.frameEnd();
RenderSliceResult slice = flow.sliceBackward({});
CHECK(!slice.passes.empty());
CHECK_EQ(slice.passes.back(), std::string("RenderSystem2D"));
CHECK(pathHas(slice, RenderEventKind::Bind, "hero"));
CHECK(pathHas(slice, RenderEventKind::Draw, "drawTexturedRectShaderUV"));
CHECK(pathHas(slice, RenderEventKind::Error, "error"));
const std::string report = flow.formatErrorReport("draw failed: bad descriptor");
CHECK(report.find("Render Pipeline Trace") != std::string::npos);
CHECK(report.find("RenderSystem2D") != std::string::npos);
CHECK(report.find("hero") != std::string::npos);
}
TEST_CASE("devtools.renderflow.nestedPasses") {
RenderFlow flow;
flow.frameBegin();
flow.passBegin("RenderSystem3D");
flow.passBegin("ShadowPass");
flow.bind("mesh", "caster");
flow.draw("drawMeshShadow", "cascade");
flow.passEnd("ShadowPass");
flow.bind("mesh", "rock");
flow.bind("texture", "albedo");
flow.draw("drawMeshShader", "default");
flow.error("pipeline create failed");
// still inside RenderSystem3D
RenderSliceResult slice = flow.sliceBackward({});
REQUIRE(slice.passes.size() >= 1u);
CHECK_EQ(slice.passes[0], std::string("RenderSystem3D"));
CHECK(pathHas(slice, RenderEventKind::Draw, "drawMeshShader"));
const bool sawMeshOrAlbedo = pathHas(slice, RenderEventKind::Bind, "rock") ||
pathHas(slice, RenderEventKind::Bind, "albedo");
CHECK(sawMeshOrAlbedo);
}
TEST_CASE("devtools.renderflow.resourceFocusedCriterion") {
RenderFlow flow;
flow.passBegin("RenderSystem2D");
flow.bind("texture", "bg");
flow.draw("drawTexturedRectShaderUV", "bg");
flow.bind("texture", "hero");
flow.draw("drawTexturedRectShaderUV", "hero");
flow.error("hero atlas missing");
RenderSliceCriterion c;
c.resource = "hero";
RenderSliceResult slice = flow.sliceBackward(c);
CHECK(pathHas(slice, RenderEventKind::Bind, "hero"));
CHECK(pathHas(slice, RenderEventKind::Error, "error"));
}
TEST_CASE("devtools.renderflow.eventIdSeed") {
RenderFlow flow;
flow.passBegin("P");
flow.bind("shader", "s1");
const uint32_t drawId = [&]() {
flow.draw("drawSolidRect", "solid");
return flow.events().back().id;
}();
flow.bind("shader", "s2");
flow.draw("drawSolidRect", "solid2");
flow.error("later");
RenderSliceCriterion c;
c.eventId = drawId;
RenderSliceResult slice = flow.sliceBackward(c);
CHECK(pathHas(slice, RenderEventKind::Draw, "drawSolidRect"));
// Seeded on first draw — should see s1 bind via data deps / parent chain.
bool sawS1 = pathHas(slice, RenderEventKind::Bind, "s1");
CHECK(sawS1);
}
TEST_CASE("devtools.renderflow.emptySlice") {
RenderFlow flow;
RenderSliceResult slice = flow.sliceBackward({});
CHECK(slice.eventIds.empty());
CHECK(slice.summary.find("empty") != std::string::npos);
}
TEST_CASE("devtools.renderflow.clear") {
RenderFlow flow;
flow.frameBegin();
flow.draw("x", "y");
flow.clear();
CHECK_EQ(flow.eventCount(), 0u);
CHECK(flow.event(1) == nullptr);
}
TEST_CASE("devtools.renderflow.drawWithoutBinds") {
RenderFlow flow;
flow.passBegin("RenderSystem2D");
flow.draw("drawSolidRect", "solid");
flow.error("clear color invalid");
RenderSliceResult slice = flow.sliceBackward({});
CHECK(pathHas(slice, RenderEventKind::PassBegin, "RenderSystem2D"));
CHECK(pathHas(slice, RenderEventKind::Draw, "drawSolidRect"));
}
TEST_CASE("devtools.renderflow.errorWithoutDraw") {
RenderFlow flow;
flow.passBegin("Present");
flow.error("swapchain recreate failed");
RenderSliceResult slice = flow.sliceBackward({});
CHECK(pathHas(slice, RenderEventKind::Error, "error"));
CHECK(pathHas(slice, RenderEventKind::PassBegin, "Present"));
}
TEST_CASE("devtools.renderflow.multipleErrorsSeedsLast") {
RenderFlow flow;
flow.passBegin("A");
flow.error("first");
flow.passEnd("A");
flow.passBegin("B");
flow.bind("mesh", "m");
flow.error("second");
RenderSliceResult slice = flow.sliceBackward({});
CHECK(pathHas(slice, RenderEventKind::PassBegin, "B"));
CHECK(pathHas(slice, RenderEventKind::Bind, "m"));
// Active passes at last error should be B only.
REQUIRE(!slice.passes.empty());
CHECK_EQ(slice.passes.back(), std::string("B"));
}
TEST_CASE("devtools.renderflow.targetSwitch") {
RenderFlow flow;
flow.passBegin("RenderSystem2D");
flow.target("screen");
flow.draw("drawSolidRect", "a");
flow.target("canvas");
flow.draw("drawSolidRect", "b");
flow.error("canvas size 0");
RenderSliceResult slice = flow.sliceBackward({});
CHECK(pathHas(slice, RenderEventKind::Target, "canvas"));
}
TEST_CASE("devtools.renderflow.bindKinds") {
RenderFlow flow;
flow.passBegin("RenderSystem3D");
flow.bind("texture", "albedo");
flow.bind("shader", "pbr");
flow.bind("mesh", "rock");
flow.bind("font", "ui");
flow.bind("canvas", "offscreen");
flow.draw("drawMeshShader", "pbr");
flow.error("descriptor");
RenderSliceResult slice = flow.sliceBackward({});
CHECK(pathHas(slice, RenderEventKind::Bind, "albedo"));
const bool sawShaderOrDraw = pathHas(slice, RenderEventKind::Bind, "pbr") ||
pathHas(slice, RenderEventKind::Draw, "drawMeshShader");
CHECK(sawShaderOrDraw);
CHECK(pathHas(slice, RenderEventKind::Bind, "rock"));
}
TEST_CASE("devtools.renderflow.twoFramesErrorInSecond") {
RenderFlow flow;
flow.frameBegin();
flow.passBegin("RenderSystem2D");
flow.draw("drawSolidRect", "ok");
flow.passEnd("RenderSystem2D");
flow.frameEnd();
flow.frameBegin();
flow.passBegin("RenderSystem2D");
flow.bind("texture", "bad");
flow.draw("drawTexturedRectShaderUV", "bad");
flow.error("oom");
RenderSliceResult slice = flow.sliceBackward({});
CHECK(pathHas(slice, RenderEventKind::Bind, "bad"));
CHECK(pathHas(slice, RenderEventKind::Error, "error"));
}
TEST_CASE("devtools.renderflow.formatReportTruncates") {
RenderFlow flow;
flow.passBegin("P");
for (int i = 0; i < 80; ++i) {
flow.bind("texture", ("t" + std::to_string(i)).c_str());
flow.draw("draw", ("d" + std::to_string(i)).c_str());
}
flow.error("boom");
const std::string report = flow.formatErrorReport("boom");
CHECK(report.find("...") != std::string::npos);
CHECK(report.find("render slice:") != std::string::npos);
}
TEST_CASE("devtools.renderflow.passEndPopsStack") {
RenderFlow flow;
flow.passBegin("Outer");
flow.passBegin("Inner");
flow.passEnd("Inner");
flow.error("after inner");
RenderSliceResult slice = flow.sliceBackward({});
REQUIRE(slice.passes.size() == 1u);
CHECK_EQ(slice.passes[0], std::string("Outer"));
}
TEST_CASE("devtools.renderflow.nullNameArgs") {
RenderFlow flow;
flow.passBegin(nullptr);
flow.bind(nullptr, nullptr);
flow.draw(nullptr, nullptr);
flow.target(nullptr);
flow.error(nullptr);
CHECK(flow.eventCount() >= 5u);
RenderSliceResult slice = flow.sliceBackward({});
CHECK(!slice.eventIds.empty());
}
// ---------------------------------------------------------------------------
// Ring buffer
// ---------------------------------------------------------------------------
TEST_CASE("devtools.renderflow.ringBuffer") {
RenderFlow flow;
flow.setMaxEvents(16);
for (int i = 0; i < 40; ++i) flow.draw("drawSolidRect", "solid");
CHECK_EQ(flow.eventCount(), 16u);
CHECK(flow.event(flow.events().front().id - 1) == nullptr);
}
TEST_CASE("devtools.renderflow.setMaxEventsClampShrink") {
RenderFlow flow;
flow.setMaxEvents(1);
CHECK_EQ(flow.maxEvents(), 16u);
for (int i = 0; i < 20; ++i) flow.draw("d", "x");
CHECK_EQ(flow.eventCount(), 16u);
flow.setMaxEvents(16);
CHECK_EQ(flow.eventCount(), 16u);
}
TEST_CASE("devtools.renderflow.ringKeepsRecentErrorContext") {
RenderFlow flow;
flow.setMaxEvents(16);
for (int i = 0; i < 40; ++i) {
flow.passBegin("Old");
flow.draw("old", "x");
flow.passEnd("Old");
}
flow.passBegin("Hot");
flow.bind("mesh", "last");
flow.draw("drawMeshShader", "last");
flow.error("fail");
CHECK_EQ(flow.eventCount(), 16u);
RenderSliceResult slice = flow.sliceBackward({});
const bool sawHotOrError = pathHas(slice, RenderEventKind::PassBegin, "Hot") ||
pathHas(slice, RenderEventKind::Error, "error");
CHECK(sawHotOrError);
const bool sawLast = pathHas(slice, RenderEventKind::Bind, "last") ||
pathHas(slice, RenderEventKind::Draw, "drawMeshShader");
CHECK(sawLast);
}
TEST_CASE("devtools.renderflow.oneInOneOut") {
RenderFlow flow;
flow.setMaxEvents(16);
for (int i = 0; i < 16; ++i) flow.draw("d", "x");
const uint32_t front = flow.events().front().id;
flow.draw("d", "new");
CHECK_EQ(flow.eventCount(), 16u);
CHECK_EQ(flow.events().front().id, front + 1);
CHECK_EQ(flow.events().back().detail, std::string("new"));
}
// ---------------------------------------------------------------------------
// Hooks / Exception / RAII
// ---------------------------------------------------------------------------
TEST_CASE("devtools.renderflow.hookNoopWhenUnset") {
eve::debug::setRenderTracer(nullptr);
eve::debug::rtFrameBegin();
eve::debug::rtPassBegin("X");
eve::debug::rtBind("texture", "t");
eve::debug::rtDraw("draw", "d");
eve::debug::rtPassEnd("X");
eve::debug::rtFrameEnd();
eve::debug::rtError("no tracer");
CHECK(eve::debug::renderTracer() == nullptr);
}
TEST_CASE("devtools.renderflow.hookViaException") {
RenderFlow flow;
eve::debug::setRenderTracer(&flow);
eve::debug::rtFrameBegin();
eve::debug::rtPassBegin("RenderSystem3D");
eve::debug::rtBind("mesh", "rock");
eve::debug::rtDraw("drawMeshShader", "default");
try {
throw eve::Exception("failed to create mesh3d-style pipeline");
} catch (const eve::Exception& e) {
const std::string report = flow.formatErrorReport(e.what());
CHECK(report.find("Render Pipeline Trace") != std::string::npos);
CHECK(report.find("RenderSystem3D") != std::string::npos);
const bool hasDrawOrBind = report.find("drawMeshShader") != std::string::npos ||
report.find("rock") != std::string::npos;
CHECK(hasDrawOrBind);
CHECK(std::string(e.what()).find("mesh3d") != std::string::npos);
}
eve::debug::setRenderTracer(nullptr);
CHECK(eve::debug::renderTracer() == nullptr);
}
TEST_CASE("devtools.renderflow.exceptionWithoutTracer") {
eve::debug::setRenderTracer(nullptr);
try {
throw eve::Exception("newTexture: invalid args");
} catch (const eve::Exception& e) {
CHECK(std::string(e.what()).find("newTexture") != std::string::npos);
}
}
TEST_CASE("devtools.renderflow.passScopeRAII") {
RenderFlow flow;
eve::debug::setRenderTracer(&flow);
{
eve::debug::RenderPassScope scope("ScopedPass");
eve::debug::rtDraw("inside", "x");
}
// After scope, pass should be ended.
bool sawBegin = false, sawEnd = false;
for (const auto& e : flow.events()) {
if (e.kind == RenderEventKind::PassBegin && e.name == "ScopedPass") sawBegin = true;
if (e.kind == RenderEventKind::PassEnd && e.name == "ScopedPass") sawEnd = true;
}
CHECK(sawBegin);
CHECK(sawEnd);
eve::debug::setRenderTracer(nullptr);
}
TEST_CASE("devtools.renderflow.rtHelpersForward") {
RenderFlow flow;
eve::debug::setRenderTracer(&flow);
eve::debug::rtFrameBegin();
eve::debug::rtTarget("screen");
eve::debug::rtBind("font", "main");
eve::debug::rtDraw("print", "text");
eve::debug::rtError("Graphics::drawText: font must not be null");
eve::debug::rtFrameEnd();
CHECK(flow.eventCount() >= 6u);
RenderSliceResult slice = flow.sliceBackward({});
const bool sawPrint = pathHas(slice, RenderEventKind::Bind, "main") ||
pathHas(slice, RenderEventKind::Draw, "print");
CHECK(sawPrint);
eve::debug::setRenderTracer(nullptr);
}
TEST_CASE("devtools.renderflow.shadowThenMainPass") {
RenderFlow flow;
flow.frameBegin();
flow.passBegin("RenderSystem3D");
for (int c = 0; c < 3; ++c) {
flow.passBegin("ShadowPass");
flow.bind("mesh", "caster");
flow.draw("drawMeshShadow", "cascade");
flow.passEnd("ShadowPass");
}
flow.target("screen");
flow.bind("mesh", "hero");
flow.bind("texture", "heroAlbedo");
flow.draw("drawMeshShader", "default");
flow.error("validation: unbound descriptor");
flow.passEnd("RenderSystem3D");
flow.frameEnd();
RenderSliceResult slice = flow.sliceBackward({});
CHECK(pathHas(slice, RenderEventKind::Draw, "drawMeshShader"));
CHECK(pathHas(slice, RenderEventKind::PassBegin, "RenderSystem3D"));
const std::string report = flow.formatErrorReport("validation: unbound descriptor");
CHECK(report.find("Active passes") != std::string::npos);
}
TEST_CASE("devtools.renderflow.simulateSpriteBatch") {
RenderFlow flow;
flow.setMaxEvents(128);
flow.frameBegin();
flow.passBegin("RenderSystem2D");
flow.target("screen");
for (int i = 0; i < 50; ++i) {
flow.bind("texture", "sprite");
flow.draw("drawTexturedRectShaderUV", "textured");
}
flow.bind("texture", "boss");
flow.draw("drawTexturedRectLitUV", "lit2d");
flow.error("lit2d normal missing");
flow.passEnd("RenderSystem2D");
flow.passBegin("Present");
flow.passEnd("Present");
flow.frameEnd();
RenderSliceResult slice = flow.sliceBackward({});
CHECK(pathHas(slice, RenderEventKind::Draw, "drawTexturedRectLitUV"));
CHECK(pathHas(slice, RenderEventKind::Error, "error"));
}
TEST_CASE("devtools.renderflow.rebindSameNameUpdatesReaching") {
RenderFlow flow;
flow.passBegin("P");
flow.bind("texture", "hero"); // v1
flow.draw("draw", "hero");
flow.bind("texture", "hero"); // v2 overwrite
flow.draw("draw", "hero");
flow.error("bad");
// Last draw should prefer the second bind; both may appear via control chain,
// but data deps for the last draw should include a Bind of hero.
RenderSliceResult slice = flow.sliceBackward({});
CHECK(pathHas(slice, RenderEventKind::Bind, "hero"));
}
TEST_CASE("devtools.renderflow.eventLookupContiguous") {
RenderFlow flow;
flow.setMaxEvents(16);
for (int i = 0; i < 20; ++i) flow.draw("d", "x");
const uint32_t lo = flow.events().front().id;
const uint32_t hi = flow.events().back().id;
for (uint32_t id = lo; id <= hi; ++id) {
REQUIRE(flow.event(id) != nullptr);
CHECK_EQ(flow.event(id)->id, id);
}
CHECK(flow.event(lo - 1) == nullptr);
CHECK(flow.event(hi + 1) == nullptr);
}