Files
egutierrez c967c2edfd feat(viz): renderer shapes/iconos/flechas/edge-styles (issue 0049f)
graph_renderer 1.5.0:
- 6 shapes SDF (circle, square, diamond, hex, triangle, rounded square)
  con dispatch en fragment shader y AA via fwidth.
- Atlas opcional de iconos Tabler bakeado por graph_icons; el shader
  compone overlay desde un uniform vec4 u_icon_uvs[256]. Setter publico
  graph_renderer_set_icon_atlas(r, tex, uv_table, count).
- Aristas direccionales: 6 vertices por arista (line + chevron de la
  flecha) en una sola draw call; segmento principal acortado por el
  radio del nodo target.
- Edge styles solid/dashed/dotted via descarte por arc_length en el
  fragment shader; las lineas del chevron son siempre solidas.

graph_icons 1.0.0 (nuevo):
- Atlas RGBA8 512x512 = grid 16x16 (256 iconos max) bakeado con
  stb_truetype desde tabler-icons.ttf.
- API: graph_icons_build/texture/region/uv_table/destroy. icon_id es
  1-based; 0 reservado para "sin icono".
- Hook FN_GRAPH_ICONS_SKIP_GL=1 para tests sin contexto GL.

Demo demos_graph_styles en primitives_gallery: 6 EntityTypes (uno por
shape) con icono Tabler representativo + 3 RelationTypes (knows/uses/
owns) con flechas direccionales y los 3 estilos.

test_graph_icons: 6 casos cubriendo bake, regiones 1-indexed, uv_table
consistente, layout en grid 16x16, validacion de count fuera de rango,
y verificacion de alpha != 0 en las celdas tras bake.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 23:01:49 +02:00

