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 -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;