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:
@@ -1,5 +1,6 @@
|
|||||||
add_imgui_app(primitives_gallery
|
add_imgui_app(primitives_gallery
|
||||||
main.cpp
|
main.cpp
|
||||||
|
capture.cpp
|
||||||
demo.cpp
|
demo.cpp
|
||||||
demos_core.cpp
|
demos_core.cpp
|
||||||
demos_viz.cpp
|
demos_viz.cpp
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
// Implementacion de gallery::run_capture — render offscreen + glReadPixels +
|
||||||
|
// PNG via stb_image_write. Ver capture.h.
|
||||||
|
|
||||||
|
#include "capture.h"
|
||||||
|
|
||||||
|
#include "imgui.h"
|
||||||
|
#include "imgui_impl_glfw.h"
|
||||||
|
#include "imgui_impl_opengl3.h"
|
||||||
|
#include "implot.h"
|
||||||
|
#include "implot3d.h"
|
||||||
|
#include "core/tokens.h"
|
||||||
|
#include "core/icon_font.h"
|
||||||
|
#include "core/app_settings.h"
|
||||||
|
#include "gfx/gl_loader.h"
|
||||||
|
|
||||||
|
#include <GLFW/glfw3.h>
|
||||||
|
|
||||||
|
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||||
|
#include "stb_image_write.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace gallery {
|
||||||
|
|
||||||
|
static void glfw_capture_error(int error, const char* description) {
|
||||||
|
std::fprintf(stderr, "GLFW Error %d: %s\n", error, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flip vertical in-place: OpenGL origin = bottom-left, PNG = top-left.
|
||||||
|
static void flip_vertical_rgba(unsigned char* px, int w, int h) {
|
||||||
|
const int stride = w * 4;
|
||||||
|
std::vector<unsigned char> row(stride);
|
||||||
|
for (int y = 0; y < h / 2; ++y) {
|
||||||
|
unsigned char* a = px + y * stride;
|
||||||
|
unsigned char* b = px + (h - 1 - y) * stride;
|
||||||
|
std::copy(a, a + stride, row.begin());
|
||||||
|
std::copy(b, b + stride, a);
|
||||||
|
std::copy(row.begin(), row.end(), b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool run_capture(const CaptureConfig& cfg, const std::vector<CaptureItem>& items) {
|
||||||
|
glfwSetErrorCallback(&glfw_capture_error);
|
||||||
|
if (!glfwInit()) {
|
||||||
|
std::fprintf(stderr, "capture: glfwInit failed\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
|
||||||
|
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
|
||||||
|
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
|
||||||
|
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
|
||||||
|
#ifdef __APPLE__
|
||||||
|
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
GLFWwindow* window = glfwCreateWindow(
|
||||||
|
cfg.capture_w, cfg.capture_h, "capture", nullptr, nullptr);
|
||||||
|
if (!window) {
|
||||||
|
std::fprintf(stderr, "capture: glfwCreateWindow failed (no GL?)\n");
|
||||||
|
glfwTerminate();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
glfwMakeContextCurrent(window);
|
||||||
|
glfwSwapInterval(0);
|
||||||
|
|
||||||
|
if (!fn::gfx::gl_loader_init()) {
|
||||||
|
std::fprintf(stderr, "capture: gl_loader_init failed\n");
|
||||||
|
glfwDestroyWindow(window);
|
||||||
|
glfwTerminate();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
IMGUI_CHECKVERSION();
|
||||||
|
ImGui::CreateContext();
|
||||||
|
ImPlot::CreateContext();
|
||||||
|
ImPlot3D::CreateContext();
|
||||||
|
|
||||||
|
ImGuiIO& io = ImGui::GetIO();
|
||||||
|
io.IniFilename = nullptr; // no .ini side effects in capture mode.
|
||||||
|
io.DisplaySize = ImVec2((float)cfg.capture_w, (float)cfg.capture_h);
|
||||||
|
|
||||||
|
fn_ui::settings_load();
|
||||||
|
fn_ui::load_fonts_from_settings();
|
||||||
|
{
|
||||||
|
ImGuiStyle& style = ImGui::GetStyle();
|
||||||
|
style.FontSizeBase = fn_ui::settings().font_size_px;
|
||||||
|
style._NextFrameFontSizeBase = style.FontSizeBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn_tokens::apply_dark_theme();
|
||||||
|
|
||||||
|
ImGui_ImplGlfw_InitForOpenGL(window, false);
|
||||||
|
ImGui_ImplOpenGL3_Init("#version 330");
|
||||||
|
|
||||||
|
bool ok_all = true;
|
||||||
|
std::vector<unsigned char> pixels((size_t)cfg.capture_w * cfg.capture_h * 4u);
|
||||||
|
|
||||||
|
for (const auto& item : items) {
|
||||||
|
// Warmup: rinde varios frames para que ImGui/ImPlot estabilicen layout
|
||||||
|
// (el primer frame frecuentemente carece de mediciones de tamaño).
|
||||||
|
for (int frame = 0; frame < cfg.warmup_frames + 1; ++frame) {
|
||||||
|
glfwPollEvents();
|
||||||
|
|
||||||
|
ImGui_ImplOpenGL3_NewFrame();
|
||||||
|
ImGui_ImplGlfw_NewFrame();
|
||||||
|
ImGui::NewFrame();
|
||||||
|
|
||||||
|
// Ventana fullscreen sobre el viewport con la demo activa,
|
||||||
|
// sin sidebar (queremos el render del primitivo lo mas limpio
|
||||||
|
// posible para el diff visual).
|
||||||
|
const ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||||
|
ImGui::SetNextWindowPos(vp->WorkPos);
|
||||||
|
ImGui::SetNextWindowSize(vp->WorkSize);
|
||||||
|
ImGui::Begin("##capture_root",
|
||||||
|
nullptr,
|
||||||
|
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
||||||
|
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse |
|
||||||
|
ImGuiWindowFlags_NoBringToFrontOnFocus |
|
||||||
|
ImGuiWindowFlags_NoSavedSettings);
|
||||||
|
if (item.fn) item.fn();
|
||||||
|
ImGui::End();
|
||||||
|
|
||||||
|
ImGui::Render();
|
||||||
|
int dw, dh;
|
||||||
|
glfwGetFramebufferSize(window, &dw, &dh);
|
||||||
|
glViewport(0, 0, dw, dh);
|
||||||
|
glClearColor(fn_tokens::colors::bg.x,
|
||||||
|
fn_tokens::colors::bg.y,
|
||||||
|
fn_tokens::colors::bg.z, 1.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
|
||||||
|
glfwSwapBuffers(window);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read framebuffer (GL_RGBA / GL_UNSIGNED_BYTE).
|
||||||
|
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||||
|
glReadPixels(0, 0, cfg.capture_w, cfg.capture_h,
|
||||||
|
GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||||
|
|
||||||
|
flip_vertical_rgba(pixels.data(), cfg.capture_w, cfg.capture_h);
|
||||||
|
|
||||||
|
char path[1024];
|
||||||
|
std::snprintf(path, sizeof(path), "%s/%s.png",
|
||||||
|
cfg.output_dir.c_str(), item.id.c_str());
|
||||||
|
const int rc = stbi_write_png(
|
||||||
|
path, cfg.capture_w, cfg.capture_h, 4,
|
||||||
|
pixels.data(), cfg.capture_w * 4);
|
||||||
|
if (rc == 0) {
|
||||||
|
std::fprintf(stderr, "capture: stbi_write_png failed for %s\n", path);
|
||||||
|
ok_all = false;
|
||||||
|
} else {
|
||||||
|
std::fprintf(stdout, "captured: %s\n", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui_ImplOpenGL3_Shutdown();
|
||||||
|
ImGui_ImplGlfw_Shutdown();
|
||||||
|
ImPlot3D::DestroyContext();
|
||||||
|
ImPlot::DestroyContext();
|
||||||
|
ImGui::DestroyContext();
|
||||||
|
glfwDestroyWindow(window);
|
||||||
|
glfwTerminate();
|
||||||
|
return ok_all;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace gallery
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#pragma once
|
||||||
|
// Capture mode: renderiza cada demo de la gallery en una ventana GLFW
|
||||||
|
// invisible y guarda un PNG en `output_dir/<demo_id>.png` via stb_image_write.
|
||||||
|
//
|
||||||
|
// Diseñado para CI / golden-image diffing: ver `cpp/scripts/update_goldens.sh`
|
||||||
|
// y `cpp/tests/test_visual.cpp`.
|
||||||
|
//
|
||||||
|
// Importante:
|
||||||
|
// - Requiere un contexto OpenGL real. En entornos sin GPU (containers minimos)
|
||||||
|
// funciona con `LIBGL_ALWAYS_SOFTWARE=1` (Mesa/llvmpipe) o swiftshader.
|
||||||
|
// - Si el entorno (WSL sin GL) no puede crear un contexto GL valido, el
|
||||||
|
// binario sale con codigo != 0 sin generar PNGs.
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace gallery {
|
||||||
|
|
||||||
|
struct CaptureItem {
|
||||||
|
std::string id;
|
||||||
|
void (*fn)();
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CaptureConfig {
|
||||||
|
std::string output_dir;
|
||||||
|
int warmup_frames = 3;
|
||||||
|
int capture_w = 800;
|
||||||
|
int capture_h = 600;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Devuelve true si todo el set se capturo OK.
|
||||||
|
bool run_capture(const CaptureConfig& cfg, const std::vector<CaptureItem>& items);
|
||||||
|
|
||||||
|
} // namespace gallery
|
||||||
@@ -17,10 +17,12 @@
|
|||||||
|
|
||||||
#include "demos.h"
|
#include "demos.h"
|
||||||
#include "demo.h"
|
#include "demo.h"
|
||||||
|
#include "capture.h"
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <sys/stat.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
struct DemoEntry {
|
struct DemoEntry {
|
||||||
@@ -161,7 +163,35 @@ static void render() {
|
|||||||
fn_ui::toast_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(
|
return fn::run_app(
|
||||||
{.title = "fn_registry · Primitives Gallery",
|
{.title = "fn_registry · Primitives Gallery",
|
||||||
.width = 1400,
|
.width = 1400,
|
||||||
|
|||||||
Vendored
+1724
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user