Files
fn_registry/cpp/functions/viz/graph_force_layout.md
T
egutierrez 02b4141cc1 perf(viz): graph_renderer Tier 1 (RGBA8 + orphan + frustum cull) + force_layout auto-pause helper
Issue 0049c. Tres optimizaciones internas en graph_renderer.cpp + un
helper puro en graph_force_layout para detectar convergencia. API publica
intacta — solo cambian el layout interno de los buffers, el shader y
los costes por frame.

1. RGBA8 color packing
   - El instance buffer de nodos pasa de (x,y,size,r,g,b,a) 28B a
     (x,y,size,color_u32) 16B (-43%). Aristas: 24B → 12B/vertex (-50%).
   - Shaders desempaquetan con bit shifts (compatible GL 3.30+, no
     necesita unpackUnorm4x8 que es 4.20+).
   - Helpers expuestos: pack_rgba8 / unpack_rgba8 / modulate_alpha_rgba8
     en graph_renderer.h. Los GraphNode.color y la paleta ya tenian el
     layout correcto (R en LSB), asi que CPU ahora pasa el uint32 directo
     sin convertir a 4 floats por nodo y por frame.

2. Capacity-tracked streaming buffers
   - Sustituye el doble glBufferData de antes por:
       glBufferData(NULL, capacity, STREAM_DRAW)   // orphan + reserva
       glBufferSubData(0, used_bytes, data)        // solo lo usado
   - capacity crece x2 cuando hace falta (inicial 4096 nodos /
     8192 vertices de aristas) → reallocaciones en O(log N).
   - Staging CPU (NodeInstance* / EdgeVertex*) reusado entre frames con
     realloc, no malloc/free per frame.

3. Frustum cull (CPU-side)
   - AABB del viewport en world coords con margen 10%.
   - Aristas: skip si AABB del segmento no intersecta el viewport.
   - Nodos: solo los visibles entran al instance buffer; visible_count
     es el N que pasa a glDrawArraysInstanced. Pop-in de borde mitigado
     por el margen.

4. graph_force_layout_should_pause(low_frames, min_consecutive)
   - Helper puro: el caller mantiene el contador, la funcion solo
     decide si parar. Reemplaza la rama inline en demos_graph.cpp.
   - Test Catch2 con secuencias artificiales.

Tests: test_graph_pack_rgba8 (16401 asserts, 4 cases — roundtrip exhaustivo
+ alpha modulation + clamp). test_graph_should_pause (3 cases, 14 asserts).
Los 29 tests del cpp/tests/ siguen verdes (incluido test_visual con goldens).

Bump versiones:
- graph_renderer 1.1.0 → 1.2.0
- graph_force_layout 1.0.0 → 1.1.0  (tested: true via should_pause test)
2026-04-29 22:17:13 +02:00

4.4 KiB

name, kind, lang, domain, version, purity, signature, description, tags, uses_functions, uses_types, returns, returns_optional, error_type, imports, tested, tests, test_file_path, file_path, framework, params, output, notes
name kind lang domain version purity signature description tags uses_functions uses_types returns returns_optional error_type imports tested tests test_file_path file_path framework params output notes
graph_force_layout function cpp viz 1.1.0 pure float graph_force_layout_step(GraphData& graph, const ForceLayoutConfig& config) Layout force-directed con aproximacion Barnes-Hut para grafos grandes, ejecuta un paso de simulacion por llamada
graph
layout
force-directed
barnes-hut
physics
gpu
GraphData_cpp_viz
false
true
should_pause threshold
should_pause requires consecutive frames
should_pause emulating low->high->low sequence
cpp/tests/test_graph_should_pause.cpp cpp/functions/viz/graph_force_layout.cpp imgui
name desc
graph Referencia al grafo (GraphData) cuyos nodos se actualizan in-place. Modifica x, y, vx, vy de cada nodo no pinned.
name desc
config Parametros de la simulacion: repulsion (fuerza coulombiana), attraction (spring constant), damping (decay de velocidad), theta (precision Barnes-Hut 0=exacto/1=rapido), gravity (atraccion al centro), max_velocity, iterations.
Energia cinetica total (suma de |v|^2). Cuando cae por debajo de un umbral elegido por el caller, el layout ha convergido y se puede dejar de llamar. scaffolding/demo en primitives_gallery

graph_force_layout

Implementa el algoritmo de layout force-directed clasico (Fruchterman-Reingold / Eades) con aproximacion Barnes-Hut O(n log n) para escalar a grafos de miles de nodos.

Algoritmo

Cada llamada a graph_force_layout_step ejecuta config.iterations pasos. Un paso:

  1. Construccion del quadtree (Barnes-Hut): se calcula el bounding box de las posiciones actuales, se construye un quadtree flat en quad_pool (sin allocaciones por nodo). Cada celda acumula centro de masa y masa total.
  2. Repulsion: para cada nodo se recorre el quadtree. Si el cociente cell_size / distance < theta, la celda se trata como una sola masa puntual (multipolo de orden 0). Si no, se desciende a los hijos. Con theta=0 es O(n²) exacto; con theta=0.5 es O(n log n).
  3. Atraccion: para cada arista (s, t), fuerza de Hooke F = k * dist * weight en la direccion del arco.
  4. Gravedad: fuerza proporcional a la distancia al origen, evita que el grafo derive fuera de pantalla.
  5. Integracion: v = v * damping + F, pos += v, con clamping de velocidad.
  6. Nodos con pinned = true no se mueven en ningun paso.

Funciones auxiliares

// Randomizar posiciones para empezar la simulacion
graph_force_layout_reset(graph, 200.0f);

// Layout circular instantaneo (sin iteracion)
graph_layout_circular(graph, 150.0f);

// Layout en grid instantaneo
graph_layout_grid(graph, 25.0f);

// Auto-pause: parar la simulacion cuando la energia se ha estabilizado.
// Pure: el caller mantiene el contador, la funcion solo decide.
//   bool graph_force_layout_should_pause(int low_frames, int min_consecutive);

Ejemplo de uso tipico (loop ImGui)

static ForceLayoutConfig cfg;
static bool running = true;
static int  low_frames = 0;
const int   k_min_consecutive = 30;
const float k_threshold_per_node = 0.001f;

if (running) {
    float energy = graph_force_layout_step(my_graph, cfg);
    float per_node = my_graph.node_count > 0
        ? energy / my_graph.node_count : 0.0f;
    if (per_node < k_threshold_per_node) ++low_frames;
    else                                 low_frames = 0;
    if (graph_force_layout_should_pause(low_frames, k_min_consecutive)) {
        running    = false;
        low_frames = 0;
    }
}

Notas de implementacion

  • El quadtree usa un pool dinamico (std::vector<QuadNode>) que se redimensiona una vez por step a 5*N + 1024 celdas. La pila de traversal en quad_force es local en pila (256 entradas) — thread-safe bajo OpenMP.
  • graph_force_layout_reset usa rand(). Para reproducibilidad llama srand(seed) antes.
  • Los buffers de fuerza (fx_buf, fy_buf) se realocan una sola vez cuando el conteo de nodos supera la capacidad previa; en el uso normal (tamano fijo) no hay allocaciones por frame.

Notas de version

  • v1.1 (2026-04-29, issue 0049c): añade el helper puro graph_force_layout_should_pause(low_frames, min_consecutive) para que las apps detecten convergencia sin replicar el contador por todas partes. Sin cambios en graph_force_layout_step ni en la API existente. Test: cpp/tests/test_graph_should_pause.cpp.