c967c2edfd
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>
884 lines
34 KiB
C++
884 lines
34 KiB
C++
#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;
|
||
}
|