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:
西风逍遥游andCursor committed 2026-09-06 18:03:19 +08:00
1 parent 6443348d0e
commit 7b493d9f03
3 files changed
+440

No files matched your search

+5
View File
@@ -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()
+34
View File
@@ -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()
+401
View File
@@ -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;
}
}