9c26c3917c
Layout force-directed en GPU usando 5 compute shaders 4.3 + spatial hash
grid 64x64. API simetrica con graph_force_layout (CPU) para que el consumer
pueda swappear sin cambios. atomicCompSwap loop para float-add portable.
- cpp/functions/viz/graph_force_layout_gpu.{h,cpp,md}: nuevo modulo
- cpp/functions/gfx/gl_loader: anade glDispatchCompute, glMemoryBarrier,
glBindBufferBase, glGetBufferSubData (Windows wgl)
- cpp/tests/test_graph_force_layout_gpu.cpp: smoke + pinned + CPU vs GPU.
Crea ventana GLFW oculta GL 4.3; SKIP si headless o sin compute.
- demos_graph: checkbox "GPU layout" para swappear CPU/GPU en runtime
- issue movido a dev/issues/completed/
317 lines
13 KiB
C++
317 lines
13 KiB
C++
#include "demos.h"
|
|
#include "demo.h"
|
|
|
|
#include "viz/graph_types.h"
|
|
#include "viz/graph_viewport.h"
|
|
#include "viz/graph_force_layout.h"
|
|
#include "viz/graph_force_layout_gpu.h"
|
|
#include "core/button.h"
|
|
#include "core/tokens.h"
|
|
|
|
#include <imgui.h>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <vector>
|
|
|
|
namespace gallery {
|
|
|
|
// Paleta del demo: 8 colores tipo Mantine. v2.0 los usamos a traves de la
|
|
// tabla EntityType en lugar de escribirlos por nodo. Asi el modelo nuevo
|
|
// queda demostrado tal cual lo van a usar las apps reales (osint_graph,
|
|
// fn_explorer): tabla pequena de tipos + nodos que solo guardan type_id.
|
|
static const uint32_t k_demo_palette[] = {
|
|
0xFFEF8D5Bu, 0xFF8CCA58u, 0xFF3E97F5u, 0xFF5051D9u,
|
|
0xFFE07FB8u, 0xFFCCCD5Fu, 0xFF52CDF2u, 0xFF61D199u,
|
|
};
|
|
static constexpr int k_demo_palette_n =
|
|
sizeof(k_demo_palette) / sizeof(k_demo_palette[0]);
|
|
|
|
// Tabla compartida entre regeneraciones — las apariencias no cambian aunque
|
|
// el usuario regenere el grafo, asi que vive como `static`.
|
|
static EntityType s_demo_entity_types[k_demo_palette_n];
|
|
static RelationType s_demo_relation_types[1];
|
|
static bool s_demo_types_initialized = false;
|
|
|
|
static void init_demo_types() {
|
|
if (s_demo_types_initialized) return;
|
|
for (int k = 0; k < k_demo_palette_n; ++k) {
|
|
s_demo_entity_types[k] = entity_type(k_demo_palette[k],
|
|
SHAPE_CIRCLE, 4.0f, "cluster");
|
|
}
|
|
s_demo_relation_types[0] = relation_type(0xFF888888u, EDGE_SOLID, 1.0f, "default");
|
|
s_demo_types_initialized = true;
|
|
}
|
|
|
|
// Genera un grafo sintetico con N nodos en K clusters. Cada nodo tiene
|
|
// `edges_per_node` aristas intra-cluster + un pct% global inter-cluster.
|
|
// Cluster radio escala con sqrt(N) para que la "nube" no sea siempre el
|
|
// mismo cuadrado de 200 px — a 1M nodos crece a ~6 km de radio en graph
|
|
// space y los nodos pueden esparcirse libremente sin caja artificial.
|
|
static void generate_synthetic_graph(int N, int K,
|
|
int edges_per_node, int inter_pct,
|
|
std::vector<GraphNode>& nodes_out,
|
|
std::vector<GraphEdge>& edges_out) {
|
|
nodes_out.clear();
|
|
edges_out.clear();
|
|
nodes_out.reserve(N);
|
|
edges_out.reserve((size_t)N * (size_t)edges_per_node + (size_t)N * (size_t)inter_pct / 100u);
|
|
|
|
unsigned seed = 0x1234abcd;
|
|
auto rnd = [&]() {
|
|
seed = seed * 1664525u + 1013904223u;
|
|
return static_cast<float>((seed >> 8) & 0xffffff) / 16777216.0f;
|
|
};
|
|
|
|
// Cluster radius y scatter escalan con sqrt(N) para que los nodos no
|
|
// queden empaquetados al subir el slider. A 1M nodes el espacio inicial
|
|
// es ~12k px de lado en lugar de los 280 px hardcoded de antes.
|
|
const float scale = std::sqrt(static_cast<float>(std::max(N, 1)));
|
|
const float cluster_r = 12.0f * scale;
|
|
const float scatter = 4.0f * scale;
|
|
|
|
std::vector<float> cluster_cx(K), cluster_cy(K);
|
|
for (int k = 0; k < K; k++) {
|
|
float angle = 2.0f * 3.14159f * k / K;
|
|
cluster_cx[k] = std::cos(angle) * cluster_r;
|
|
cluster_cy[k] = std::sin(angle) * cluster_r;
|
|
}
|
|
|
|
for (int i = 0; i < N; i++) {
|
|
int k = i % K;
|
|
// type_id mapea al EntityType (k % k_demo_palette_n) que define
|
|
// color y shape. size_override = 3..5 px para conservar la
|
|
// variacion sutil del demo v1 — apariencia visual identica.
|
|
uint16_t tid = static_cast<uint16_t>(k % k_demo_palette_n);
|
|
GraphNode n = graph_node(
|
|
cluster_cx[k] + (rnd() - 0.5f) * scatter,
|
|
cluster_cy[k] + (rnd() - 0.5f) * scatter,
|
|
tid);
|
|
n.size_override = 3.0f + rnd() * 2.0f;
|
|
n.user_data = static_cast<uint64_t>(i);
|
|
nodes_out.push_back(n);
|
|
}
|
|
|
|
auto add_edge = [&](uint32_t a, uint32_t b, float w) {
|
|
if (a == b) return;
|
|
edges_out.push_back(graph_edge(a, b, w));
|
|
};
|
|
int per_cluster = N / K;
|
|
for (int k = 0; k < K; k++) {
|
|
int base = k * per_cluster;
|
|
int end = (k == K - 1) ? N : (base + per_cluster);
|
|
int size = end - base;
|
|
if (size < 2) continue;
|
|
for (int i = base; i < end; i++) {
|
|
for (int e = 0; e < edges_per_node; e++) {
|
|
int j = base + static_cast<int>(rnd() * size);
|
|
add_edge(static_cast<uint32_t>(i),
|
|
static_cast<uint32_t>(j), 1.0f);
|
|
}
|
|
}
|
|
}
|
|
// Inter-cluster: pct% del total de nodos
|
|
long long inter = (long long)N * (long long)inter_pct / 100LL;
|
|
for (long long e = 0; e < inter; e++) {
|
|
uint32_t a = static_cast<uint32_t>(rnd() * N);
|
|
uint32_t b = static_cast<uint32_t>(rnd() * N);
|
|
add_edge(a, b, 0.3f);
|
|
}
|
|
}
|
|
|
|
void demo_graph() {
|
|
demo_header("graph_viewport", "v1.0.0",
|
|
"Pipeline completo de visualizacion de grafos: graph_renderer (instanced GPU) "
|
|
"+ graph_force_layout (Barnes-Hut) + graph_spatial_hash (hit-testing). "
|
|
"Render a FBO mostrado via ImGui::Image — escala a decenas de miles de nodos.");
|
|
|
|
static int s_n_nodes = 1000;
|
|
static int s_n_clusters = 6;
|
|
static int s_edges_per_n = 3; // aristas intra-cluster por nodo
|
|
static int s_inter_pct = 5; // % de nodos para edges inter-cluster
|
|
static float s_repulsion = 3500.0f; // fuerza de dispersion entre nodos
|
|
static float s_attraction = 0.02f; // muelle entre nodos conectados
|
|
static float s_gravity = 0.001f; // tiron hacia el centro
|
|
static std::vector<GraphNode> s_nodes;
|
|
static std::vector<GraphEdge> s_edges;
|
|
static GraphData s_graph{};
|
|
static GraphViewportState s_state;
|
|
static bool s_initialized = false;
|
|
static bool s_needs_regen = true;
|
|
|
|
// GPU layout (issue 0049h): toggle CPU/GPU. ctx se crea perezosamente al
|
|
// primer frame en GPU mode; max_nodes/max_edges se dimensionan al maximo
|
|
// que ofrece el slider (1M nodos x 10 edges/nodo = 10M edges) — los SSBOs
|
|
// ocupan ~80 MB en ese tope, suficientemente barato para no
|
|
// recrear el ctx cada Regenerate. Si compute no esta disponible, el
|
|
// toggle queda deshabilitado.
|
|
static bool s_use_gpu = false;
|
|
static ForceLayoutGPU* s_gpu_ctx = nullptr;
|
|
static bool s_gpu_dirty = true; // re-upload tras regen / cambio
|
|
|
|
if (s_needs_regen) {
|
|
init_demo_types();
|
|
generate_synthetic_graph(s_n_nodes, s_n_clusters,
|
|
s_edges_per_n, s_inter_pct,
|
|
s_nodes, s_edges);
|
|
s_graph.nodes = s_nodes.data();
|
|
s_graph.node_count = static_cast<int>(s_nodes.size());
|
|
s_graph.node_capacity = static_cast<int>(s_nodes.capacity());
|
|
s_graph.edges = s_edges.data();
|
|
s_graph.edge_count = static_cast<int>(s_edges.size());
|
|
s_graph.edge_capacity = static_cast<int>(s_edges.capacity());
|
|
s_graph.types = s_demo_entity_types;
|
|
s_graph.type_count = k_demo_palette_n;
|
|
s_graph.rel_types = s_demo_relation_types;
|
|
s_graph.rel_type_count = 1;
|
|
s_graph.update_bounds();
|
|
s_state.layout_running = true;
|
|
s_state.layout_energy = 0.0f;
|
|
s_needs_regen = false;
|
|
s_initialized = true;
|
|
s_gpu_dirty = true;
|
|
}
|
|
|
|
section("Controls");
|
|
{
|
|
using namespace fn_ui;
|
|
ImGui::PushItemWidth(180);
|
|
// Slider Nodes con escala logaritmica para que sea util tanto a 100
|
|
// como a 1M sin tener que arrastrar 10000px.
|
|
ImGui::SliderInt("Nodes", &s_n_nodes, 100, 1000000, "%d",
|
|
ImGuiSliderFlags_Logarithmic);
|
|
ImGui::SameLine();
|
|
ImGui::SliderInt("Clusters", &s_n_clusters, 2, 16);
|
|
ImGui::SliderInt("Edges/node", &s_edges_per_n, 1, 10);
|
|
ImGui::SameLine();
|
|
ImGui::SliderInt("Inter %", &s_inter_pct, 0, 30, "%d%%");
|
|
ImGui::SliderFloat("Repulsion", &s_repulsion, 100.0f, 20000.0f, "%.0f");
|
|
ImGui::SameLine();
|
|
ImGui::SliderFloat("Attraction", &s_attraction, 0.001f, 0.5f, "%.3f");
|
|
ImGui::SameLine();
|
|
ImGui::SliderFloat("Gravity", &s_gravity, 0.0f, 0.05f, "%.4f");
|
|
ImGui::PopItemWidth();
|
|
|
|
if (button("Regenerate", ButtonVariant::Primary)) s_needs_regen = true;
|
|
ImGui::SameLine();
|
|
if (button(s_state.layout_running ? "Pause layout" : "Resume layout",
|
|
ButtonVariant::Secondary)) {
|
|
s_state.layout_running = !s_state.layout_running;
|
|
}
|
|
ImGui::SameLine();
|
|
if (button("Fit view", ButtonVariant::Subtle)) {
|
|
graph_viewport_fit(s_graph, s_state);
|
|
}
|
|
ImGui::SameLine();
|
|
// Toggle GPU layout. Si compute no esta disponible (Mesa software o
|
|
// driver < 4.3), deshabilitamos visualmente el checkbox.
|
|
bool prev_gpu = s_use_gpu;
|
|
if (s_gpu_ctx == nullptr && s_use_gpu == false) {
|
|
// primera oportunidad: intentar crear el ctx para detectar soporte.
|
|
// Lazy init solo si el usuario lo activa.
|
|
}
|
|
ImGui::Checkbox("GPU layout", &s_use_gpu);
|
|
if (s_use_gpu != prev_gpu) {
|
|
s_gpu_dirty = true; // re-upload al cambiar de modo
|
|
}
|
|
}
|
|
|
|
section("Stats");
|
|
{
|
|
// Una sola linea fija — sin secciones condicionales que cambien la
|
|
// altura del panel (eso provocaba que el viewport saltara al hacer
|
|
// hover/select).
|
|
char hover_buf[32];
|
|
char sel_buf[32];
|
|
if (s_state.hovered_node >= 0) {
|
|
std::snprintf(hover_buf, sizeof(hover_buf), "#%d t%u",
|
|
s_state.hovered_node,
|
|
(unsigned)s_nodes[s_state.hovered_node].type_id);
|
|
} else {
|
|
std::snprintf(hover_buf, sizeof(hover_buf), "-");
|
|
}
|
|
if (s_state.selected_node >= 0) {
|
|
std::snprintf(sel_buf, sizeof(sel_buf), "#%d", s_state.selected_node);
|
|
} else {
|
|
std::snprintf(sel_buf, sizeof(sel_buf), "-");
|
|
}
|
|
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
|
|
ImGui::Text("nodes=%d edges=%d energy=%.2f fps=%.0f | hover=%s sel=%s",
|
|
s_graph.node_count, s_graph.edge_count,
|
|
s_state.layout_energy, ImGui::GetIO().Framerate,
|
|
hover_buf, sel_buf);
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
section("Viewport (drag = pan, wheel = zoom, click = select)");
|
|
if (s_initialized) {
|
|
// Avanzamos 1 paso de force layout cada frame mientras layout_running.
|
|
// Auto-pause: si la energia por nodo cae bajo el umbral durante N
|
|
// frames consecutivos, paramos la simulacion automaticamente — el
|
|
// grafo ya esta estable. El usuario lo retoma con "Resume layout"
|
|
// o "Regenerate".
|
|
static int s_low_energy_frames = 0;
|
|
const int k_pause_after_frames = 30;
|
|
const float k_pause_per_node = 0.001f; // umbral de energia/nodo
|
|
if (s_state.layout_running) {
|
|
ForceLayoutConfig cfg;
|
|
cfg.repulsion = s_repulsion;
|
|
cfg.attraction = s_attraction;
|
|
cfg.gravity = s_gravity;
|
|
cfg.iterations = 1;
|
|
if (s_use_gpu) {
|
|
if (!s_gpu_ctx) {
|
|
s_gpu_ctx = graph_force_layout_gpu_create(s_graph.node_count + 1024,
|
|
s_graph.edge_count + 1024);
|
|
s_gpu_dirty = true;
|
|
}
|
|
if (s_gpu_ctx) {
|
|
if (s_gpu_dirty) {
|
|
graph_force_layout_gpu_upload(s_gpu_ctx, s_graph);
|
|
s_gpu_dirty = false;
|
|
}
|
|
s_state.layout_energy = graph_force_layout_gpu_step(s_gpu_ctx, cfg);
|
|
graph_force_layout_gpu_readback(s_gpu_ctx, s_graph, /*include_velocities=*/true);
|
|
} else {
|
|
// GPU no disponible: caer a CPU silenciosamente.
|
|
s_use_gpu = false;
|
|
s_state.layout_energy = graph_force_layout_step(s_graph, cfg);
|
|
}
|
|
} else {
|
|
s_state.layout_energy = graph_force_layout_step(s_graph, cfg);
|
|
}
|
|
|
|
const float per_node = s_graph.node_count > 0
|
|
? s_state.layout_energy / (float)s_graph.node_count
|
|
: 0.0f;
|
|
if (per_node < k_pause_per_node) ++s_low_energy_frames;
|
|
else s_low_energy_frames = 0;
|
|
|
|
if (graph_force_layout_should_pause(s_low_energy_frames,
|
|
k_pause_after_frames)) {
|
|
s_state.layout_running = false;
|
|
s_low_energy_frames = 0;
|
|
}
|
|
} else {
|
|
s_low_energy_frames = 0;
|
|
}
|
|
graph_viewport("##graph_demo", s_graph, s_state, ImVec2(0, 460));
|
|
}
|
|
|
|
code_block(
|
|
"static GraphData graph;\n"
|
|
"static GraphViewportState state;\n"
|
|
"// ... rellenar graph.nodes / graph.edges ...\n"
|
|
"graph.update_bounds();\n"
|
|
"\n"
|
|
"// Por frame:\n"
|
|
"if (state.layout_running) {\n"
|
|
" ForceLayoutConfig cfg;\n"
|
|
" cfg.repulsion = 3500; cfg.gravity = 0.001f;\n"
|
|
" graph_force_layout_step(graph, cfg);\n"
|
|
"}\n"
|
|
"graph_viewport(\"##g\", graph, state, ImVec2(0, 460));"
|
|
);
|
|
}
|
|
|
|
} // namespace gallery
|