perf(viz): graph_renderer edges via TBO + vertex pulling (issue 0049d)

El buffer de aristas pasa a estatico (16B/arista: source, target, color,
flags) y solo se reupload cuando cambia el grafo. Las posiciones de los
nodos viven en un Texture Buffer Object (RG32F) actualizado por frame; el
vertex shader hace texelFetch con gl_VertexID & 1 para elegir endpoint.
Draw call: glDrawArraysInstanced(GL_LINES, 0, 2, edge_count) con divisor=1.

Para 100k aristas: el upload de 4.8 MB/frame baja a 0 en regimen estable.
edge_alpha pasa a uniform; la pre-multiplicacion en CPU desaparece. GLSL
sigue en 330 core (samplerBuffer/texelFetch estan en 1.40+).

gl_loader gana glBufferSubData, glVertexAttribIPointer y glTexBuffer (en
Linux ya estaban via GL_GLEXT_PROTOTYPES; ahora estan disponibles tambien
en MinGW/Windows).

Tests: nuevo test_graph_edge_static valida el layout de 16B y el packing
RGBA8 del fallback. test_visual sigue verde — render visualmente identico.

Bump graph_renderer 1.2.0 -> 1.3.0.
This commit is contained in:
2026-04-29 22:32:38 +02:00
parent b156942cea
commit daf491cd99
8 changed files with 298 additions and 86 deletions
@@ -0,0 +1,104 @@
# 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 |