chore: sync from fn-registry agent

This commit is contained in:
fn-registry agent
2026-05-11 16:28:44 +02:00
commit f1e2c1cd19
25 changed files with 3973 additions and 0 deletions
+173
View File
@@ -0,0 +1,173 @@
// 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;
}
// Capture mode usa GL 3.3 deliberadamente: WSL Mesa no entrega contexto
// 4.3 offscreen (GLXBadFBConfig). Las pruebas visuales no necesitan
// compute/SSBO — ImGui+ImPlot funciona en 3.3 core. La build interactiva
// (app_base.cpp) si pide 4.3.
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