Files
fn_registry/cpp/apps/primitives_gallery/demos_graph.cpp
T
egutierrez 9a4ff33e68 perf(graph): quick wins — OpenMP force step + buffer orphan + auto-pause
Tres atajos de rendimiento sin GPU compute (eso llega en 0049h). Probados
en Linux y cross-compile Windows, todos los tests pasan, OpenMP 4.5
detectado.

1. **OpenMP en graph_force_layout_step** (cpp/functions/viz/...)
   - find_package(OpenMP) en cpp/CMakeLists.txt; fn_framework lo enlaza
     PUBLIC para que cualquier app/funcion lo herede transparentemente.
     Si no esta disponible, los pragmas se ignoran (single-thread).
   - #pragma omp parallel for con guard if(N>=1024) en los 4 bucles
     embarazosamente paralelos: zero forces, repulsion Barnes-Hut (con
     schedule dynamic), gravity, integration (con reduction sobre energy).
     La attraction-along-edges se queda secuencial: edges multiples
     escriben en el mismo nodo y meterle atomic mata el speedup.
   - quad_force usaba un static int stack[1<<20] (4MB compartidos entre
     threads — race). Lo reemplazo por int stack[256] en pila: el
     quadtree crece como log4(N) ~= 10 niveles para N <= 1M, asi que 256
     es holgado y thread-safe sin coste.
   - Esperable: ~4-8x menos tiempo CPU/step en 20k nodos en CPU multicore.

2. **Buffer orphan en graph_renderer** (edges + nodes)
   - Antes del glBufferData(.., data, DYNAMIC_DRAW), un primer
     glBufferData(.., NULL, DYNAMIC_DRAW) que descarta el buffer previo.
     El driver da uno fresco sin esperar al frame anterior — evita los
     sync stalls clasicos del DYNAMIC_DRAW reuploadeado cada frame.
   - Esperable: 2-3x throughput de upload (Mesa/NVIDIA/AMD respetan el
     hint).

3. **Auto-pause en demo_graph cuando converge**
   - Si energy_per_node < 0.001 durante 30 frames consecutivos, paramos
     la simulacion automaticamente. CPU/GPU a 0% cuando el grafo ya
     esta estable. Resume con "Resume layout" o "Regenerate".

Lo de OpenMP se sustituye cuando entre 0049h (force layout en compute
shader): cuando llegue, los #pragma omp se borran. Orphan y auto-pause
son keepers definitivos.
2026-04-29 21:38:13 +02:00

226 lines
8.4 KiB
C++

