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)
This commit is contained in:
2026-04-29 22:17:13 +02:00
parent 0e6a013937
commit 02b4141cc1
12 changed files with 437 additions and 146 deletions
+30 -9
View File
@@ -3,7 +3,7 @@ name: graph_force_layout
kind: function
lang: cpp
domain: viz
version: "1.0.0"
version: "1.1.0"
purity: pure
signature: "float graph_force_layout_step(GraphData& graph, const ForceLayoutConfig& config)"
description: "Layout force-directed con aproximacion Barnes-Hut para grafos grandes, ejecuta un paso de simulacion por llamada"
@@ -14,9 +14,9 @@ returns: []
returns_optional: false
error_type: ""
imports: []
tested: false
tests: []
test_file_path: ""
tested: true
tests: ["should_pause threshold", "should_pause requires consecutive frames", "should_pause emulating low->high->low sequence"]
test_file_path: "cpp/tests/test_graph_should_pause.cpp"
file_path: "cpp/functions/viz/graph_force_layout.cpp"
framework: imgui
params:
@@ -54,6 +54,10 @@ 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)
@@ -61,20 +65,37 @@ graph_layout_grid(graph, 25.0f);
```cpp
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);
if (energy < 0.01f) running = false; // convergido
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 estatico de `1 << 20` (~1M) celdas. Para grafos de >500K nodos
se recomienda reducir `MAX_QUAD_NODES` o aumentarlo segun memoria disponible.
- La pila de traversal en `quad_force` es tambien estatica (`static int stack[]`); no es
thread-safe si se llama desde multiples hilos simultaneamente.
- 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`.