Files
fn_registry/cpp/functions/viz/graph_renderer.cpp
T
egutierrez daf491cd99 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.
2026-04-29 22:32:38 +02:00

672 lines
27 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "viz/graph_renderer.h"
#include "viz/graph_types.h"
// gl_loader: en Linux es no-op (incluye GL headers con GL_GLEXT_PROTOTYPES);
// en Windows expone los punteros via #define gl* fn_gl* tras gl_loader_init().
#include "gfx/gl_loader.h"
#include <cstdlib>
#include <cstring>
#include <cstdio>
#include <cstddef>
#include <cmath>
#include <algorithm>
// ---------------------------------------------------------------------------
// Community palette (ABGR packed, 10 colors)
// ---------------------------------------------------------------------------
static const uint32_t k_palette[10] = {
0xFF4CAF50, // green
0xFFF44336, // red
0xFF2196F3, // blue
0xFFFF9800, // orange
0xFF9C27B0, // purple
0xFF00BCD4, // cyan
0xFFFFEB3B, // yellow
0xFFE91E63, // pink
0xFF795548, // brown
0xFF607D8B // blue-grey
};
// ---------------------------------------------------------------------------
// Per-instance / per-vertex data layouts
// ---------------------------------------------------------------------------
// Tier 1 packing: el color va como uint32 unico en lugar de 4 floats. Reduce
// el bandwidth de upload en 60% para nodos (28 → 16 bytes/instance) y 50%
// para aristas (24 → 12 bytes/vertex), y elimina la conversion ABGR→4floats
// en CPU (los uint32 ya tienen el layout de unpackUnorm4x8 en little-endian).
struct NodeInstance { // 16 bytes
float x, y; // world position
float size; // diameter
uint32_t color; // packed RGBA8
};
// Tier 2 (issue 0049d): aristas via vertex pulling. El buffer es estatico —
// solo `(source_idx, target_idx, color, flags)` por arista, 16 bytes — y
// se reuploads solo cuando cambia el grafo. El vertex shader hace fetch de
// las posiciones desde un TBO RG32F que SI se actualiza por frame.
struct EdgeStatic { // 16 bytes
uint32_t source; // index into nodes
uint32_t target; // index into nodes
uint32_t color; // packed RGBA8 (sin pre-multiplicar — el shader aplica edge_alpha)
uint32_t flags; // reservado para flechas/styles futuros
};
// ---------------------------------------------------------------------------
// Internal struct
// ---------------------------------------------------------------------------
struct GraphRenderer {
unsigned int fbo;
unsigned int texture;
unsigned int rbo; // depth/stencil renderbuffer
int width, height;
// Node rendering (instanced quads)
unsigned int node_vao, node_quad_vbo, node_instance_vbo;
unsigned int node_shader;
// Edge rendering (vertex pulling — issue 0049d)
// edge_vao : VAO con atributos por-instancia (divisor=1) leyendo de edge_static_vbo
// edge_vbo : buffer estatico (uno por grafo) con (source, target, color, flags)
// node_pos_buf / node_pos_tex : TBO RG32F que el vertex shader muestrea via texelFetch
unsigned int edge_vao, edge_vbo;
unsigned int edge_shader;
unsigned int node_pos_buf;
unsigned int node_pos_tex;
int edge_u_viewport_loc;
int edge_u_scale_loc;
int edge_u_translate_loc;
int edge_u_alpha_loc;
int edge_u_node_pos_loc;
// Streaming buffer capacities (in bytes). Grow x2 cuando used > capacity.
// Mantenemos el VBO orphaned con glBufferData(NULL, capacity) y luego
// hacemos glBufferSubData con los bytes realmente usados — evita el
// sync stall del driver y reduce las reallocaciones a O(log N).
size_t node_vbo_capacity;
size_t node_pos_capacity; // bytes del TBO RG32F
size_t edge_static_capacity; // bytes del buffer estatico de aristas
// CPU staging buffers — se reusan entre frames; crecen igual que el VBO.
NodeInstance* node_staging;
size_t node_staging_cap; // en NodeInstances, no bytes
float* node_pos_staging; // 2 floats (x,y) por nodo
size_t node_pos_staging_cap; // en floats
EdgeStatic* edge_static_staging;
size_t edge_static_staging_cap; // en EdgeStatic
// Cache para detectar cambios del grafo y reuploadear el edge_vbo
// estatico solo entonces. Identificamos el grafo por (puntero, count);
// basta para los flujos actuales (graph_viewport recrea el array al
// recargar). Cuando GraphData gane un campo `revision` se sustituira.
const void* cached_edges_ptr;
int cached_edge_count; // edges del grafo en el ultimo upload
int cached_edges_drawn; // edges realmente subidos (post-filtro)
bool edges_uploaded;
GraphRendererConfig config;
};
// ---------------------------------------------------------------------------
// Shader sources
// ---------------------------------------------------------------------------
// Node vertex shader — instanced unit quad
// a_color es uint32 packeado (R,G,B,A) — unpackUnorm4x8 esta en GLSL 4.20+,
// pero en core 3.30 lo hacemos manualmente con bit shifts. Eso mantiene
// compatibilidad con drivers que no exponen GL 4.x sin tener que tocar
// fn_framework.
static const char* k_node_vert = R"(
#version 330 core
// Quad corners [-0.5, 0.5]
layout(location = 0) in vec2 a_quad;
// Per-instance: world position, size, packed RGBA8 color.
layout(location = 1) in vec2 a_pos;
layout(location = 2) in float a_size;
layout(location = 3) in uint a_color;
out vec2 v_uv;
out vec4 v_color;
uniform vec2 u_viewport; // (width, height) in pixels
uniform float u_scale; // cam_zoom
uniform vec2 u_translate; // (tx, ty) in pixels
vec4 unpack_rgba8(uint c) {
return vec4(
float( c & 0xFFu),
float((c >> 8) & 0xFFu),
float((c >> 16) & 0xFFu),
float((c >> 24) & 0xFFu)
) * (1.0 / 255.0);
}
void main() {
vec2 screen = a_pos * u_scale + u_translate;
screen += a_quad * a_size * u_scale;
vec2 ndc = (screen / u_viewport) * 2.0 - 1.0;
ndc.y = -ndc.y;
gl_Position = vec4(ndc, 0.0, 1.0);
v_uv = a_quad + 0.5;
v_color = unpack_rgba8(a_color);
}
)";
// Node fragment shader — SDF circle with outline
static const char* k_node_frag = R"(
#version 330 core
in vec2 v_uv;
in vec4 v_color;
out vec4 frag_color;
uniform float u_outline_px; // outline width in uv units
uniform float u_node_px; // node diameter in pixels (= size * zoom)
void main() {
float dist = length(v_uv - 0.5);
float r = 0.5;
float fwidth_uv = 1.5 / max(u_node_px, 1.0);
float alpha = 1.0 - smoothstep(r - fwidth_uv, r, dist);
if (alpha < 0.001) discard;
float outline_uv = u_outline_px / max(u_node_px, 1.0);
float outline = smoothstep(r - outline_uv - fwidth_uv, r - outline_uv, dist);
vec3 fill = v_color.rgb;
vec3 outline_col = mix(fill, vec3(1.0), 0.6);
vec3 color = mix(fill, outline_col, outline);
frag_color = vec4(color, v_color.a * alpha);
}
)";
// Edge vertex shader — vertex pulling (issue 0049d).
// El buffer de aristas es estatico: solo indices y color. Las posiciones
// vienen del TBO `u_node_pos` (RG32F, vec2 por nodo). gl_VertexID indica si
// dibujamos el endpoint source (0) o target (1). Asi eliminamos el upload
// de `12 floats × E` por frame que dominaba el coste de aristas.
//
// Nota: usamos divisor=1 en los 4 atributos y `glDrawArraysInstanced(LINES,
// 0, 2, edge_count)` — cada instancia rinde una linea de 2 vertices, los
// atributos se mantienen constantes en la instancia y `gl_VertexID` cicla
// 0..1 dentro de ella.
//
// `samplerBuffer` y `texelFetch(samplerBuffer, int)` estan en GLSL 1.40+;
// 330 core nos vale (no necesitamos 4.30 — el issue exageraba).
static const char* k_edge_vert = R"(
#version 330 core
layout(location = 0) in uint a_source;
layout(location = 1) in uint a_target;
layout(location = 2) in uint a_color;
// location 3 (flags) reservado en el buffer (16B alignment) pero no leido aqui.
uniform samplerBuffer u_node_pos;
uniform vec2 u_viewport;
uniform float u_scale;
uniform vec2 u_translate;
uniform float u_alpha; // edge_alpha
out vec4 v_color;
vec4 unpack_rgba8(uint c) {
return vec4(
float( c & 0xFFu),
float((c >> 8) & 0xFFu),
float((c >> 16) & 0xFFu),
float((c >> 24) & 0xFFu)
) * (1.0 / 255.0);
}
void main() {
int idx = (gl_VertexID & 1) == 0 ? int(a_source) : int(a_target);
vec2 wpos = texelFetch(u_node_pos, idx).xy;
vec2 screen = wpos * u_scale + u_translate;
vec2 ndc = (screen / u_viewport) * 2.0 - 1.0;
ndc.y = -ndc.y;
gl_Position = vec4(ndc, 0.0, 1.0);
vec4 c = unpack_rgba8(a_color);
c.a *= u_alpha;
v_color = c;
}
)";
// Edge fragment shader
static const char* k_edge_frag = R"(
#version 330 core
in vec4 v_color;
out vec4 frag_color;
void main() {
frag_color = v_color;
}
)";
// ---------------------------------------------------------------------------
// Shader helpers
// ---------------------------------------------------------------------------
static unsigned int compile_shader(GLenum type, const char* src) {
unsigned int s = glCreateShader(type);
glShaderSource(s, 1, &src, nullptr);
glCompileShader(s);
int ok;
glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
if (!ok) {
char buf[512];
glGetShaderInfoLog(s, sizeof(buf), nullptr, buf);
fprintf(stderr, "[graph_renderer] shader compile error: %s\n", buf);
}
return s;
}
static unsigned int link_program(const char* vert_src, const char* frag_src) {
unsigned int vs = compile_shader(GL_VERTEX_SHADER, vert_src);
unsigned int fs = compile_shader(GL_FRAGMENT_SHADER, frag_src);
unsigned int prog = glCreateProgram();
glAttachShader(prog, vs);
glAttachShader(prog, fs);
glLinkProgram(prog);
int ok;
glGetProgramiv(prog, GL_LINK_STATUS, &ok);
if (!ok) {
char buf[512];
glGetProgramInfoLog(prog, sizeof(buf), nullptr, buf);
fprintf(stderr, "[graph_renderer] program link error: %s\n", buf);
}
glDeleteShader(vs);
glDeleteShader(fs);
return prog;
}
// ---------------------------------------------------------------------------
// FBO helpers
// ---------------------------------------------------------------------------
static void create_fbo(GraphRenderer* r) {
glGenTextures(1, &r->texture);
glBindTexture(GL_TEXTURE_2D, r->texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, r->width, r->height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0);
glGenRenderbuffers(1, &r->rbo);
glBindRenderbuffer(GL_RENDERBUFFER, r->rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, r->width, r->height);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
glGenFramebuffers(1, &r->fbo);
glBindFramebuffer(GL_FRAMEBUFFER, r->fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, r->texture, 0);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, r->rbo);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
static void destroy_fbo(GraphRenderer* r) {
glDeleteFramebuffers(1, &r->fbo);
glDeleteTextures(1, &r->texture);
glDeleteRenderbuffers(1, &r->rbo);
r->fbo = r->texture = r->rbo = 0;
}
// ---------------------------------------------------------------------------
// Capacity-tracked streaming helpers
// ---------------------------------------------------------------------------
// Doblar la capacidad cada vez que el upload supera el VBO. Asi las
// reallocaciones quedan en O(log N) en el peor caso y en >0 en el regimen
// estable. Capacidad inicial razonable: 4096 nodos / aristas (segun el .md
// del issue) — la primera llamada paga el redimensionado si hay mas.
static size_t grow_capacity(size_t current, size_t needed, size_t initial) {
size_t cap = current > 0 ? current : initial;
while (cap < needed) cap *= 2;
return cap;
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
GraphRenderer* graph_renderer_create(int width, int height, const GraphRendererConfig& config) {
GraphRenderer* r = new GraphRenderer();
r->width = width;
r->height = height;
r->config = config;
r->node_vbo_capacity = 0;
r->node_pos_capacity = 0;
r->edge_static_capacity = 0;
r->node_staging = nullptr;
r->node_staging_cap = 0;
r->node_pos_staging = nullptr;
r->node_pos_staging_cap = 0;
r->edge_static_staging = nullptr;
r->edge_static_staging_cap = 0;
r->cached_edges_ptr = nullptr;
r->cached_edge_count = 0;
r->cached_edges_drawn = 0;
r->edges_uploaded = false;
// --- FBO ---
create_fbo(r);
// --- Node VAO ---
static const float quad_verts[8] = {
-0.5f, -0.5f,
0.5f, -0.5f,
-0.5f, 0.5f,
0.5f, 0.5f,
};
glGenVertexArrays(1, &r->node_vao);
glBindVertexArray(r->node_vao);
// Quad VBO (location 0)
glGenBuffers(1, &r->node_quad_vbo);
glBindBuffer(GL_ARRAY_BUFFER, r->node_quad_vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(quad_verts), quad_verts, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
// Instance VBO — layout: NodeInstance (x, y, size, color_u32)
glGenBuffers(1, &r->node_instance_vbo);
glBindBuffer(GL_ARRAY_BUFFER, r->node_instance_vbo);
glEnableVertexAttribArray(1); // pos (2 float)
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE,
sizeof(NodeInstance),
(void*)offsetof(NodeInstance, x));
glVertexAttribDivisor(1, 1);
glEnableVertexAttribArray(2); // size (1 float)
glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE,
sizeof(NodeInstance),
(void*)offsetof(NodeInstance, size));
glVertexAttribDivisor(2, 1);
glEnableVertexAttribArray(3); // color (1 uint32) — IPointer, no normalizado
glVertexAttribIPointer(3, 1, GL_UNSIGNED_INT,
sizeof(NodeInstance),
(void*)offsetof(NodeInstance, color));
glVertexAttribDivisor(3, 1);
glBindVertexArray(0);
// --- Edge VAO (vertex pulling, divisor=1 sobre el buffer estatico) ---
glGenVertexArrays(1, &r->edge_vao);
glBindVertexArray(r->edge_vao);
glGenBuffers(1, &r->edge_vbo);
glBindBuffer(GL_ARRAY_BUFFER, r->edge_vbo);
// (source, target, color, flags) — los 4 con divisor=1.
glEnableVertexAttribArray(0);
glVertexAttribIPointer(0, 1, GL_UNSIGNED_INT, sizeof(EdgeStatic),
(void*)offsetof(EdgeStatic, source));
glVertexAttribDivisor(0, 1);
glEnableVertexAttribArray(1);
glVertexAttribIPointer(1, 1, GL_UNSIGNED_INT, sizeof(EdgeStatic),
(void*)offsetof(EdgeStatic, target));
glVertexAttribDivisor(1, 1);
glEnableVertexAttribArray(2);
glVertexAttribIPointer(2, 1, GL_UNSIGNED_INT, sizeof(EdgeStatic),
(void*)offsetof(EdgeStatic, color));
glVertexAttribDivisor(2, 1);
// location 3 reservado en el buffer pero no enabled — el shader actual
// no lo lee. Mantenemos el slot para futuros estilos/flechas.
glBindVertexArray(0);
// --- TBO de posiciones de nodos (RG32F, vec2 por nodo) ---
glGenBuffers(1, &r->node_pos_buf);
glBindBuffer(GL_TEXTURE_BUFFER, r->node_pos_buf);
// Reservamos capacidad inicial; se redimensiona en draw segun N.
glBufferData(GL_TEXTURE_BUFFER, 4096 * 2 * sizeof(float), nullptr, GL_STREAM_DRAW);
r->node_pos_capacity = 4096 * 2 * sizeof(float);
glGenTextures(1, &r->node_pos_tex);
glBindTexture(GL_TEXTURE_BUFFER, r->node_pos_tex);
glTexBuffer(GL_TEXTURE_BUFFER, GL_RG32F, r->node_pos_buf);
glBindTexture(GL_TEXTURE_BUFFER, 0);
glBindBuffer(GL_TEXTURE_BUFFER, 0);
// --- Shaders ---
r->node_shader = link_program(k_node_vert, k_node_frag);
r->edge_shader = link_program(k_edge_vert, k_edge_frag);
// Cachear locations de uniforms del edge shader (issue 0049d): se
// resuelven una vez en lugar de glGetUniformLocation cada frame.
r->edge_u_viewport_loc = glGetUniformLocation(r->edge_shader, "u_viewport");
r->edge_u_scale_loc = glGetUniformLocation(r->edge_shader, "u_scale");
r->edge_u_translate_loc = glGetUniformLocation(r->edge_shader, "u_translate");
r->edge_u_alpha_loc = glGetUniformLocation(r->edge_shader, "u_alpha");
r->edge_u_node_pos_loc = glGetUniformLocation(r->edge_shader, "u_node_pos");
return r;
}
void graph_renderer_destroy(GraphRenderer* r) {
if (!r) return;
destroy_fbo(r);
glDeleteVertexArrays(1, &r->node_vao);
glDeleteBuffers(1, &r->node_quad_vbo);
glDeleteBuffers(1, &r->node_instance_vbo);
glDeleteVertexArrays(1, &r->edge_vao);
glDeleteBuffers(1, &r->edge_vbo);
glDeleteBuffers(1, &r->node_pos_buf);
glDeleteTextures(1, &r->node_pos_tex);
glDeleteProgram(r->node_shader);
glDeleteProgram(r->edge_shader);
free(r->node_staging);
free(r->node_pos_staging);
free(r->edge_static_staging);
delete r;
}
void graph_renderer_resize(GraphRenderer* r, int width, int height) {
if (!r) return;
if (r->width == width && r->height == height) return;
r->width = width;
r->height = height;
destroy_fbo(r);
create_fbo(r);
}
unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph,
float cam_x, float cam_y, float cam_zoom) {
if (!r) return 0;
// --- Save GL state ---
GLint prev_fbo;
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prev_fbo);
GLint prev_viewport[4];
glGetIntegerv(GL_VIEWPORT, prev_viewport);
// --- Bind FBO ---
glBindFramebuffer(GL_FRAMEBUFFER, r->fbo);
glViewport(0, 0, r->width, r->height);
// Clear with bg_color (interpreted as RGBA8 packed — same memory layout)
uint8_t br, bg, bb, ba;
unpack_rgba8(r->config.bg_color, br, bg, bb, ba);
glClearColor(br / 255.0f, bg / 255.0f, bb / 255.0f, ba / 255.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// View transform: world -> screen pixels
float scale = cam_zoom;
float tx = -cam_x * scale + (float)r->width * 0.5f;
float ty = -cam_y * scale + (float)r->height * 0.5f;
// Frustum cull AABB en world coords. Margen del 10% para que un nodo o
// arista a punto de entrar en pantalla no haga pop-in al moverse.
float half_w = ((float)r->width * 0.5f) / std::max(scale, 0.0001f);
float half_h = ((float)r->height * 0.5f) / std::max(scale, 0.0001f);
const float margin = 0.10f;
float vx0 = cam_x - half_w * (1.0f + margin);
float vx1 = cam_x + half_w * (1.0f + margin);
float vy0 = cam_y - half_h * (1.0f + margin);
float vy1 = cam_y + half_h * (1.0f + margin);
// ----------------------------------------------------------------
// Subir posiciones de nodos al TBO (vec2 por nodo). Lo necesitamos
// tanto si dibujamos aristas (vertex pulling) como antes de dibujar
// nodos — pero se calcula una sola vez por frame.
// ----------------------------------------------------------------
bool tbo_ready = false;
if (graph.node_count > 0 && graph.nodes) {
size_t need_floats = (size_t)graph.node_count * 2;
if (need_floats > r->node_pos_staging_cap) {
size_t new_cap = grow_capacity(r->node_pos_staging_cap, need_floats, 8192);
r->node_pos_staging = (float*)realloc(r->node_pos_staging, new_cap * sizeof(float));
r->node_pos_staging_cap = new_cap;
}
for (int i = 0; i < graph.node_count; ++i) {
r->node_pos_staging[i * 2 + 0] = graph.nodes[i].x;
r->node_pos_staging[i * 2 + 1] = graph.nodes[i].y;
}
const size_t used_bytes = need_floats * sizeof(float);
if (used_bytes > r->node_pos_capacity) {
r->node_pos_capacity = grow_capacity(r->node_pos_capacity, used_bytes,
4096 * 2 * sizeof(float));
}
glBindBuffer(GL_TEXTURE_BUFFER, r->node_pos_buf);
// Orphan + subdata: misma estrategia que en 0049c, evita stall.
glBufferData(GL_TEXTURE_BUFFER, (GLsizeiptr)r->node_pos_capacity, nullptr, GL_STREAM_DRAW);
glBufferSubData(GL_TEXTURE_BUFFER, 0, (GLsizeiptr)used_bytes, r->node_pos_staging);
// glTexBuffer ya esta vinculado al buffer en create — el view sigue
// valido tras orphan: GL_TEXTURE_BUFFER referencia al BO por nombre.
glBindBuffer(GL_TEXTURE_BUFFER, 0);
tbo_ready = true;
}
// ----------------------------------------------------------------
// Aristas via vertex pulling. El buffer estatico solo se reupload
// cuando el grafo cambia — detectamos con (puntero, count).
// ----------------------------------------------------------------
if (tbo_ready && graph.edge_count > 0 && graph.edges) {
const bool graph_changed =
!r->edges_uploaded
|| r->cached_edges_ptr != (const void*)graph.edges
|| r->cached_edge_count != graph.edge_count;
if (graph_changed) {
// (Re)build el buffer estatico. Skipeamos aristas con indices
// fuera de rango — pueden aparecer durante una recarga parcial
// del grafo y no queremos que el GPU lea fuera del TBO.
if ((size_t)graph.edge_count > r->edge_static_staging_cap) {
size_t new_cap = grow_capacity(r->edge_static_staging_cap,
(size_t)graph.edge_count, 8192);
r->edge_static_staging = (EdgeStatic*)realloc(r->edge_static_staging,
new_cap * sizeof(EdgeStatic));
r->edge_static_staging_cap = new_cap;
}
size_t out = 0;
for (int i = 0; i < graph.edge_count; ++i) {
const GraphEdge& e = graph.edges[i];
if (e.source >= (uint32_t)graph.node_count) continue;
if (e.target >= (uint32_t)graph.node_count) continue;
uint32_t col = e.color != 0 ? e.color
: pack_rgba8(0x88, 0x88, 0x88, 0xFF);
r->edge_static_staging[out++] = { e.source, e.target, col, 0u };
}
if (out > 0) {
const size_t used_bytes = out * sizeof(EdgeStatic);
if (used_bytes > r->edge_static_capacity) {
r->edge_static_capacity = grow_capacity(r->edge_static_capacity,
used_bytes,
8192 * sizeof(EdgeStatic));
}
glBindBuffer(GL_ARRAY_BUFFER, r->edge_vbo);
glBufferData(GL_ARRAY_BUFFER, (GLsizeiptr)r->edge_static_capacity,
nullptr, GL_STATIC_DRAW);
glBufferSubData(GL_ARRAY_BUFFER, 0, (GLsizeiptr)used_bytes,
r->edge_static_staging);
}
r->cached_edges_ptr = (const void*)graph.edges;
r->cached_edge_count = graph.edge_count;
r->cached_edges_drawn = (int)out;
r->edges_uploaded = (out > 0);
}
if (r->edges_uploaded) {
glUseProgram(r->edge_shader);
glUniform2f(r->edge_u_viewport_loc, (float)r->width, (float)r->height);
glUniform1f(r->edge_u_scale_loc, scale);
glUniform2f(r->edge_u_translate_loc, tx, ty);
glUniform1f(r->edge_u_alpha_loc, r->config.edge_alpha);
// Bind TBO al sampler u_node_pos en la texture unit 0.
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_BUFFER, r->node_pos_tex);
glUniform1i(r->edge_u_node_pos_loc, 0);
glLineWidth(r->config.edge_width);
glBindVertexArray(r->edge_vao);
// Una "instancia" = 1 linea (2 vertices). gl_VertexID dentro
// de la instancia es 0 o 1 → elige endpoint source o target.
glDrawArraysInstanced(GL_LINES, 0, 2, (GLsizei)r->cached_edges_drawn);
glBindVertexArray(0);
glBindTexture(GL_TEXTURE_BUFFER, 0);
}
} else if (graph.edge_count == 0) {
// Si el caller borra todas las aristas, invalidamos el cache para
// que el siguiente upload reconstruya el buffer.
r->edges_uploaded = false;
}
// ----------------------------------------------------------------
// Draw nodes (instanced quads, frustum-culled)
// ----------------------------------------------------------------
if (graph.node_count > 0 && graph.nodes) {
if ((size_t)graph.node_count > r->node_staging_cap) {
size_t new_cap = grow_capacity(r->node_staging_cap, (size_t)graph.node_count, 4096);
r->node_staging = (NodeInstance*)realloc(r->node_staging, new_cap * sizeof(NodeInstance));
r->node_staging_cap = new_cap;
}
size_t visible = 0;
for (int i = 0; i < graph.node_count; ++i) {
const GraphNode& n = graph.nodes[i];
float sz = n.size > 0.0f ? n.size : 4.0f;
float half = sz * 0.5f;
// AABB del nodo: centro ± half. Skip si fuera del viewport.
if (n.x + half < vx0 || n.x - half > vx1) continue;
if (n.y + half < vy0 || n.y - half > vy1) continue;
uint32_t ncol = n.color != 0 ? n.color : k_palette[n.community % 10];
r->node_staging[visible++] = { n.x, n.y, sz, ncol };
}
if (visible > 0) {
const size_t used_bytes = visible * sizeof(NodeInstance);
if (used_bytes > r->node_vbo_capacity) {
r->node_vbo_capacity = grow_capacity(r->node_vbo_capacity, used_bytes,
4096 * sizeof(NodeInstance));
}
glUseProgram(r->node_shader);
glUniform2f(glGetUniformLocation(r->node_shader, "u_viewport"),
(float)r->width, (float)r->height);
glUniform1f(glGetUniformLocation(r->node_shader, "u_scale"), scale);
glUniform2f(glGetUniformLocation(r->node_shader, "u_translate"), tx, ty);
glUniform1f(glGetUniformLocation(r->node_shader, "u_outline_px"), r->config.node_outline);
glBindVertexArray(r->node_vao);
glBindBuffer(GL_ARRAY_BUFFER, r->node_instance_vbo);
glBufferData(GL_ARRAY_BUFFER, (GLsizeiptr)r->node_vbo_capacity, nullptr, GL_STREAM_DRAW);
glBufferSubData(GL_ARRAY_BUFFER, 0, (GLsizeiptr)used_bytes, r->node_staging);
float avg_px = 8.0f * scale; // estimacion para el AA del SDF
glUniform1f(glGetUniformLocation(r->node_shader, "u_node_px"), avg_px);
glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, (GLsizei)visible);
glBindVertexArray(0);
}
}
// --- Restore GL state ---
glDisable(GL_BLEND);
glBindFramebuffer(GL_FRAMEBUFFER, (GLuint)prev_fbo);
glViewport(prev_viewport[0], prev_viewport[1], prev_viewport[2], prev_viewport[3]);
return r->texture;
}