# 0049d — Aristas via vertex pulling con TBO

## Metadata

| Campo | Valor |
|-------|-------|
| **ID** | 0049d |
| **Estado** | pendiente |
| **Prioridad** | alta |
| **Tipo** | mejora rendimiento — parte de [#0049](0049-osint-graph-viewer.md) |

## Dependencias

**Bloqueada por:** [0049c](0049c-graph-renderer-tier1.md) (orphan + RGBA8 ya en sitio).

---

## Objetivo

Eliminar la reconstruccion del buffer de aristas en CPU cada frame. Las posiciones de nodos viven en un Texture Buffer Object (TBO); el vertex shader de aristas hace fetch de source/target con `gl_VertexID`. El buffer de aristas es estatico (`source_idx`, `target_idx`, `type_id`), solo cambian las posiciones — y eso ya estaba para los nodos.

## Contexto

Tras 0049c, el bottleneck principal restante es el upload de 12 floats × E aristas cada frame. Para 100k aristas: 4.8 MB/frame. Vertex pulling lo elimina por completo.

## Arquitectura

```
cpp/functions/viz/
├── graph_renderer.{h,cpp}     # MOD: anadir TBO + buffer estatico de aristas
└── graph_renderer.md          # MOD: bump 1.1 → 1.2
```

Cambios:
1. `GraphRenderer` gana `unsigned int node_pos_tbo, node_pos_tex` (texture buffer y su sampler view).
2. `GraphRenderer` gana `unsigned int edge_static_vbo` con `(uint source, uint target, uint color, uint flags)` por arista — subido una vez (o cuando cambia el grafo, no cada frame).
3. Buffer de posiciones de nodos (`vec2[]`) se sube como TBO vinculado al `node_pos_tex`.
4. Vertex shader de aristas:
   ```glsl
   #version 430 core
   layout(location=0) in uint a_source;
   layout(location=1) in uint a_target;
   layout(location=2) in uint a_color;
   layout(location=3) in uint a_flags;

   uniform samplerBuffer u_node_pos;  // vec2[] como TBO

   out vec4 v_color;

   void main() {
       int idx = (gl_VertexID & 1) == 0 ? int(a_source) : int(a_target);
       vec2 p  = texelFetch(u_node_pos, idx).xy;
       // mismo MVP que ya estaba
       gl_Position = u_mvp * vec4(p, 0.0, 1.0);
       v_color     = unpackUnorm4x8(a_color);
   }
   ```
   Cada arista renderiza con `glDrawArrays(GL_LINES, 0, edge_count*2)`. El fragment shader ya existia.

## Tareas

### Fase 1 — TBO de posiciones

- [ ] **1.1** En `graph_renderer_create`: crear `node_pos_buf` (VBO) + `node_pos_tex` (texture buffer view) con `glTexBuffer(GL_TEXTURE_BUFFER, GL_RG32F, node_pos_buf)`.
- [ ] **1.2** En `graph_renderer_draw`: empaquetar posiciones de nodos en un buffer flotante `(x,y) × N` y `glBufferSubData` al `node_pos_buf` (orphan + sub).
- [ ] **1.3** Antes del draw de aristas: `glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_BUFFER, node_pos_tex); glUniform1i(u_node_pos_loc, 0);`.

### Fase 2 — Buffer estatico de aristas

- [ ] **2.1** Anadir API interna `mark_edges_dirty()` que regenera el `edge_static_vbo`.
- [ ] **2.2** Si el caller pasa `graph.edges_revision != cached_revision`, regenerar. Para el primer paso, usar siempre `cached==false → regenerar` y optimizar luego con un campo `revision` en `GraphData`.
- [ ] **2.3** Layout: `struct EdgeStatic { uint source; uint target; uint color_rgba8; uint flags; }` (16 bytes por arista).

### Fase 3 — Shaders de aristas

- [ ] **3.1** Reescribir vertex shader como en la arquitectura (4.3 core).
- [ ] **3.2** Verificar fragment shader no necesita cambios.
- [ ] **3.3** Reverificar `glLineWidth`/edge_alpha siguen funcionando.

### Fase 4 — Bench + tests

- [ ] **4.1** `demos_graph` con 20k nodos + 100k aristas a 60fps en GPU integrada.
- [ ] **4.2** Profile con Tracy: el bucle de aristas en CPU debe desaparecer.
- [ ] **4.3** Test Catch2 minimo: render a FBO + readback, verificar que un grafo conocido produce un frame no-vacio (smoke test, no golden).

### Fase 5 — Cleanup

- [ ] Bump version `graph_renderer` 1.1.0 → 1.2.0.
- [ ] `fn index`.
- [ ] Commit `perf(viz): graph_renderer edges via TBO + vertex pulling`.

## Criterio de done

- [ ] CPU ms del frame para 100k aristas baja a < 0.5 ms (medible con Tracy o reloj manual).
- [ ] Render visualmente identico al pre-cambio.
- [ ] Demos de la galeria afectados (`demos_graph`) sin regresiones.

## Riesgos

| Riesgo | Mitigacion |
|---|---|
| `samplerBuffer` no funciona en alguna driver Linux | GL 4.3 core lo exige; si falla en WSL software, marcar test como SKIP igual que `test_visual` |
| Mantener `edges_revision` complica la API | Empezar con regenerar siempre y optimizar despues — no premature optimization |
| 4 bytes desperdiciados por arista (`flags`) | Justificado por alineacion + futuras flechas/styles |
