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.
This commit is contained in:
2026-04-29 21:38:13 +02:00
parent 96db47f083
commit 9a4ff33e68
4 changed files with 55 additions and 6 deletions
+12
View File
@@ -138,6 +138,18 @@ if(TRACY_ENABLE)
target_link_libraries(fn_framework PUBLIC tracy)
endif()
# --- OpenMP (opcional) ---
# Habilita #pragma omp en las funciones del registry que lo declaren bajo
# guardia _OPENMP. Si el compilador no lo soporta (no debiera, gcc/clang
# y mingw-w64 lo traen), los pragmas se ignoran sin romper el build.
find_package(OpenMP QUIET)
if(OpenMP_CXX_FOUND)
target_link_libraries(fn_framework PUBLIC OpenMP::OpenMP_CXX)
message(STATUS "OpenMP enabled for fn_framework (${OpenMP_CXX_VERSION})")
else()
message(STATUS "OpenMP NOT found — force layout fallback to single-thread")
endif()
# --- Macro for creating ImGui apps ---
# Capturamos la raiz del modulo cpp/ para que add_imgui_app la use desde
# subdirectorios (donde CMAKE_CURRENT_SOURCE_DIR apunta al app, no al root).
+22 -1
View File
@@ -173,7 +173,14 @@ void demo_graph() {
section("Viewport (drag = pan, wheel = zoom, click = select)");
if (s_initialized) {
// Avanzamos 1 paso de force layout cada frame mientras layout_running
// 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;
@@ -181,6 +188,20 @@ void demo_graph() {
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));
}
+12 -4
View File
@@ -146,8 +146,11 @@ static void quad_insert_body(int qi, int node_idx) {
static void quad_force(int qi, float nx, float ny,
float theta, float repulsion, float min_dist,
float& fx, float& fy) {
// Iterative traversal using a small stack to avoid recursion depth issues.
static int stack[MAX_QUAD_NODES]; // reuse static stack
// Stack en pila de la funcion: thread-safe (la version anterior con
// `static` se rompia bajo OpenMP). La profundidad de un quadtree con N
// bodies acotada por log4(N) ~= 10 niveles para N <= 1M, asi que 256
// entradas son holgadas para todos los pushes simultaneos.
int stack[256];
int top = 0;
stack[top++] = qi;
@@ -207,6 +210,7 @@ float graph_force_layout_step(GraphData& graph, const ForceLayoutConfig& config)
for (int iter = 0; iter < config.iterations; ++iter) {
// Zero forces
#pragma omp parallel for if(graph.node_count >= 1024) schedule(static)
for (int i = 0; i < graph.node_count; ++i) {
fx_buf[i] = 0.0f;
fy_buf[i] = 0.0f;
@@ -240,14 +244,16 @@ float graph_force_layout_step(GraphData& graph, const ForceLayoutConfig& config)
}
// ---- Repulsion via Barnes-Hut ----
// Cada iteracion lee del quadtree (read-only) y escribe en su propio
// slot de fx_buf/fy_buf — embarrassingly parallel. quad_force usa
// stack local en pila, asi que es thread-safe.
#pragma omp parallel for if(graph.node_count >= 1024) schedule(dynamic, 256)
for (int i = 0; i < graph.node_count; ++i) {
if (graph.nodes[i].pinned) continue;
quad_force(root,
graph.nodes[i].x, graph.nodes[i].y,
config.theta, config.repulsion, config.min_distance,
fx_buf[i], fy_buf[i]);
// Subtract self-interaction (the tree includes the node itself)
// Self-force: repulsion * 1 / min_dist^2, but direction is (0,0) -> skip
}
// ---- Attraction along edges (spring force) ----
@@ -274,6 +280,7 @@ float graph_force_layout_step(GraphData& graph, const ForceLayoutConfig& config)
// ---- Gravity toward center (0,0) ----
if (config.gravity != 0.0f) {
#pragma omp parallel for if(graph.node_count >= 1024) schedule(static)
for (int i = 0; i < graph.node_count; ++i) {
if (graph.nodes[i].pinned) continue;
fx_buf[i] -= config.gravity * graph.nodes[i].x;
@@ -283,6 +290,7 @@ float graph_force_layout_step(GraphData& graph, const ForceLayoutConfig& config)
// ---- Integrate: v = v * damping + F; pos += v ----
total_energy = 0.0f;
#pragma omp parallel for if(graph.node_count >= 1024) schedule(static) reduction(+:total_energy)
for (int i = 0; i < graph.node_count; ++i) {
GraphNode& n = graph.nodes[i];
if (n.pinned) continue;
+9 -1
View File
@@ -389,6 +389,11 @@ unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph,
glBindVertexArray(r->edge_vao);
glBindBuffer(GL_ARRAY_BUFFER, r->edge_vbo);
// Orphan: descarta el buffer anterior antes de subir el nuevo. Evita
// que el driver bloquee esperando que termine el frame previo (sync
// stall) y nos da un VBO fresco. Coste: ~0; ganancia: 2-3x upload
// throughput en drivers que respetan el hint (Mesa, NVIDIA, AMD).
glBufferData(GL_ARRAY_BUFFER, vi * (int)sizeof(float), nullptr, GL_DYNAMIC_DRAW);
glBufferData(GL_ARRAY_BUFFER, vi * (int)sizeof(float), edge_buf, GL_DYNAMIC_DRAW);
glDrawArrays(GL_LINES, 0, vi / 6);
glBindVertexArray(0);
@@ -422,7 +427,10 @@ unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph,
glBindVertexArray(r->node_vao);
glBindBuffer(GL_ARRAY_BUFFER, r->node_instance_vbo);
glBufferData(GL_ARRAY_BUFFER, graph.node_count * 7 * (int)sizeof(float), node_buf, GL_DYNAMIC_DRAW);
// Orphan + reupload (ver comentario en edge upload arriba).
const GLsizeiptr node_bytes = graph.node_count * 7 * (GLsizeiptr)sizeof(float);
glBufferData(GL_ARRAY_BUFFER, node_bytes, nullptr, GL_DYNAMIC_DRAW);
glBufferData(GL_ARRAY_BUFFER, node_bytes, node_buf, GL_DYNAMIC_DRAW);
// Draw 4 vertices (triangle strip quad) x node_count instances
// Pass per-instance node_px uniform via the average size (approximation)