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
@@ -0,0 +1,105 @@
# 0049c — `graph_renderer` Tier 1: RGBA8, orphan buffers, frustum cull, auto-pause
## Metadata
| Campo | Valor |
|-------|-------|
| **ID** | 0049c |
| **Estado** | pendiente |
| **Prioridad** | alta |
| **Tipo** | mejora rendimiento — parte de [#0049](0049-osint-graph-viewer.md) |
## Dependencias
**Bloqueada por:** [0049b](0049b-cpp-bump-gl-43.md) (recomendado pero no estricto — cambios funcionan en 3.3 tambien).
**Desbloquea:** [0049d](0049d-graph-edges-vertex-pulling.md), demos perf-realistas para issues posteriores.
---
## Objetivo
Optimizaciones baratas y de gran impacto sobre `graph_renderer.cpp` y `graph_force_layout` para subir de ~5k nodos a ~20k nodos a 60fps en GPU integrada **sin cambiar la API publica**.
## Contexto
Hoy el renderer:
- Empaqueta colores como 4 floats × N (16 bytes/nodo) en el instance buffer.
- Llama `glBufferData` cada frame → driver realloca el VBO.
- Sube todas las aristas siempre, aunque esten fuera del viewport.
- Force layout corre cada frame aunque la energia sea minima (estado convergido).
## Arquitectura
```
cpp/functions/viz/
├── graph_renderer.{h,cpp} # MOD
├── graph_renderer.md # MOD: bump version (1.x)
├── graph_force_layout.{h,cpp} # MOD: helper auto_pause
└── graph_force_layout.md # MOD
```
Sin cambios en la API publica — son optimizaciones internas.
## Tareas
### Fase 1 — Color packing RGBA8
- [ ] **1.1** En el instance buffer, cambiar layout de `(x, y, size, r, g, b, a)` floats a `(x, y, size, color_rgba8)` donde `color_rgba8` es uint32 packed.
- [ ] **1.2** Ajustar shader vertex de nodos: `layout(location=3) in uint a_color; vec4 col = unpackUnorm4x8(a_color);`.
- [ ] **1.3** Ajustar el packing en CPU: helper `pack_rgba8(r,g,b,a) = (a<<24)|(b<<16)|(g<<8)|r`.
- [ ] **1.4** Idem para el buffer de aristas (color por vertex → uint32 por vertex).
### Fase 2 — Orphan buffer pattern
- [ ] **2.1** Reemplazar `glBufferData(GL_ARRAY_BUFFER, sz, data, GL_DYNAMIC_DRAW)` por:
```cpp
glBufferData(GL_ARRAY_BUFFER, capacity_bytes, nullptr, GL_STREAM_DRAW); // orphan
glBufferSubData(GL_ARRAY_BUFFER, 0, used_bytes, data);
```
- [ ] **2.2** Mantener `capacity_bytes` interno en el `GraphRenderer` y crecer al doble si `used_bytes > capacity`.
### Fase 3 — Frustum cull aristas
- [ ] **3.1** Calcular AABB visible en world coords:
```cpp
float wx0 = cam_x - (width/2)/zoom; float wx1 = cam_x + (width/2)/zoom;
float wy0 = cam_y - (height/2)/zoom; float wy1 = cam_y + (height/2)/zoom;
```
- [ ] **3.2** En el bucle de aristas, skip si AABB de la arista (segmento source→target con margen) no intersecta el viewport AABB.
- [ ] **3.3** Nodos: similar — skip nodos cuyo AABB (centro ± size) cae fuera. Como son draws instanced, el cull se hace empaquetando solo los visibles en el instance buffer (mantener un counter `visible_count`).
### Fase 4 — Auto-pause force layout
- [ ] **4.1** En `graph_force_layout.h`, anadir helper:
```cpp
// Devuelve true si la energia ha caido bajo el umbral durante N frames consecutivos.
bool graph_force_layout_should_pause(float energy, float threshold, int min_consecutive);
```
- [ ] **4.2** Documentar uso en el `.md`. El consumer guarda un contador interno; el helper es puro.
- [ ] **4.3** Migrar `demos_graph.cpp` para usarlo y para no invocar `_step` cuando `paused == true`. Boton "Resume" ya existe.
### Fase 5 — Tests + benchmark
- [ ] **5.1** Test Catch2 sobre `pack_rgba8`/`unpack_rgba8`: roundtrip exacto.
- [ ] **5.2** Test Catch2 sobre `graph_force_layout_should_pause`: secuencias artificiales.
- [ ] **5.3** Benchmark manual en `demos_graph` con N=20000: anotar fps antes/despues en el .md de la funcion (`notes:`).
### Fase 6 — Cleanup
- [ ] Bump version del .md de `graph_renderer` a 1.1.0 y de `graph_force_layout` a 1.1.0.
- [ ] `fn index` y verificar.
- [ ] Commit `perf(viz): graph_renderer Tier 1 (RGBA8, orphan, cull) + force_layout auto-pause`.
## Criterio de done
- [ ] `demos_graph` con 20k nodos a 60fps en GPU integrada de pruebas.
- [ ] Tests verdes.
- [ ] `nvidia-smi` o `radeontop` muestran que la CPU baja respecto al baseline (perfilar con Tracy si TRACY_ENABLE).
## Riesgos
| Riesgo | Mitigacion |
|---|---|
| `unpackUnorm4x8` no esta en GL 3.3 sin extension | Esta en core 4.0+; con bump 0049b ya disponible. Si 0049b no se mergea antes, fallback a `(color>>0)&0xff)/255.0` manual |
| Frustum cull provoca pop-in en bordes | Anadir margen del 10% del viewport AABB |
| Crecimiento de capacity buffer en streaming | Crecer al doble; documentar capacity inicial razonable (4096 nodos) |