#include "demos.h"
#include "demo.h"
#include "viz/graph_types.h"
#include "viz/graph_viewport.h"
#include "viz/graph_force_layout.h"
#include "core/button.h"
#include "core/tokens.h"
#include <imgui.h>
#include <cmath>
#include <cstdio>
#include <vector>
namespace gallery {
// Genera un grafo sintetico con N nodos en K clusters + aristas intra-cluster
// y unas pocas inter-cluster. Pensado para demostrar el rendimiento del
// pipeline graph_renderer + graph_force_layout + graph_viewport.
static void generate_synthetic_graph(int N, int K,
std::vector<GraphNode>& nodes_out,
std::vector<GraphEdge>& edges_out) {
nodes_out.clear();
edges_out.clear();
nodes_out.reserve(N);
edges_out.reserve(N * 3);
unsigned seed = 0x1234abcd;
auto rnd = [&]() {
seed = seed * 1664525u + 1013904223u;
return static_cast<float>((seed >> 8) & 0xffffff) / 16777216.0f;
};
// Paleta por cluster (ABGR)
const uint32_t palette[] = {
0xff5b8def, 0xff58ca8c, 0xfff5973e, 0xffd95150,
0xffb87fe0, 0xff5fcdcc, 0xfff2cd52, 0xff99d161,
};
const int palette_n = sizeof(palette) / sizeof(palette[0]);
// Asignar cluster + posicion inicial cerca del centroide del cluster
std::vector<float> cluster_cx(K), cluster_cy(K);
for (int k = 0; k < K; k++) {
float angle = 2.0f * 3.14159f * k / K;
cluster_cx[k] = std::cos(angle) * 200.0f;
cluster_cy[k] = std::sin(angle) * 200.0f;
}
for (int i = 0; i < N; i++) {
int k = i % K;
GraphNode n = graph_node(static_cast<uint32_t>(i),
cluster_cx[k] + (rnd() - 0.5f) * 80.0f,
cluster_cy[k] + (rnd() - 0.5f) * 80.0f);
n.size = 3.0f + rnd() * 2.0f;
n.color = palette[k % palette_n];
n.community = static_cast<uint32_t>(k);
nodes_out.push_back(n);
}
// Aristas: ~3 por nodo dentro del cluster, +5% inter-cluster.
auto add_edge = [&](uint32_t a, uint32_t b, float w) {
if (a == b) return;
edges_out.push_back(graph_edge(a, b, w));
};
int per_cluster = N / K;
for (int k = 0; k < K; k++) {
int base = k * per_cluster;
int end = (k == K - 1) ? N : (base + per_cluster);
int size = end - base;
if (size < 2) continue;
// Dentro del cluster
for (int i = base; i < end; i++) {
for (int e = 0; e < 3; e++) {
int j = base + static_cast<int>(rnd() * size);
add_edge(static_cast<uint32_t>(i),
static_cast<uint32_t>(j), 1.0f);
}
}
}
// Inter-cluster (5% de los nodos)
int inter = N / 20;
for (int e = 0; e < inter; e++) {
uint32_t a = static_cast<uint32_t>(rnd() * N);
uint32_t b = static_cast<uint32_t>(rnd() * N);
add_edge(a, b, 0.3f);
}
}
void demo_graph() {
demo_header("graph_viewport", "v1.0.0",
"Pipeline completo de visualizacion de grafos: graph_renderer (instanced GPU) "
"+ graph_force_layout (Barnes-Hut) + graph_spatial_hash (hit-testing). "
"Render a FBO mostrado via ImGui::Image — escala a decenas de miles de nodos.");
static int s_n_nodes = 1000;
static int s_n_clusters = 6;
static float s_repulsion = 3500.0f; // fuerza de dispersion entre nodos
static float s_attraction = 0.02f; // muelle entre nodos conectados
static float s_gravity = 0.001f; // tiron hacia el centro
static std::vector<GraphNode> s_nodes;
static std::vector<GraphEdge> s_edges;
static GraphData s_graph{};
static GraphViewportState s_state;
static bool s_initialized = false;
static bool s_needs_regen = true;
if (s_needs_regen) {
generate_synthetic_graph(s_n_nodes, s_n_clusters, s_nodes, s_edges);
s_graph.nodes = s_nodes.data();
s_graph.node_count = static_cast<int>(s_nodes.size());
s_graph.edges = s_edges.data();
s_graph.edge_count = static_cast<int>(s_edges.size());
s_graph.update_bounds();
s_state.layout_running = true;
s_state.layout_energy = 0.0f;
s_needs_regen = false;
s_initialized = true;
}
section("Controls");
{
using namespace fn_ui;
// Sliders en dos filas para que quepan sin scrollbar
ImGui::PushItemWidth(180);
ImGui::SliderInt("Nodes", &s_n_nodes, 100, 20000);
ImGui::SameLine();
ImGui::SliderInt("Clusters", &s_n_clusters, 2, 16);
ImGui::SliderFloat("Repulsion", &s_repulsion, 100.0f, 20000.0f, "%.0f");
ImGui::SameLine();
ImGui::SliderFloat("Attraction", &s_attraction, 0.001f, 0.5f, "%.3f");
ImGui::SameLine();
ImGui::SliderFloat("Gravity", &s_gravity, 0.0f, 0.05f, "%.4f");
ImGui::PopItemWidth();
if (button("Regenerate", ButtonVariant::Primary)) s_needs_regen = true;
ImGui::SameLine();
if (button(s_state.layout_running ? "Pause layout" : "Resume layout",
ButtonVariant::Secondary)) {
s_state.layout_running = !s_state.layout_running;
}
ImGui::SameLine();
if (button("Fit view", ButtonVariant::Subtle)) {
graph_viewport_fit(s_graph, s_state);
}
}
section("Stats");
{
// Una sola linea fija — sin secciones condicionales que cambien la
// altura del panel (eso provocaba que el viewport saltara al hacer
// hover/select).
char hover_buf[32];
char sel_buf[32];
if (s_state.hovered_node >= 0) {
std::snprintf(hover_buf, sizeof(hover_buf), "#%d c%u",
s_state.hovered_node,
s_nodes[s_state.hovered_node].community);
} else {
std::snprintf(hover_buf, sizeof(hover_buf), "-");
}
if (s_state.selected_node >= 0) {
std::snprintf(sel_buf, sizeof(sel_buf), "#%d", s_state.selected_node);
} else {
std::snprintf(sel_buf, sizeof(sel_buf), "-");
}
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
ImGui::Text("nodes=%d edges=%d energy=%.2f fps=%.0f | hover=%s sel=%s",
s_graph.node_count, s_graph.edge_count,
s_state.layout_energy, ImGui::GetIO().Framerate,
hover_buf, sel_buf);
ImGui::PopStyleColor();
}
section("Viewport (drag = pan, wheel = zoom, click = select)");
if (s_initialized) {
// Avanzamos 1 paso de force layout cada frame mientras layout_running.
// Auto-pause: si la energia por nodo cae bajo el umbral durante N
// frames consecutivos, paramos la simulacion automaticamente — el
// grafo ya esta estable. El usuario lo retoma con "Resume layout"
// o "Regenerate".
static int s_low_energy_frames = 0;
const int k_pause_after_frames = 30;
const float k_pause_per_node = 0.001f; // umbral de energia/nodo
if (s_state.layout_running) {
ForceLayoutConfig cfg;
cfg.repulsion = s_repulsion;
cfg.attraction = s_attraction;
cfg.gravity = s_gravity;
cfg.iterations = 1;
s_state.layout_energy = graph_force_layout_step(s_graph, cfg);
const float per_node = s_graph.node_count > 0
? s_state.layout_energy / (float)s_graph.node_count
: 0.0f;
if (per_node < k_pause_per_node) {
if (++s_low_energy_frames >= k_pause_after_frames) {
s_state.layout_running = false;
s_low_energy_frames = 0;
}
} else {
s_low_energy_frames = 0;
}
} else {
s_low_energy_frames = 0;
}
graph_viewport("##graph_demo", s_graph, s_state, ImVec2(0, 460));
}
code_block(
"static GraphData graph;\n"
"static GraphViewportState state;\n"
"// ... rellenar graph.nodes / graph.edges ...\n"
"graph.update_bounds();\n"
"\n"
"// Por frame:\n"
"if (state.layout_running) {\n"
" ForceLayoutConfig cfg;\n"
" cfg.repulsion = 3500; cfg.gravity = 0.001f;\n"
" graph_force_layout_step(graph, cfg);\n"
"}\n"
"graph_viewport(\"##g\", graph, state, ImVec2(0, 460));"
);
}
} // namespace gallery