perf(viz): graph_renderer Tier 1 (RGBA8 + orphan + frustum cull) + force_layout auto-pause helper

Issue 0049c. Tres optimizaciones internas en graph_renderer.cpp + un
helper puro en graph_force_layout para detectar convergencia. API publica
intacta — solo cambian el layout interno de los buffers, el shader y
los costes por frame.

1. RGBA8 color packing
   - El instance buffer de nodos pasa de (x,y,size,r,g,b,a) 28B a
     (x,y,size,color_u32) 16B (-43%). Aristas: 24B → 12B/vertex (-50%).
   - Shaders desempaquetan con bit shifts (compatible GL 3.30+, no
     necesita unpackUnorm4x8 que es 4.20+).
   - Helpers expuestos: pack_rgba8 / unpack_rgba8 / modulate_alpha_rgba8
     en graph_renderer.h. Los GraphNode.color y la paleta ya tenian el
     layout correcto (R en LSB), asi que CPU ahora pasa el uint32 directo
     sin convertir a 4 floats por nodo y por frame.

2. Capacity-tracked streaming buffers
   - Sustituye el doble glBufferData de antes por:
       glBufferData(NULL, capacity, STREAM_DRAW)   // orphan + reserva
       glBufferSubData(0, used_bytes, data)        // solo lo usado
   - capacity crece x2 cuando hace falta (inicial 4096 nodos /
     8192 vertices de aristas) → reallocaciones en O(log N).
   - Staging CPU (NodeInstance* / EdgeVertex*) reusado entre frames con
     realloc, no malloc/free per frame.

3. Frustum cull (CPU-side)
   - AABB del viewport en world coords con margen 10%.
   - Aristas: skip si AABB del segmento no intersecta el viewport.
   - Nodos: solo los visibles entran al instance buffer; visible_count
     es el N que pasa a glDrawArraysInstanced. Pop-in de borde mitigado
     por el margen.

4. graph_force_layout_should_pause(low_frames, min_consecutive)
   - Helper puro: el caller mantiene el contador, la funcion solo
     decide si parar. Reemplaza la rama inline en demos_graph.cpp.
   - Test Catch2 con secuencias artificiales.

Tests: test_graph_pack_rgba8 (16401 asserts, 4 cases — roundtrip exhaustivo
+ alpha modulation + clamp). test_graph_should_pause (3 cases, 14 asserts).
Los 29 tests del cpp/tests/ siguen verdes (incluido test_visual con goldens).

Bump versiones:
- graph_renderer 1.1.0 → 1.2.0
- graph_force_layout 1.0.0 → 1.1.0  (tested: true via should_pause test)
This commit is contained in:
2026-04-29 22:17:13 +02:00
parent 0e6a013937
commit 02b4141cc1
12 changed files with 437 additions and 146 deletions
+69
View File
@@ -0,0 +1,69 @@
// Unit tests for graph_renderer's RGBA8 packing helpers (cpp/functions/viz/
// graph_renderer.h). Roundtrip + alpha modulation + bit-layout match con
// unpackUnorm4x8 de GLSL (byte 0 = R, byte 3 = A) — el shader interpreta el
// uint32 sin swizzle, asi que el packing debe colocar R en el byte LSB.
#define CATCH_CONFIG_MAIN
#include "catch_amalgamated.hpp"
#include "viz/graph_renderer.h"
#include <cstdint>
TEST_CASE("pack_rgba8 places R in the LSB byte", "[viz][rgba8]") {
uint32_t c = pack_rgba8(0x12, 0x34, 0x56, 0x78);
REQUIRE(((c ) & 0xFFu) == 0x12u); // R
REQUIRE(((c >> 8) & 0xFFu) == 0x34u); // G
REQUIRE(((c >> 16) & 0xFFu) == 0x56u); // B
REQUIRE(((c >> 24) & 0xFFu) == 0x78u); // A
}
TEST_CASE("pack/unpack roundtrip is exact for arbitrary bytes", "[viz][rgba8]") {
const uint8_t samples[] = { 0x00, 0x01, 0x7F, 0x80, 0xAB, 0xCD, 0xFE, 0xFF };
for (uint8_t r : samples) for (uint8_t g : samples)
for (uint8_t b : samples) for (uint8_t a : samples) {
uint32_t c = pack_rgba8(r, g, b, a);
uint8_t r2, g2, b2, a2;
unpack_rgba8(c, r2, g2, b2, a2);
REQUIRE(r == r2);
REQUIRE(g == g2);
REQUIRE(b == b2);
REQUIRE(a == a2);
}
}
TEST_CASE("modulate_alpha_rgba8 preserves RGB and scales alpha", "[viz][rgba8]") {
uint32_t opaque = pack_rgba8(0x10, 0x20, 0x30, 0xFF);
// Full pass-through: scale=1.0 -> alpha=255
REQUIRE(modulate_alpha_rgba8(opaque, 1.0f) == opaque);
// Half: alpha goes to 128 (255 * 0.5 + 0.5 = 128)
uint32_t half = modulate_alpha_rgba8(opaque, 0.5f);
uint8_t r, g, b, a;
unpack_rgba8(half, r, g, b, a);
REQUIRE(r == 0x10);
REQUIRE(g == 0x20);
REQUIRE(b == 0x30);
REQUIRE(a == 128);
// Zero: alpha goes to 0
uint32_t zero = modulate_alpha_rgba8(opaque, 0.0f);
unpack_rgba8(zero, r, g, b, a);
REQUIRE(a == 0);
// RGB intactos
REQUIRE(r == 0x10);
REQUIRE(g == 0x20);
REQUIRE(b == 0x30);
}
TEST_CASE("modulate_alpha_rgba8 clamps overflow to 255", "[viz][rgba8]") {
uint32_t c = pack_rgba8(1, 2, 3, 200);
uint32_t out = modulate_alpha_rgba8(c, 5.0f); // 200*5 = 1000, clamp 255
uint8_t r, g, b, a;
unpack_rgba8(out, r, g, b, a);
REQUIRE(a == 255);
REQUIRE(r == 1);
REQUIRE(g == 2);
REQUIRE(b == 3);
}