884 lines
34 KiB
C++
Raw Permalink 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>
// ---------------------------------------------------------------------------
// Fallback palette (RGBA8 con R en LSB) — usada solo cuando GraphData::types
// esta vacio. En el modelo extendido (issue 0049e) la apariencia de cada
// nodo viene resuelta por `resolve_node_color()`, que mira primero el
// override del nodo, luego el EntityType de la tabla, y finalmente este
// fallback. Mantener 10 colores para nodos sin tipos sigue siendo util en
// demos que aun no construyen tablas EntityType.
// ---------------------------------------------------------------------------
static const uint32_t k_fallback_palette[10] = {
0xFF4CAF50u, 0xFFF44336u, 0xFF2196F3u, 0xFFFF9800u, 0xFF9C27B0u,
0xFF00BCD4u, 0xFFFFEB3Bu, 0xFFE91E63u, 0xFF795548u, 0xFF607D8Bu,
};
// Maximo de iconos que cabe en el uniform array del shader. 256 es lo que
// genera `graph_icons` (grid 16×16 en 512×512). Subirlo requiere mas budget
// de uniforms (vec4×N → 4 floats por entrada) y aun cabe holgado en el
// limite GL 3.30 de 1024 vec4 por bloque.
static constexpr int k_max_icons = 256;
// ---------------------------------------------------------------------------
// Per-instance / per-vertex data layouts
// ---------------------------------------------------------------------------
// 0049f: NodeInstance crece de 16 a 24 bytes para llevar shape + icon_id.
// `shape_icon` empaqueta shape (8 bits bajos) + icon_id (16 bits siguientes);
// los UVs del icono no viajan por instancia — el shader los busca en un
// `uniform vec4 u_icon_uvs[256]` indexado por icon_id-1. Asi conservamos
// bandwidth aunque haya muchos nodos con el mismo icono.
struct NodeInstance { // 24 bytes (alineado a 4)
float x, y; // 8
float size; // 4 (= diametro en pixels world-space)
uint32_t color; // 4
uint32_t shape_icon; // 4 — (shape & 0xFF) | (icon_id << 8)
uint32_t pad_; // 4 — relleno explicito; reservado para flags futuros
};
// 0049f: EdgeStatic crece de 16 a 20 bytes para llevar style + flags reales.
// `style_flags`: flags (low 8 bits) | style (next 8 bits). El resto sigue
// siendo source/target/color como en 0049d.
struct EdgeStatic { // 20 bytes
uint32_t source; // index into nodes
uint32_t target; // index into nodes
uint32_t color; // packed RGBA8
uint32_t style_flags; // (flags & 0xFF) | (style << 8)
uint32_t pad_; // pad a multiplo de 4 — actualmente sin uso
};
// ---------------------------------------------------------------------------
// Internal struct
// ---------------------------------------------------------------------------
struct GraphRenderer {
unsigned int fbo;
unsigned int texture;
unsigned int rbo;
int width, height;
// Node rendering (instanced quads)
unsigned int node_vao, node_quad_vbo, node_instance_vbo;
unsigned int node_shader;
int node_u_viewport_loc;
int node_u_scale_loc;
int node_u_translate_loc;
int node_u_outline_loc;
int node_u_node_px_loc;
int node_u_icon_atlas_loc;
int node_u_has_icons_loc;
int node_u_icon_uvs_loc;
// Edge rendering (vertex pulling con 6 vertices/instancia para flecha)
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).
size_t node_vbo_capacity;
size_t node_pos_capacity;
size_t edge_static_capacity;
// CPU staging
NodeInstance* node_staging;
size_t node_staging_cap;
float* node_pos_staging;
size_t node_pos_staging_cap;
EdgeStatic* edge_static_staging;
size_t edge_static_staging_cap;
// Edge cache (reupload solo cuando cambia el grafo)
const void* cached_edges_ptr;
int cached_edge_count;
int cached_edges_drawn;
bool edges_uploaded;
// Icon atlas binding (0 = sin iconos)
unsigned int icon_atlas_tex;
float icon_uvs[k_max_icons * 4];
int icon_uv_count;
GraphRendererConfig config;
};
// ---------------------------------------------------------------------------
// Shader sources
// ---------------------------------------------------------------------------
// Node vertex shader — instanced unit quad, ahora con shape + icon_id.
// Pasamos el texto del shape al fragment para que despache el SDF correcto;
// los UVs del icono se buscan en `u_icon_uvs[icon_id-1]`.
static const char* k_node_vert = R"(
#version 330 core
layout(location = 0) in vec2 a_quad;
layout(location = 1) in vec2 a_pos;
layout(location = 2) in float a_size;
layout(location = 3) in uint a_color;
layout(location = 4) in uint a_shape_icon;
out vec2 v_uv;
out vec4 v_color;
flat out uint v_shape;
flat out uint v_icon_id;
flat out vec4 v_icon_uv;
uniform vec2 u_viewport;
uniform float u_scale;
uniform vec2 u_translate;
uniform vec4 u_icon_uvs[256];
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);
v_shape = a_shape_icon & 0xFFu;
v_icon_id = (a_shape_icon >> 8) & 0xFFFFu;
if (v_icon_id != 0u) {
v_icon_uv = u_icon_uvs[int(v_icon_id) - 1];
} else {
v_icon_uv = vec4(0.0);
}
}
)";
// Node fragment shader — SDF dispatch + opcional icon overlay.
// Para mantener la calidad del AA usamos `fwidth(d)` en lugar del
// `1.5/u_node_px` viejo: sirve igual a cualquier zoom y se queda nitido en
// los bordes complejos (hexagono, triangulo).
static const char* k_node_frag = R"(
#version 330 core
in vec2 v_uv;
in vec4 v_color;
flat in uint v_shape;
flat in uint v_icon_id;
flat in vec4 v_icon_uv;
out vec4 frag_color;
uniform float u_outline_px;
uniform float u_node_px;
uniform sampler2D u_icon_atlas;
uniform int u_has_icons;
float sdf_circle(vec2 uv) {
return length(uv - 0.5) - 0.5;
}
float sdf_square(vec2 uv) {
vec2 d = abs(uv - 0.5) - 0.5;
return max(d.x, d.y);
}
float sdf_diamond(vec2 uv) {
vec2 d = abs(uv - 0.5);
return d.x + d.y - 0.5;
}
// Hexagono regular alineado horizontalmente; SDF derivado del clasico de
// Inigo Quilez adaptado al cuadrado [0,1]^2. Inscribimos el hex dentro del
// circulo de radio 0.5 para que sus vertices toquen los bordes — asi
// area visual ~ a la del circulo del mismo `size`.
float sdf_hex(vec2 uv) {
vec2 p = abs(uv - 0.5);
const vec2 k = vec2(0.866025404, 0.5);
p -= 2.0 * min(dot(k, p), 0.0) * k;
p -= vec2(clamp(p.x, -k.y * 0.5, k.y * 0.5), 0.5);
return length(p) * sign(p.y) - (0.5 * 0.866025404);
}
// Triangulo equilatero apuntando hacia arriba dentro de [0,1]^2.
float sdf_triangle(vec2 uv) {
const float k = 1.732050808; // sqrt(3)
vec2 p = uv - vec2(0.5, 0.5);
p.x = abs(p.x) - 0.5;
p.y = p.y + 0.5 / k;
if (p.x + k * p.y > 0.0) p = vec2(p.x - k * p.y, -k * p.x - p.y) / 2.0;
p.x -= clamp(p.x, -1.0, 0.0);
return -length(p) * sign(p.y);
}
float sdf_rrect(vec2 uv) {
float r = 0.18;
vec2 d = abs(uv - 0.5) - (0.5 - r);
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0) - r;
}
float pick_sdf(uint shape, vec2 uv) {
// shape 0 = SHAPE_USE_TYPE: el CPU resuelve antes; aqui no debe llegar.
// 1=circle 2=square 3=diamond 4=hex 5=triangle 6=rounded_square
if (shape == 1u) return sdf_circle(uv);
else if (shape == 2u) return sdf_square(uv);
else if (shape == 3u) return sdf_diamond(uv);
else if (shape == 4u) return sdf_hex(uv);
else if (shape == 5u) return sdf_triangle(uv);
else if (shape == 6u) return sdf_rrect(uv);
return sdf_circle(uv); // default robusto si shape mal codificado
}
void main() {
float d = pick_sdf(v_shape, v_uv);
float aa = max(fwidth(d), 0.001);
float fill_alpha = 1.0 - smoothstep(-aa, 0.0, d);
if (fill_alpha < 0.001) discard;
// Outline: anillo exterior — la anchura en uv viene de outline_px / node_px.
float outline_uv = u_outline_px / max(u_node_px, 1.0);
float outline = smoothstep(-outline_uv - aa, -outline_uv, d);
vec3 fill = v_color.rgb;
vec3 outline_col = mix(fill, vec3(1.0), 0.6);
vec3 col = mix(fill, outline_col, outline);
// Overlay del icono (solo si hay atlas + icon_id != 0). El icono se
// tintamos sumando blanco modulado por su alpha — el resultado sigue
// siendo legible sobre cualquier color de fondo del nodo.
if (u_has_icons != 0 && v_icon_id != 0u) {
vec2 atlas_uv = mix(v_icon_uv.xy, v_icon_uv.zw, v_uv);
vec4 ic = texture(u_icon_atlas, atlas_uv);
col = mix(col, vec3(1.0), ic.a * 0.85);
}
frag_color = vec4(col, v_color.a * fill_alpha);
}
)";
// Edge vertex shader — vertex pulling, ahora 6 vertices por arista para
// soportar flecha en aristas EF_DIRECTED:
// gl_VertexID 0 (line src→tip), 1 (tip)
// gl_VertexID 2 (tip), 3 (back_left)
// gl_VertexID 4 (tip), 5 (back_right)
// Si no esta directed, los vertices 2..5 se colapsan al tip (lineas
// degeneradas no visibles).
//
// Para dashed/dotted: pasamos `arc_length` interpolado (en pixels) al
// fragment shader; este descarta segun style.
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;
layout(location = 3) in uint a_style_flags;
uniform samplerBuffer u_node_pos;
uniform vec2 u_viewport;
uniform float u_scale;
uniform vec2 u_translate;
uniform float u_alpha;
out vec4 v_color;
flat out uint v_style;
flat out uint v_segment; // 0=line, 1=arrow
out float v_arc;
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);
}
vec2 to_screen(vec2 wpos) {
return wpos * u_scale + u_translate;
}
vec2 to_ndc(vec2 screen) {
vec2 ndc = (screen / u_viewport) * 2.0 - 1.0;
return vec2(ndc.x, -ndc.y);
}
void main() {
int vid = gl_VertexID;
uint flags = a_style_flags & 0xFFu;
uint style = (a_style_flags >> 8) & 0xFFu;
bool directed = (flags & 1u) != 0u; // EF_DIRECTED == 1
vec2 wsrc = texelFetch(u_node_pos, int(a_source)).xy;
vec2 wtgt = texelFetch(u_node_pos, int(a_target)).xy;
vec2 ssrc = to_screen(wsrc);
vec2 stgt = to_screen(wtgt);
vec2 dir = stgt - ssrc;
float seg_len = length(dir);
vec2 dir_n = (seg_len > 0.0001) ? dir / seg_len : vec2(1.0, 0.0);
vec2 perp = vec2(-dir_n.y, dir_n.x);
// Acortamos el segmento principal si la arista es directed para que la
// flecha no se incruste en el nodo target. Tamano fijo en pixels = 10.
float arrow_px = 10.0;
vec2 tip = stgt;
vec2 line_end = directed ? (stgt - dir_n * arrow_px * 0.5) : stgt;
vec2 spos;
float arc;
uint segment;
if (vid <= 1) {
// Linea principal source→line_end.
segment = 0u;
if (vid == 0) { spos = ssrc; arc = 0.0; }
else { spos = line_end; arc = length(line_end - ssrc); }
} else {
segment = 1u;
arc = 0.0;
if (!directed) {
// Sin flecha: degenerado en el tip — sin pintar.
spos = tip;
} else {
// Triangulo de la flecha en 2 lineas (chevron):
// (tip, back_left) y (tip, back_right)
vec2 back = tip - dir_n * arrow_px;
vec2 left_p = back + perp * arrow_px * 0.5;
vec2 right_p = back - perp * arrow_px * 0.5;
if (vid == 2) spos = tip;
else if (vid == 3) spos = left_p;
else if (vid == 4) spos = tip;
else spos = right_p;
}
}
gl_Position = vec4(to_ndc(spos), 0.0, 1.0);
vec4 c = unpack_rgba8(a_color);
c.a *= u_alpha;
v_color = c;
v_style = style;
v_segment = segment;
v_arc = arc;
}
)";
// Edge fragment shader — descarta segun style + arc_length para producir
// dashed (period 8 px, duty 0.5) o dotted (period 4 px, duty 0.25). Las
// lineas de la flecha (segment==1) se renderizan siempre solidas.
static const char* k_edge_frag = R"(
#version 330 core
in vec4 v_color;
flat in uint v_style;
flat in uint v_segment;
in float v_arc;
out vec4 frag_color;
void main() {
if (v_segment == 0u) {
// EDGE_USE_TYPE=0, EDGE_SOLID=1, EDGE_DASHED=2, EDGE_DOTTED=3
if (v_style == 2u) {
if (mod(v_arc, 8.0) > 4.0) discard;
} else if (v_style == 3u) {
if (mod(v_arc, 4.0) > 1.0) discard;
}
}
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[1024];
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[1024];
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
// ---------------------------------------------------------------------------
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;
r->icon_atlas_tex = 0;
r->icon_uv_count = 0;
std::memset(r->icon_uvs, 0, sizeof(r->icon_uvs));
create_fbo(r);
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);
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);
glGenBuffers(1, &r->node_instance_vbo);
glBindBuffer(GL_ARRAY_BUFFER, r->node_instance_vbo);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(NodeInstance),
(void*)offsetof(NodeInstance, x));
glVertexAttribDivisor(1, 1);
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, sizeof(NodeInstance),
(void*)offsetof(NodeInstance, size));
glVertexAttribDivisor(2, 1);
glEnableVertexAttribArray(3);
glVertexAttribIPointer(3, 1, GL_UNSIGNED_INT, sizeof(NodeInstance),
(void*)offsetof(NodeInstance, color));
glVertexAttribDivisor(3, 1);
glEnableVertexAttribArray(4);
glVertexAttribIPointer(4, 1, GL_UNSIGNED_INT, sizeof(NodeInstance),
(void*)offsetof(NodeInstance, shape_icon));
glVertexAttribDivisor(4, 1);
glBindVertexArray(0);
glGenVertexArrays(1, &r->edge_vao);
glBindVertexArray(r->edge_vao);
glGenBuffers(1, &r->edge_vbo);
glBindBuffer(GL_ARRAY_BUFFER, r->edge_vbo);
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);
glEnableVertexAttribArray(3);
glVertexAttribIPointer(3, 1, GL_UNSIGNED_INT, sizeof(EdgeStatic),
(void*)offsetof(EdgeStatic, style_flags));
glVertexAttribDivisor(3, 1);
glBindVertexArray(0);
glGenBuffers(1, &r->node_pos_buf);
glBindBuffer(GL_TEXTURE_BUFFER, r->node_pos_buf);
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);
r->node_shader = link_program(k_node_vert, k_node_frag);
r->edge_shader = link_program(k_edge_vert, k_edge_frag);
r->node_u_viewport_loc = glGetUniformLocation(r->node_shader, "u_viewport");
r->node_u_scale_loc = glGetUniformLocation(r->node_shader, "u_scale");
r->node_u_translate_loc = glGetUniformLocation(r->node_shader, "u_translate");
r->node_u_outline_loc = glGetUniformLocation(r->node_shader, "u_outline_px");
r->node_u_node_px_loc = glGetUniformLocation(r->node_shader, "u_node_px");
r->node_u_icon_atlas_loc = glGetUniformLocation(r->node_shader, "u_icon_atlas");
r->node_u_has_icons_loc = glGetUniformLocation(r->node_shader, "u_has_icons");
r->node_u_icon_uvs_loc = glGetUniformLocation(r->node_shader, "u_icon_uvs");
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);
}
void graph_renderer_set_icon_atlas(GraphRenderer* r,
unsigned int texture_id,
const float* uv_table,
int count) {
if (!r) return;
r->icon_atlas_tex = texture_id;
r->icon_uv_count = (count > k_max_icons) ? k_max_icons : (count < 0 ? 0 : count);
if (r->icon_uv_count > 0 && uv_table) {
std::memcpy(r->icon_uvs, uv_table,
(size_t)r->icon_uv_count * 4 * sizeof(float));
}
// Limpia las entradas no usadas para evitar UVs basura si el shader las
// sample por error. Costo: O(k_max_icons) — irrelevante.
if (r->icon_uv_count < k_max_icons) {
std::memset(r->icon_uvs + r->icon_uv_count * 4, 0,
(size_t)(k_max_icons - r->icon_uv_count) * 4 * sizeof(float));
}
}
unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph,
float cam_x, float cam_y, float cam_zoom) {
if (!r) return 0;
GLint prev_fbo;
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prev_fbo);
GLint prev_viewport[4];
glGetIntegerv(GL_VIEWPORT, prev_viewport);
glBindFramebuffer(GL_FRAMEBUFFER, r->fbo);
glViewport(0, 0, r->width, r->height);
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);
float scale = cam_zoom;
float tx = -cam_x * scale + (float)r->width * 0.5f;
float ty = -cam_y * scale + (float)r->height * 0.5f;
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.
// ----------------------------------------------------------------
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);
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);
glBindBuffer(GL_TEXTURE_BUFFER, 0);
tbo_ready = true;
}
// ----------------------------------------------------------------
// Aristas via vertex pulling. 6 vertices por arista (line + arrow).
// El buffer estatico se reupload solo cuando cambia el grafo.
// ----------------------------------------------------------------
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) {
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;
if (!(e.flags & EF_VISIBLE)) continue;
if (!(graph.nodes[e.source].flags & NF_VISIBLE)) continue;
if (!(graph.nodes[e.target].flags & NF_VISIBLE)) continue;
uint32_t col = resolve_edge_color(e, graph.rel_types,
graph.rel_type_count);
if (col == 0u) col = pack_rgba8(0x88, 0x88, 0x88, 0xFF);
uint8_t style = resolve_edge_style(e, graph.rel_types,
graph.rel_type_count);
uint32_t style_flags = ((uint32_t)e.flags & 0xFFu)
| ((uint32_t)style << 8);
r->edge_static_staging[out++] = { e.source, e.target, col, style_flags, 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);
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);
// 6 vertices por instancia: 2 linea + 4 chevron de la flecha.
glDrawArraysInstanced(GL_LINES, 0, 6, (GLsizei)r->cached_edges_drawn);
glBindVertexArray(0);
glBindTexture(GL_TEXTURE_BUFFER, 0);
}
} else if (graph.edge_count == 0) {
r->edges_uploaded = false;
}
// ----------------------------------------------------------------
// Draw nodes (instanced quads, frustum-culled). Empaqueta shape e
// icon_id por instancia; el shader despacha el SDF y aplica overlay.
// ----------------------------------------------------------------
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];
if (!(n.flags & NF_VISIBLE)) continue;
float sz = resolve_node_size(n, graph.types, graph.type_count);
if (sz <= 0.0f) sz = 4.0f;
float half = sz * 0.5f;
if (n.x + half < vx0 || n.x - half > vx1) continue;
if (n.y + half < vy0 || n.y - half > vy1) continue;
uint32_t ncol;
if (n.color_override != 0u) {
ncol = n.color_override;
} else if (graph.types && n.type_id < (uint16_t)graph.type_count) {
ncol = graph.types[n.type_id].color;
} else {
ncol = k_fallback_palette[n.type_id % 10];
}
uint8_t shape = resolve_node_shape(n, graph.types, graph.type_count);
if (shape == SHAPE_USE_TYPE) shape = SHAPE_CIRCLE;
// icon_id solo viene del EntityType (los nodos no tienen override
// de icono en el modelo actual). 0 = sin overlay.
uint16_t icon_id = 0;
if (graph.types && n.type_id < (uint16_t)graph.type_count) {
icon_id = graph.types[n.type_id].icon_id;
}
if (icon_id > r->icon_uv_count) icon_id = 0; // fuera de tabla
uint32_t shape_icon = ((uint32_t)shape & 0xFFu)
| ((uint32_t)icon_id << 8);
r->node_staging[visible++] = { n.x, n.y, sz, ncol, shape_icon, 0u };
}
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(r->node_u_viewport_loc, (float)r->width, (float)r->height);
glUniform1f(r->node_u_scale_loc, scale);
glUniform2f(r->node_u_translate_loc, tx, ty);
glUniform1f(r->node_u_outline_loc, r->config.node_outline);
float avg_px = 8.0f * scale;
glUniform1f(r->node_u_node_px_loc, avg_px);
// Subimos siempre la tabla de UVs — son 256 vec4 = 4KB, peanuts.
glUniform4fv(r->node_u_icon_uvs_loc, k_max_icons, r->icon_uvs);
const int has_icons = (r->icon_atlas_tex != 0 && r->icon_uv_count > 0) ? 1 : 0;
glUniform1i(r->node_u_has_icons_loc, has_icons);
if (has_icons) {
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, r->icon_atlas_tex);
glUniform1i(r->node_u_icon_atlas_loc, 1);
}
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);
glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, (GLsizei)visible);
glBindVertexArray(0);
if (has_icons) {
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, 0);
glActiveTexture(GL_TEXTURE0);
}
}
}
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;
}