chore(issues): plan 0049 OSINT graph viewer multi-issue

Aggregates the planning artifacts for the 0049 series (umbrella + 0049a..0049k):

- New rule cpp_apps.md (registered in INDEX) — standardize structure, CMake
  patterns, app.md frontmatter and sub-repo for C++ apps; points to the
  authoritative cpp/PATTERNS.md and cpp/DESIGN_SYSTEM.md.
- Feature flag osint_graph_v1 (disabled until 0049k closes).
- Issue 0049 (umbrella) and sub-issues 0049b..0049k describing the GPU
  rendering system, force-layout, types, sources, labels and the final
  graph_explorer app integration.
- README updated with the new rows (all pending; 0049a will flip to
  completed in the next commit).
This commit is contained in:
2026-04-29 21:08:36 +02:00
parent 4cdd650502
commit f8f72e4bf7
15 changed files with 1742 additions and 0 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 |