feat(benchmarks): add optional windowed rendering performance suite
Give GPU/CPU frame timings for 2D batching, ECS sprites, lit/shadowed meshes, and GPU particles without folding that into unit tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
6443348d0e
commit
7b493d9f03
3 files changed
+440
No files matched your search
@@ -103,6 +103,8 @@ option(EVENGINE_BUILD_PROFILE_CHECKS
|
||||
"Build independent public-header and optional-capability profile checks" OFF)
|
||||
option(EVENGINE_BUILD_PIXELWORLD_BENCHMARK
|
||||
"Build the renderer-independent PixelWorld million-cell benchmark" OFF)
|
||||
option(EVENGINE_BUILD_RENDER_BENCHMARK
|
||||
"Build the windowed engine rendering performance benchmark" OFF)
|
||||
option(EVENGINE_BUILD_DEMO "Build optional Demo module (procedural demo assets + default demo.nut)" ON)
|
||||
option(EVENGINE_VULKAN_USE_VMA
|
||||
"Use the bundled Vulkan Memory Allocator for VKBuilder buffers" ON)
|
||||
@@ -465,6 +467,9 @@ function(main)
|
||||
if(EVENGINE_BUILD_PIXELWORLD_BENCHMARK)
|
||||
add_subdirectory(benchmarks/pixelworld)
|
||||
endif()
|
||||
if(EVENGINE_BUILD_HOST AND EVENGINE_BUILD_RENDER_BENCHMARK)
|
||||
add_subdirectory(benchmarks/render)
|
||||
endif()
|
||||
if(EVENGINE_BUILD_HOST)
|
||||
add_subdirectory(src/scripts)
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
add_executable(render_benchmark main.cpp)
|
||||
|
||||
eve_append_system_libraries(_render_bench_sys)
|
||||
eve_thirdparty_libs(_render_bench_tp ${EVE_THIRDPARTY_GROUPS})
|
||||
set(_render_bench_libs EVScripts ${EVE_MODULE_LIBS})
|
||||
if(TARGET eve_imgui)
|
||||
list(APPEND _render_bench_libs eve_imgui)
|
||||
endif()
|
||||
if(EVENGINE_BUILD_DEMO)
|
||||
list(APPEND _render_bench_libs EVDemo)
|
||||
endif()
|
||||
if(EVENGINE_PLATFORM_LIB)
|
||||
list(APPEND _render_bench_libs ${EVENGINE_PLATFORM_LIB})
|
||||
endif()
|
||||
|
||||
target_link_libraries(render_benchmark PRIVATE
|
||||
eve_engine_includes
|
||||
${_render_bench_libs}
|
||||
zeroerr
|
||||
${_render_bench_tp}
|
||||
${_render_bench_sys}
|
||||
${EVENGINE_VULKAN_LIB}
|
||||
${EVENGINE_WEBGPU_LIB})
|
||||
|
||||
eve_module_enabled(particles _render_bench_particles)
|
||||
if(_render_bench_particles)
|
||||
target_compile_definitions(render_benchmark PRIVATE EVENGINE_BENCH_PARTICLES=1)
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
target_compile_definitions(render_benchmark PRIVATE
|
||||
POCO_STATIC POCO_NO_AUTOMATIC_LIBS NOMINMAX)
|
||||
target_link_libraries(render_benchmark PRIVATE EVBacktrace)
|
||||
endif()
|
||||
@@ -0,0 +1,401 @@
|
||||
#include "common/Capability.h"
|
||||
#include "common/CrashHandler.h"
|
||||
#include "common/Exception.h"
|
||||
#include "common/GpuTimer.h"
|
||||
#include "graphics/Color.h"
|
||||
#include "graphics/Graphics.h"
|
||||
#include "graphics/Light.h"
|
||||
#include "graphics/Mesh.h"
|
||||
#include "graphics/RenderSystem.h"
|
||||
#include "graphics/RenderSystem3D.h"
|
||||
#include "graphics/Texture.h"
|
||||
#include "window/Window.h"
|
||||
|
||||
#if defined(EVENGINE_BENCH_PARTICLES)
|
||||
#include "particles/ParticleEmitter.h"
|
||||
#include "particles/ParticleSystem.h"
|
||||
#include "particles/Particles.h"
|
||||
#endif
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using eve::graphics::Camera2D;
|
||||
using eve::graphics::Camera3D;
|
||||
using eve::graphics::Color;
|
||||
using eve::graphics::Graphics;
|
||||
using eve::graphics::Light3D;
|
||||
using eve::graphics::Mesh;
|
||||
using eve::graphics::Renderable2D;
|
||||
using eve::graphics::Renderable3D;
|
||||
using eve::graphics::RenderSystem;
|
||||
using eve::graphics::RenderSystem3D;
|
||||
using eve::graphics::Texture;
|
||||
|
||||
std::size_t parsePositive(char *text, std::size_t fallback) {
|
||||
if (text == nullptr) return fallback;
|
||||
char *end = nullptr;
|
||||
const unsigned long long value = std::strtoull(text, &end, 10);
|
||||
return end != text && *end == '\0' && value > 0 ? std::size_t(value) : fallback;
|
||||
}
|
||||
|
||||
int parseEnvFrames(int fallback) {
|
||||
const char *env = std::getenv("EVENGINE_PERF_FRAMES");
|
||||
if (!env || !env[0]) return fallback;
|
||||
const int n = std::atoi(env);
|
||||
return n > 8 ? n : 8;
|
||||
}
|
||||
|
||||
void pumpEvents() {
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event)) {
|
||||
}
|
||||
}
|
||||
|
||||
void hideScene() {
|
||||
if (ecs::current()->getManager<Renderable3D>() != nullptr) {
|
||||
auto view = ecs::View<Renderable3D, Renderable3D::MeshRenderer>();
|
||||
for (auto it = view.begin(); it != view.end(); ++it) {
|
||||
auto [mr] = *it;
|
||||
mr->visible = false;
|
||||
}
|
||||
}
|
||||
if (ecs::current()->getManager<Camera3D>() != nullptr) {
|
||||
auto camView = ecs::View<Camera3D, Camera3D::Data>();
|
||||
for (auto it = camView.begin(); it != camView.end(); ++it) {
|
||||
auto [data] = *it;
|
||||
data->active = false;
|
||||
}
|
||||
}
|
||||
if (ecs::current()->getManager<Light3D>() != nullptr) {
|
||||
auto lightView = ecs::View<Light3D, Light3D::Data>();
|
||||
for (auto it = lightView.begin(); it != lightView.end(); ++it) {
|
||||
auto [d] = *it;
|
||||
d->enabled = false;
|
||||
d->castShadow = false;
|
||||
}
|
||||
}
|
||||
if (ecs::current()->getManager<Renderable2D>() != nullptr) {
|
||||
auto view = ecs::View<Renderable2D, Renderable2D::Sprite>();
|
||||
for (auto it = view.begin(); it != view.end(); ++it) {
|
||||
auto [sp] = *it;
|
||||
sp->visible = false;
|
||||
}
|
||||
}
|
||||
if (ecs::current()->getManager<Camera2D>() != nullptr) {
|
||||
auto camView = ecs::View<Camera2D, Camera2D::Data>();
|
||||
for (auto it = camView.begin(); it != camView.end(); ++it) {
|
||||
auto [data] = *it;
|
||||
data->active = false;
|
||||
}
|
||||
}
|
||||
#if defined(EVENGINE_BENCH_PARTICLES)
|
||||
if (ecs::current()->getManager<eve::particles::ParticleEmitter>() != nullptr) {
|
||||
auto view = ecs::View<eve::particles::ParticleEmitter, eve::particles::ParticleEmitter::Draw>();
|
||||
for (auto it = view.begin(); it != view.end(); ++it) {
|
||||
auto [draw] = *it;
|
||||
draw->visible = false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
Texture *makeSolid(Graphics *gfx, std::uint8_t r, std::uint8_t g, std::uint8_t b) {
|
||||
const std::uint8_t px[4] = {r, g, b, 255};
|
||||
return gfx->newTexture(1, 1, px);
|
||||
}
|
||||
|
||||
int gridSide(std::size_t count) {
|
||||
return std::max(1, static_cast<int>(std::ceil(std::sqrt(double(count)))));
|
||||
}
|
||||
|
||||
void spawnSprites(Graphics *gfx, Texture *tex, std::size_t count) {
|
||||
auto *cam = Camera2D::createCamera();
|
||||
cam->setPosition(float(gfx->getWidth()) * 0.5f, float(gfx->getHeight()) * 0.5f);
|
||||
const int side = gridSide(count);
|
||||
const float cell = 18.f;
|
||||
const float originX = float(gfx->getWidth()) * 0.5f - float(side) * cell * 0.5f;
|
||||
const float originY = float(gfx->getHeight()) * 0.5f - float(side) * cell * 0.5f;
|
||||
for (std::size_t i = 0; i < count; ++i) {
|
||||
auto *sprite = Renderable2D::create();
|
||||
sprite->setTexture(tex);
|
||||
sprite->setSize(16.f, 16.f);
|
||||
sprite->setPosition(originX + float(i % std::size_t(side)) * cell,
|
||||
originY + float(i / std::size_t(side)) * cell);
|
||||
sprite->setColor(0.85f, 0.9f, 1.f, 1.f);
|
||||
sprite->setReceiveLight(false);
|
||||
}
|
||||
}
|
||||
|
||||
void spawnCubes(Mesh *mesh, Texture *tex, std::size_t count, bool lit, bool shadows) {
|
||||
const int side = gridSide(count);
|
||||
const float spacing = 1.6f;
|
||||
const float origin = -float(side - 1) * spacing * 0.5f;
|
||||
auto *cam = Camera3D::createCamera();
|
||||
const float span = float(side) * spacing;
|
||||
cam->setEye(0.f, span * 0.85f, span * 1.15f);
|
||||
cam->setTarget(0.f, 0.f, 0.f);
|
||||
cam->data()->nearZ = 0.1f;
|
||||
cam->data()->farZ = std::max(80.f, span * 4.f);
|
||||
cam->data()->ambientR = lit ? 0.08f : 0.55f;
|
||||
cam->data()->ambientG = lit ? 0.09f : 0.55f;
|
||||
cam->data()->ambientB = lit ? 0.11f : 0.6f;
|
||||
if (lit) {
|
||||
auto *light = Light3D::createLight("dir");
|
||||
light->setDirection(0.35f, 1.f, 0.25f);
|
||||
light->setColor(1.f, 0.97f, 0.92f, 2.4f);
|
||||
light->setCastShadow(shadows);
|
||||
} else {
|
||||
RenderSystem3D::setDirectionalLight(0.f, 1.f, 0.f, 0.f, 0.f, 0.f);
|
||||
}
|
||||
for (std::size_t i = 0; i < count; ++i) {
|
||||
auto *ent = Renderable3D::create();
|
||||
ent->setMesh(mesh);
|
||||
ent->setTexture(tex);
|
||||
ent->setPosition(origin + float(i % std::size_t(side)) * spacing, 0.f,
|
||||
origin + float(i / std::size_t(side)) * spacing);
|
||||
ent->setMetallic(0.05f);
|
||||
ent->setRoughness(0.55f);
|
||||
if (shadows) ent->setCastShadow(true);
|
||||
}
|
||||
}
|
||||
|
||||
struct FrameStats {
|
||||
std::vector<double> cpuMs;
|
||||
std::vector<double> gpuMs;
|
||||
};
|
||||
|
||||
void recordGpu(FrameStats &stats) {
|
||||
if (auto *timer = eve::cap::query<eve::service::IGpuTimer>()) {
|
||||
if (timer->gpuTimingAvailable()) stats.gpuMs.push_back(double(timer->gpuFrameMs()));
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Fn>
|
||||
FrameStats measureFrames(int warmup, int timed, Fn &&draw) {
|
||||
FrameStats stats;
|
||||
stats.cpuMs.reserve(std::size_t(timed));
|
||||
for (int i = 0; i < warmup; ++i) {
|
||||
draw();
|
||||
pumpEvents();
|
||||
}
|
||||
using Clock = std::chrono::steady_clock;
|
||||
for (int i = 0; i < timed; ++i) {
|
||||
const auto started = Clock::now();
|
||||
draw();
|
||||
pumpEvents();
|
||||
stats.cpuMs.push_back(std::chrono::duration<double, std::milli>(Clock::now() - started).count());
|
||||
recordGpu(stats);
|
||||
}
|
||||
return stats;
|
||||
}
|
||||
|
||||
double percentile(std::vector<double> values, int pct) {
|
||||
if (values.empty()) return 0.0;
|
||||
std::sort(values.begin(), values.end());
|
||||
const std::size_t index =
|
||||
std::min(values.size() - 1, (values.size() * std::size_t(pct) + 99) / 100 - 1);
|
||||
return values[index];
|
||||
}
|
||||
|
||||
double mean(const std::vector<double> &values) {
|
||||
if (values.empty()) return 0.0;
|
||||
double sum = 0.0;
|
||||
for (double v : values) sum += v;
|
||||
return sum / double(values.size());
|
||||
}
|
||||
|
||||
void printJson(std::string_view workload, std::size_t items, int frames, int width, int height,
|
||||
bool vsync, std::string_view backend, const FrameStats &stats) {
|
||||
const double p50 = percentile(stats.cpuMs, 50);
|
||||
const double p95 = percentile(stats.cpuMs, 95);
|
||||
const double avgMs = mean(stats.cpuMs);
|
||||
const double avgFps = avgMs > 0.0 ? 1000.0 / avgMs : 0.0;
|
||||
const double gpuP50 = percentile(stats.gpuMs, 50);
|
||||
const double gpuP95 = percentile(stats.gpuMs, 95);
|
||||
std::cout << "RENDER_BENCHMARK_JSON={\"workload\":\"" << workload << "\",\"items\":" << items
|
||||
<< ",\"frames\":" << frames << ",\"width\":" << width << ",\"height\":" << height
|
||||
<< ",\"vsync\":" << (vsync ? 1 : 0) << ",\"backend\":\"" << backend
|
||||
<< "\",\"p50Ms\":" << p50 << ",\"p95Ms\":" << p95 << ",\"avgMs\":" << avgMs
|
||||
<< ",\"avgFps\":" << avgFps << ",\"gpuSamples\":" << stats.gpuMs.size()
|
||||
<< ",\"gpuP50Ms\":" << gpuP50 << ",\"gpuP95Ms\":" << gpuP95 << "}\n";
|
||||
}
|
||||
|
||||
void usage() {
|
||||
std::cerr
|
||||
<< "Usage: render_benchmark [workload] [frames] [items] [width] [height]\n"
|
||||
" workload: all|clear|2d-rects|2d-textured|2d-sprites|3d-cubes|3d-lit|3d-shadows"
|
||||
#if defined(EVENGINE_BENCH_PARTICLES)
|
||||
"|gpu-particles"
|
||||
#endif
|
||||
"\n"
|
||||
" EVENGINE_PERF_FRAMES overrides [frames] (default 60).\n"
|
||||
" Build with -DEVENGINE_BUILD_RENDER_BENCHMARK=ON.\n";
|
||||
}
|
||||
|
||||
std::size_t defaultItems(std::string_view workload, std::size_t requested) {
|
||||
if (requested > 0) return requested;
|
||||
if (workload.rfind("3d-", 0) == 0) return 1024;
|
||||
if (workload == "gpu-particles") return 16384;
|
||||
if (workload == "clear") return 0;
|
||||
return 10000;
|
||||
}
|
||||
|
||||
int runWorkload(Graphics *gfx, std::string_view workload, int frames, std::size_t requestedItems) {
|
||||
hideScene();
|
||||
gfx->setVSync(false);
|
||||
gfx->setScreenReadbackEnabled(false);
|
||||
gfx->setBackgroundColor(Color(0.08f, 0.09f, 0.12f, 1.f));
|
||||
const std::size_t items = defaultItems(workload, requestedItems);
|
||||
const int warmup = 20;
|
||||
Texture *white = makeSolid(gfx, 230, 230, 240);
|
||||
Texture *tint = makeSolid(gfx, 180, 90, 40);
|
||||
Mesh *cube = gfx->newMeshCube(1.f);
|
||||
|
||||
FrameStats stats;
|
||||
if (workload == "clear") {
|
||||
stats = measureFrames(warmup, frames, [&] {
|
||||
gfx->clearScreen();
|
||||
gfx->present();
|
||||
});
|
||||
} else if (workload == "2d-rects") {
|
||||
stats = measureFrames(warmup, frames, [&] {
|
||||
gfx->clearScreen();
|
||||
const int side = gridSide(items);
|
||||
const float cell = 12.f;
|
||||
for (std::size_t i = 0; i < items; ++i) {
|
||||
const float x = float(i % std::size_t(side)) * cell;
|
||||
const float y = float(i / std::size_t(side)) * cell;
|
||||
gfx->drawSolidRect(x, y, 10.f, 10.f, 0.95f, 0.45f, 0.2f, 1.f);
|
||||
}
|
||||
gfx->present();
|
||||
});
|
||||
} else if (workload == "2d-textured") {
|
||||
stats = measureFrames(warmup, frames, [&] {
|
||||
gfx->clearScreen();
|
||||
const int side = gridSide(items);
|
||||
const float cell = 12.f;
|
||||
for (std::size_t i = 0; i < items; ++i) {
|
||||
const float x = float(i % std::size_t(side)) * cell;
|
||||
const float y = float(i / std::size_t(side)) * cell;
|
||||
gfx->drawTexturedRect(white, x, y, 10.f, 10.f, 1.f, 1.f, 1.f, 1.f);
|
||||
}
|
||||
gfx->present();
|
||||
});
|
||||
} else if (workload == "2d-sprites") {
|
||||
spawnSprites(gfx, white, items);
|
||||
stats = measureFrames(warmup, frames, [&] { RenderSystem::render(*gfx); });
|
||||
} else if (workload == "3d-cubes" || workload == "3d-lit" || workload == "3d-shadows") {
|
||||
spawnCubes(cube, tint, items, workload != "3d-cubes", workload == "3d-shadows");
|
||||
stats = measureFrames(warmup, frames, [&] {
|
||||
RenderSystem3D::render(*gfx);
|
||||
RenderSystem::render(*gfx);
|
||||
});
|
||||
}
|
||||
#if defined(EVENGINE_BENCH_PARTICLES)
|
||||
else if (workload == "gpu-particles") {
|
||||
if (!gfx->supportsGpuParticles()) {
|
||||
std::cerr << "gpu-particles skipped: backend has no resident GPU particles\n";
|
||||
return 0;
|
||||
}
|
||||
auto *particles = eve::particles::Particles::create();
|
||||
auto *emitter = particles->newEmitter(int(std::max<std::size_t>(items, 1)));
|
||||
emitter->setTexture(white);
|
||||
emitter->setGpuSimulation(true);
|
||||
emitter->setPosition(float(gfx->getWidth()) * 0.5f, float(gfx->getHeight()) * 0.5f);
|
||||
emitter->setEmissionRate(0.f);
|
||||
emitter->setParticleLifetime(8.f, 8.f);
|
||||
emitter->setParticleSize(6.f, 10.f);
|
||||
emitter->setSpeed(40.f, 120.f);
|
||||
emitter->setSpread(6.2831853f);
|
||||
emitter->setBlendMode("additive");
|
||||
emitter->emit(int(items));
|
||||
stats = measureFrames(warmup, frames, [&] {
|
||||
eve::particles::ParticleSimSystem::update(1.f / 60.f);
|
||||
gfx->clearScreen();
|
||||
eve::particles::ParticleRenderSystem::render(gfx);
|
||||
gfx->present();
|
||||
});
|
||||
}
|
||||
#endif
|
||||
else {
|
||||
usage();
|
||||
return 2;
|
||||
}
|
||||
|
||||
printJson(workload, items, frames, gfx->getWidth(), gfx->getHeight(), gfx->isVSync(),
|
||||
gfx->getBackendName(), stats);
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
std::setvbuf(stdout, nullptr, _IONBF, 0);
|
||||
#if defined(EVENGINE_WINDOWS) || defined(_WIN32)
|
||||
eve::installCrashHandler();
|
||||
#endif
|
||||
const std::string_view workload = argc > 1 ? std::string_view(argv[1]) : "all";
|
||||
if (workload == "-h" || workload == "--help") {
|
||||
usage();
|
||||
return 0;
|
||||
}
|
||||
const int frames = parseEnvFrames(int(parsePositive(argc > 2 ? argv[2] : nullptr, 60)));
|
||||
const std::size_t items = parsePositive(argc > 3 ? argv[3] : nullptr, 0);
|
||||
const int width = int(parsePositive(argc > 4 ? argv[4] : nullptr, 1280));
|
||||
const int height = int(parsePositive(argc > 5 ? argv[5] : nullptr, 720));
|
||||
|
||||
try {
|
||||
auto *win = eve::window::Window::create();
|
||||
auto *gfx = Graphics::create();
|
||||
if (!win || !gfx) {
|
||||
std::cerr << "failed to create Window/Graphics modules\n";
|
||||
return 1;
|
||||
}
|
||||
eve::window::WindowSettings settings;
|
||||
settings.width = static_cast<std::uint16_t>(width);
|
||||
settings.height = static_cast<std::uint16_t>(height);
|
||||
settings.centered = true;
|
||||
settings.vsync = 0;
|
||||
settings.resizable = false;
|
||||
if (!win->setWindowSettings(settings)) {
|
||||
std::cerr << "failed to open benchmark window\n";
|
||||
return 1;
|
||||
}
|
||||
win->setWindowTitle("EVEngine render benchmark");
|
||||
gfx->setVSync(false);
|
||||
|
||||
int status = 0;
|
||||
if (workload == "all") {
|
||||
const char *names[] = {"clear", "2d-rects", "2d-textured", "2d-sprites",
|
||||
"3d-cubes", "3d-lit", "3d-shadows",
|
||||
#if defined(EVENGINE_BENCH_PARTICLES)
|
||||
"gpu-particles"
|
||||
#endif
|
||||
};
|
||||
for (const char *name : names) {
|
||||
const int rc = runWorkload(gfx, name, frames, items);
|
||||
if (rc != 0) status = rc;
|
||||
}
|
||||
} else {
|
||||
status = runWorkload(gfx, workload, frames, items);
|
||||
}
|
||||
win->close();
|
||||
return status;
|
||||
} catch (const eve::Exception &ex) {
|
||||
std::cerr << "render benchmark failed: " << ex.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user