#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 #include #include #include #include #include // --------------------------------------------------------------------------- // 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, // green 0xFFF44336u, // red 0xFF2196F3u, // blue 0xFFFF9800u, // orange 0xFF9C27B0u, // purple 0xFF00BCD4u, // cyan 0xFFFFEB3Bu, // yellow 0xFFE91E63u, // pink 0xFF795548u, // brown 0xFF607D8Bu, // 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; if (!(e.flags & EF_VISIBLE)) continue; // Saltamos aristas cuyos endpoints no estan visibles — // el shader las pintaria igualmente (las posiciones siguen // en el TBO) pero la aristas tendrian apariencia conectando // "vacios" si los nodos estan ocultos por NF_VISIBLE off. 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); 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]; 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; // 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; // Apariencia: 1) override del nodo, 2) EntityType, 3) fallback // indexado por type_id (paleta de 10 — sustituye al community // del modelo v1). 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]; } // 0049f anadira el dispatch de shape; por ahora todos circulos. 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; }