// 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 #define STB_IMAGE_WRITE_IMPLEMENTATION #include "stb_image_write.h" #include #include 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 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& 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 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