daf491cd99
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.
672 lines
27 KiB
C++
672 lines
27 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>
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 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;
|
||
}
|