feat(primitives_gallery): añadir --capture <dir> mode (offscreen render + glReadPixels)

Modo de captura que renderiza cada demo de la gallery en una ventana GLFW
invisible (GLFW_VISIBLE=GLFW_FALSE) y guarda PNG por demo via stb_image_write.

- capture.{h,cpp}: API gallery::run_capture(cfg, items) — warmup_frames,
  glReadPixels(GL_RGBA), flip vertical, stbi_write_png.
- main.cpp: parsea --capture <dir> antes de fn::run_app y delega a capture.cpp.
- vendor: stb_image_write.h v1.16 (mismo commit que stb_image.h).

Funciona en WSL con LIBGL_ALWAYS_SOFTWARE=1 (Mesa/llvmpipe). Si el entorno
no tiene contexto GL, el binario sale con rc!=0 sin generar PNGs.

Issue 0048.
This commit is contained in:
2026-04-29 00:18:27 +02:00
parent 98e134c935
commit 6be660fac6
5 changed files with 1959 additions and 1 deletions
+31 -1
View File
@@ -17,10 +17,12 @@
#include "demos.h"
#include "demo.h"
#include "capture.h"
#include <cstdio>
#include <cstring>
#include <string>
#include <sys/stat.h>
#include <vector>
struct DemoEntry {
@@ -161,7 +163,35 @@ static void render() {
fn_ui::toast_render();
}
int main(int /*argc*/, char** /*argv*/) {
int main(int argc, char** argv) {
// Capture mode: `primitives_gallery --capture <output_dir>` corre cada
// demo en una ventana GLFW invisible y guarda PNG por demo. Para CI/golden.
for (int i = 1; i < argc; i++) {
if (std::strcmp(argv[i], "--capture") == 0) {
if (i + 1 >= argc) {
std::fprintf(stderr, "--capture requires an output dir argument\n");
return 2;
}
const char* out_dir = argv[i + 1];
// Best-effort mkdir (idempotente).
mkdir(out_dir, 0755);
std::vector<gallery::CaptureItem> items;
items.reserve(k_demo_count);
for (int j = 0; j < k_demo_count; j++) {
items.push_back({k_demos[j].id, k_demos[j].fn});
}
gallery::CaptureConfig cfg;
cfg.output_dir = out_dir;
cfg.warmup_frames = 3;
cfg.capture_w = 800;
cfg.capture_h = 600;
const bool ok = gallery::run_capture(cfg, items);
return ok ? 0 : 1;
}
}
return fn::run_app(
{.title = "fn_registry · Primitives Gallery",
.width = 1400,