---
name: graph_force_layout_gpu
kind: function
lang: cpp
domain: viz
version: "1.0.0"
purity: impure
signature: "ForceLayoutGPU* graph_force_layout_gpu_create(int max_nodes, int max_edges, int grid_cells_per_side); float graph_force_layout_gpu_step(ForceLayoutGPU* ctx, const ForceLayoutConfig& config)"
description: "Layout force-directed en GPU via compute shaders 4.3 + spatial hash grid. API simetrica con graph_force_layout (CPU) para swap CPU<->GPU sin cambios en el consumer"
tags: [graph, layout, force-directed, gpu, compute-shader, ssbo, spatial-hash]
uses_functions: []
uses_types: ["GraphData_cpp_viz"]
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: true
tests:
  - "smoke + decreasing energy"
  - "pinned nodes no se mueven"
  - "CPU vs GPU energia comparable"
test_file_path: "cpp/tests/test_graph_force_layout_gpu.cpp"
file_path: "cpp/functions/viz/graph_force_layout_gpu.cpp"
framework: imgui
params:
  - name: max_nodes
    desc: "Capacidad maxima de nodos (define el tamano de los SSBOs, no se redimensionan en runtime)."
  - name: max_edges
    desc: "Capacidad maxima de aristas. Para 50k nodos en clusters densos, ~10x es seguro."
  - name: grid_cells_per_side
    desc: "Resolucion del spatial hash grid (default 64 → 4096 celdas). Subir si el grafo es muy denso o el bbox crece mucho."
  - name: ctx
    desc: "Contexto opaco devuelto por _create. NULL si compute no esta disponible."
  - name: graph
    desc: "GraphData CPU. _upload lo copia a SSBOs; _readback baja positions/velocities desde GPU."
  - name: config
    desc: "Mismos parametros que la version CPU (repulsion, attraction, damping, gravity, max_velocity, iterations)."
output: "_step devuelve la energia total (sum |v|^2) tras la ultima iteracion. _create devuelve NULL si la compilacion de compute shaders falla (driver sin 4.3, Mesa sin compute)."
notes: "Requiere contexto OpenGL 4.3 core activo. Allocacion SSBOs ~80 MB para 1M nodos x 10M edges. La via rapida es no llamar a _readback si el renderer puede leer del SSBO de positions directamente (graph_force_layout_gpu_positions_ssbo)."
---

# graph_force_layout_gpu

Layout force-directed en GPU usando compute shaders 4.3. Sustituye la version Barnes-Hut en CPU para grafos grandes (50k+ nodos a 60fps con margen).

## Pipeline (5 compute shaders por step)

| Pase | local_size | Threads | Que hace |
|---|---|---|---|
| `clear`      | 64 | max(2N, G²) | Zeroes `forces[2N]`, `grid_counts[G²]`, `energy[1]` |
| `build_grid` | 64 | N           | Calcula celda por nodo, `atomicAdd(grid_counts[ci])`, escribe a `grid_cells[ci][slot]` si slot<K |
| `repulsion`  | 64 | N           | Recorre 3x3 celdas vecinas, `F = repulsion / dist²`, escribe a `forces[2*i]` (sin contention: 1 thread/nodo) |
| `attraction` | 64 | E           | Por arista, atomic-CAS float add a `forces[2*s]` y `forces[2*t]` |
| `integrate`  | 64 | N           | Si `flags & NF_PINNED` skip; `v = damping*v + F`, clamp, `x += v`, atomic-CAS add a `energy[0]` |

Entre cada pase: `glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT)`. Tras integrate añadimos `GL_BUFFER_UPDATE_BARRIER_BIT` para que `glGetBufferSubData` (energia + readback) lea valores frescos.

## Atomic add float

GL 4.3 core no tiene `atomicAdd` para floats. Patron portable usado en `attraction` e `integrate`:

```glsl
void atomic_add_float(uint idx, float value) {
    uint cur = forces[idx];
    uint expected;
    do {
        expected = cur;
        uint new_val = floatBitsToUint(uintBitsToFloat(expected) + value);
        cur = atomicCompSwap(forces[idx], expected, new_val);
    } while (cur != expected);
}
```

`forces` es `uint[2N]`: cada nodo ocupa dos uints (fx, fy) bit-casted desde float. La pasada `repulsion` no necesita atomic (cada thread es el unico que toca su slot) y escribe directamente con `floatBitsToUint`.

## Spatial hash grid

64x64 = 4096 celdas (configurable). Cada celda guarda hasta `K = 32` indices de nodos:

- `grid_counts[G²]` cuenta cuantos nodos cayeron en cada celda (atomic).
- `grid_cells[G²][K]` guarda los indices. Si una celda satura > K, los excedentes se ignoran (efecto: repulsion subestimada en zonas hiperdenas; ajustar `grid_cells_per_side` al alza).
- En `repulsion`, cada nodo lee solo las 9 celdas 3x3 alrededor de la suya → **O(N · density)** en vez de O(N log N) Barnes-Hut.

A 100k nodos con grid 64x64 y K=32 los SSBOs ocupan ~3 MB.

## API y memoria

```cpp
ForceLayoutGPU* ctx = graph_force_layout_gpu_create(max_nodes, max_edges);
if (!ctx) { /* compute no disponible — caer a graph_force_layout_step */ }

graph_force_layout_gpu_upload(ctx, graph);   // CPU→GPU una vez tras regen

for (frame = 0; frame < ...; ++frame) {
    float energy = graph_force_layout_gpu_step(ctx, cfg);
    // Opcional: solo si el consumer dibuja desde CPU mirror.
    graph_force_layout_gpu_readback(ctx, graph);
}

graph_force_layout_gpu_destroy(ctx);
```

`graph_force_layout_gpu_positions_ssbo(ctx)` devuelve el ID GL del SSBO de positions (binding 0) — el renderer puede atarlo a un TBO sin viajar por la CPU.

## Cuando NO usar este modulo

- Grafos pequeños (<2k nodos): la version CPU es ya 60fps con OpenMP y mas simple.
- Driver sin OpenGL 4.3 core: `_create` devuelve `NULL`. Hardware ~2012+ lo soporta; Mesa software (llvmpipe) tambien.
- Tests unitarios sin contexto GL: el binario de tests crea una ventana GLFW oculta; si falla, SKIPea con `WARN`. Patron similar a `test_graph_icons` con `FN_GRAPH_ICONS_SKIP_GL`.

## Toggle CPU/GPU en demos_graph

En `cpp/apps/primitives_gallery/demos_graph.cpp` hay un checkbox "GPU layout" que swappea la implementacion. Util para comparar fps y energia visualmente, y para validar que el swap es transparente para el resto del pipeline (renderer, viewport, hit-testing).

## Notas de version

- **v1.0** (2026-04-29, issue 0049h): primer release. 5 compute shaders inline, spatial hash 64x64, atomic-CAS float add, readback opcional. Toggle en `demos_graph`.
