docs(issues): marcar 0025 y 0026 como completados + WIP master

Wave 1 de parallel-fix-issues integrada a master:
- 0025: text_editor_cpp_core + file_watcher_cpp_core
- 0026: gl_texture_load_cpp_gfx (vendor: stb_image v2.30)

Ademas se commitea WIP previo de master que estaba sin commitear (cambios
en shaders_lab, dag_*, framework, tokens, kpi_card, gl_loader.md, etc.)
para dejar HEAD buildable.

Notas:
- Algunos deps del gallery (button.cpp, toolbar.cpp, modal_dialog.cpp...)
  siguen UNTRACKED — gating con FN_BUILD_GALLERY=ON (default OFF) para
  que master build (sin flag) no los necesite.
- Build OK con y sin flag. fn index registra 904 functions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-25 21:11:26 +02:00
parent d3d5af51f2
commit b093c898a8
37 changed files with 1819 additions and 342 deletions
+299 -24
View File
@@ -1,9 +1,10 @@
#include "gfx/dag_catalog.h"
#include <algorithm>
#include <string>
namespace fn::gfx {
static const std::vector<DagNodeDef>& build_catalog() {
static std::vector<DagNodeDef>& mutable_catalog() {
static std::vector<DagNodeDef> catalog = []() {
std::vector<DagNodeDef> v;
@@ -15,8 +16,8 @@ static const std::vector<DagNodeDef>& build_catalog() {
n.desc = "color constante";
n.kind = DagKind::Gen;
n.num_inputs = 0;
n.param_names = {"r", "g", "b", ""};
n.param_defaults = {0.35f, 0.25f, 0.55f, 0.0f};
n.param_names = {"r", "g", "b"};
n.param_defaults = {0.35f, 0.25f, 0.55f};
n.controls = {
{ DagControl::Kind::Color, "color", {0, 1, 2}, 0.0f, 1.0f, 0.0f },
};
@@ -36,8 +37,8 @@ static const std::vector<DagNodeDef>& build_catalog() {
n.desc = "gradiente direccional";
n.kind = DagKind::Gen;
n.num_inputs = 0;
n.param_names = {"angle", "hue", "", ""};
n.param_defaults = {0.8f, 0.5f, 0.0f, 0.0f};
n.param_names = {"angle", "hue"};
n.param_defaults = {0.8f, 0.5f};
n.controls = {
{ DagControl::Kind::Slider, "angulo", {0, -1, -1}, 0.0f, 6.2832f, 0.01f },
{ DagControl::Kind::Slider, "tono", {1, -1, -1}, 0.0f, 1.0f, 0.01f },
@@ -61,8 +62,8 @@ static const std::vector<DagNodeDef>& build_catalog() {
n.desc = "onda trigonometrica";
n.kind = DagKind::Gen;
n.num_inputs = 0;
n.param_names = {"speed", "scale", "", ""};
n.param_defaults = {1.0f, 2.0f, 0.0f, 0.0f};
n.param_names = {"speed", "scale"};
n.param_defaults = {1.0f, 2.0f};
n.controls = {
{ DagControl::Kind::Slider, "velocidad", {0, -1, -1}, 0.0f, 3.0f, 0.01f },
{ DagControl::Kind::Slider, "escala", {1, -1, -1}, 0.5f, 10.0f, 0.1f },
@@ -112,8 +113,8 @@ static const std::vector<DagNodeDef>& build_catalog() {
n.desc = "1 - rgb";
n.kind = DagKind::Op;
n.num_inputs = 1;
n.param_names = {"", "", "", ""};
n.param_defaults = {0.0f, 0.0f, 0.0f, 0.0f};
n.param_names = {};
n.param_defaults = {};
n.controls = {};
n.body_glsl = [](int /*idx*/) -> std::string {
return " return vec4(1.0 - a.rgb, a.a);";
@@ -129,8 +130,8 @@ static const std::vector<DagNodeDef>& build_catalog() {
n.desc = "pow(rgb, gamma)";
n.kind = DagKind::Op;
n.num_inputs = 1;
n.param_names = {"gamma", "", "", ""};
n.param_defaults = {1.0f, 0.0f, 0.0f, 0.0f};
n.param_names = {"gamma"};
n.param_defaults = {1.0f};
n.controls = {
{ DagControl::Kind::Slider, "gamma", {0, -1, -1}, 0.1f, 4.0f, 0.01f },
};
@@ -150,8 +151,8 @@ static const std::vector<DagNodeDef>& build_catalog() {
n.desc = "rotar matiz";
n.kind = DagKind::Op;
n.num_inputs = 1;
n.param_names = {"h", "", "", ""};
n.param_defaults = {0.0f, 0.0f, 0.0f, 0.0f};
n.param_names = {"h"};
n.param_defaults = {0.0f};
n.controls = {
{ DagControl::Kind::Slider, "h", {0, -1, -1}, 0.0f, 1.0f, 0.01f },
};
@@ -178,8 +179,8 @@ static const std::vector<DagNodeDef>& build_catalog() {
n.desc = "interpolacion mix(a, b, t)";
n.kind = DagKind::Blend;
n.num_inputs = 2;
n.param_names = {"t", "", "", ""};
n.param_defaults = {0.5f, 0.0f, 0.0f, 0.0f};
n.param_names = {"t"};
n.param_defaults = {0.5f};
n.controls = {
{ DagControl::Kind::Slider, "t", {0, -1, -1}, 0.0f, 1.0f, 0.01f },
};
@@ -199,8 +200,8 @@ static const std::vector<DagNodeDef>& build_catalog() {
n.desc = "a * b";
n.kind = DagKind::Blend;
n.num_inputs = 2;
n.param_names = {"", "", "", ""};
n.param_defaults = {0.0f, 0.0f, 0.0f, 0.0f};
n.param_names = {};
n.param_defaults = {};
n.controls = {};
n.body_glsl = [](int /*idx*/) -> std::string {
return " return vec4(a.rgb * b.rgb, a.a);";
@@ -216,8 +217,8 @@ static const std::vector<DagNodeDef>& build_catalog() {
n.desc = "1 - (1-a)(1-b)";
n.kind = DagKind::Blend;
n.num_inputs = 2;
n.param_names = {"", "", "", ""};
n.param_defaults = {0.0f, 0.0f, 0.0f, 0.0f};
n.param_names = {};
n.param_defaults = {};
n.controls = {};
n.body_glsl = [](int /*idx*/) -> std::string {
return " return vec4(1.0 - (1.0 - a.rgb) * (1.0 - b.rgb), a.a);";
@@ -225,6 +226,253 @@ static const std::vector<DagNodeDef>& build_catalog() {
v.push_back(std::move(n));
}
// ── Gen: checker ──────────────────────────────────────────────
{
DagNodeDef n;
n.name = "checker";
n.label = "checker";
n.desc = "tablero animado";
n.kind = DagKind::Gen;
n.num_inputs = 0;
n.param_names = {"scale", "rotation", "hue", "speed"};
n.param_defaults = {8.0f, 0.0f, 0.55f, 0.2f};
n.controls = {
{ DagControl::Kind::Slider, "escala", {0, -1, -1}, 1.0f, 32.0f, 0.1f },
{ DagControl::Kind::Slider, "rotacion", {1, -1, -1}, 0.0f, 6.2832f, 0.01f },
{ DagControl::Kind::Slider, "tono", {2, -1, -1}, 0.0f, 1.0f, 0.01f },
{ DagControl::Kind::Slider, "velocidad",{3, -1, -1}, 0.0f, 3.0f, 0.01f },
};
n.body_glsl = [](int idx) -> std::string {
std::string i = std::to_string(idx);
return " vec4 p = u_params[" + i + "];\n"
" float ang = p.y + u_time * p.w * 0.2;\n"
" float ca = cos(ang), sa = sin(ang);\n"
" vec2 q = uv - 0.5;\n"
" q = vec2(ca*q.x - sa*q.y, sa*q.x + ca*q.y) * p.x + 0.5;\n"
" vec2 cell = floor(q);\n"
" float c = mod(cell.x + cell.y, 2.0);\n"
" vec3 ca_col = 0.5 + 0.5 * cos(6.28318 * (p.z + vec3(0.0, 0.33, 0.67)));\n"
" vec3 cb_col = 0.5 + 0.5 * cos(6.28318 * (p.z + 0.5 + vec3(0.0, 0.33, 0.67)));\n"
" return vec4(mix(cb_col, ca_col, c), 1.0);";
};
v.push_back(std::move(n));
}
// ── Gen: stripes ──────────────────────────────────────────────
{
DagNodeDef n;
n.name = "stripes";
n.label = "stripes";
n.desc = "bandas direccionales";
n.kind = DagKind::Gen;
n.num_inputs = 0;
n.param_names = {"angle", "freq", "phase_speed", "hue"};
n.param_defaults = {0.785f, 12.0f, 1.0f, 0.6f};
n.controls = {
{ DagControl::Kind::Slider, "angulo", {0, -1, -1}, 0.0f, 6.2832f, 0.01f },
{ DagControl::Kind::Slider, "frecuencia", {1, -1, -1}, 1.0f, 64.0f, 0.5f },
{ DagControl::Kind::Slider, "velocidad", {2, -1, -1}, 0.0f, 5.0f, 0.01f },
{ DagControl::Kind::Slider, "tono", {3, -1, -1}, 0.0f, 1.0f, 0.01f },
};
n.body_glsl = [](int idx) -> std::string {
std::string i = std::to_string(idx);
return " vec4 p = u_params[" + i + "];\n"
" vec2 dir = vec2(cos(p.x), sin(p.x));\n"
" float t = dot(uv - 0.5, dir) * p.y + u_time * p.z;\n"
" float s = 0.5 + 0.5 * sin(t);\n"
" vec3 col = 0.5 + 0.5 * cos(6.28318 * (p.w + s + vec3(0.0, 0.33, 0.67)));\n"
" return vec4(col, 1.0);";
};
v.push_back(std::move(n));
}
// ── Gen: dots ─────────────────────────────────────────────────
{
DagNodeDef n;
n.name = "dots";
n.label = "dots";
n.desc = "rejilla de puntos";
n.kind = DagKind::Gen;
n.num_inputs = 0;
n.param_names = {"scale", "radius", "soft", "hue"};
n.param_defaults = {16.0f, 0.3f, 0.05f, 0.7f};
n.controls = {
{ DagControl::Kind::Slider, "escala", {0, -1, -1}, 1.0f, 64.0f, 0.5f },
{ DagControl::Kind::Slider, "radio", {1, -1, -1}, 0.0f, 0.5f, 0.01f },
{ DagControl::Kind::Slider, "suavidad", {2, -1, -1}, 0.001f, 0.2f, 0.001f },
{ DagControl::Kind::Slider, "tono", {3, -1, -1}, 0.0f, 1.0f, 0.01f },
};
n.body_glsl = [](int idx) -> std::string {
std::string i = std::to_string(idx);
return " vec4 p = u_params[" + i + "];\n"
" float aspect = u_resolution.x / u_resolution.y;\n"
" vec2 q = vec2((uv.x - 0.5) * aspect, uv.y - 0.5) * p.x;\n"
" vec2 cell = fract(q) - 0.5;\n"
" float d = length(cell) - p.y;\n"
" float fill = smoothstep(p.z, -p.z, d);\n"
" vec3 col = 0.5 + 0.5 * cos(6.28318 * (p.w + vec3(0.0, 0.33, 0.67)));\n"
" return vec4(col * fill, 1.0);";
};
v.push_back(std::move(n));
}
// ── Gen: rings ────────────────────────────────────────────────
{
DagNodeDef n;
n.name = "rings";
n.label = "rings";
n.desc = "anillos concentricos";
n.kind = DagKind::Gen;
n.num_inputs = 0;
n.param_names = {"cx", "cy", "freq", "speed"};
n.param_defaults = {0.0f, 0.0f, 30.0f, 2.0f};
n.controls = {
{ DagControl::Kind::XY, "centro", {0, 1, -1}, -0.5f, 0.5f, 0.01f },
{ DagControl::Kind::Slider, "frecuencia",{2, -1, -1}, 1.0f, 100.0f, 0.5f },
{ DagControl::Kind::Slider, "velocidad", {3, -1, -1}, 0.0f, 10.0f, 0.05f },
};
n.body_glsl = [](int idx) -> std::string {
std::string i = std::to_string(idx);
return " vec4 p = u_params[" + i + "];\n"
" float aspect = u_resolution.x / u_resolution.y;\n"
" vec2 q = vec2((uv.x - 0.5) * aspect - p.x, uv.y - 0.5 - p.y);\n"
" float d = length(q);\n"
" float r = 0.5 + 0.5 * sin(d * p.z - u_time * p.w);\n"
" return vec4(vec3(r), 1.0);";
};
v.push_back(std::move(n));
}
// ── Gen: polar_rays ───────────────────────────────────────────
{
DagNodeDef n;
n.name = "polar_rays";
n.label = "polar rays";
n.desc = "rayos radiales";
n.kind = DagKind::Gen;
n.num_inputs = 0;
n.param_names = {"cx", "cy", "count", "speed"};
n.param_defaults = {0.0f, 0.0f, 12.0f, 0.5f};
n.controls = {
{ DagControl::Kind::XY, "centro", {0, 1, -1}, -0.5f, 0.5f, 0.01f },
{ DagControl::Kind::Slider, "rayos", {2, -1, -1}, 1.0f, 64.0f, 1.0f },
{ DagControl::Kind::Slider, "velocidad", {3, -1, -1}, -3.0f, 3.0f, 0.01f },
};
n.body_glsl = [](int idx) -> std::string {
std::string i = std::to_string(idx);
return " vec4 p = u_params[" + i + "];\n"
" float aspect = u_resolution.x / u_resolution.y;\n"
" vec2 q = vec2((uv.x - 0.5) * aspect - p.x, uv.y - 0.5 - p.y);\n"
" float a = atan(q.y, q.x);\n"
" float r = 0.5 + 0.5 * sin(a * p.z + u_time * p.w);\n"
" return vec4(vec3(r), 1.0);";
};
v.push_back(std::move(n));
}
// ── Gen: noise_value ──────────────────────────────────────────
{
DagNodeDef n;
n.name = "noise_value";
n.label = "noise value";
n.desc = "value noise 2D";
n.kind = DagKind::Gen;
n.num_inputs = 0;
n.param_names = {"scale", "speed", "hue"};
n.param_defaults = {6.0f, 0.3f, 0.5f};
n.controls = {
{ DagControl::Kind::Slider, "escala", {0, -1, -1}, 0.5f, 32.0f, 0.1f },
{ DagControl::Kind::Slider, "velocidad", {1, -1, -1}, 0.0f, 3.0f, 0.01f },
{ DagControl::Kind::Slider, "tono", {2, -1, -1}, 0.0f, 1.0f, 0.01f },
};
n.body_glsl = [](int idx) -> std::string {
std::string i = std::to_string(idx);
return " vec4 p = u_params[" + i + "];\n"
" vec2 q = uv * p.x + u_time * p.y;\n"
" vec2 fl = floor(q);\n"
" vec2 fr = fract(q);\n"
" fr = fr * fr * (3.0 - 2.0 * fr);\n"
" float a = fract(sin(dot(fl + vec2(0.0, 0.0), vec2(12.9898, 78.233))) * 43758.5453);\n"
" float b = fract(sin(dot(fl + vec2(1.0, 0.0), vec2(12.9898, 78.233))) * 43758.5453);\n"
" float c = fract(sin(dot(fl + vec2(0.0, 1.0), vec2(12.9898, 78.233))) * 43758.5453);\n"
" float d = fract(sin(dot(fl + vec2(1.0, 1.0), vec2(12.9898, 78.233))) * 43758.5453);\n"
" float n = mix(mix(a, b, fr.x), mix(c, d, fr.x), fr.y);\n"
" vec3 col = 0.5 + 0.5 * cos(6.28318 * (p.z + n + vec3(0.0, 0.33, 0.67)));\n"
" return vec4(col, 1.0);";
};
v.push_back(std::move(n));
}
// ── Gen: voronoi ──────────────────────────────────────────────
{
DagNodeDef n;
n.name = "voronoi";
n.label = "voronoi";
n.desc = "celdas voronoi";
n.kind = DagKind::Gen;
n.num_inputs = 0;
n.param_names = {"scale", "speed", "hue"};
n.param_defaults = {8.0f, 0.5f, 0.4f};
n.controls = {
{ DagControl::Kind::Slider, "escala", {0, -1, -1}, 1.0f, 32.0f, 0.1f },
{ DagControl::Kind::Slider, "velocidad", {1, -1, -1}, 0.0f, 3.0f, 0.01f },
{ DagControl::Kind::Slider, "tono", {2, -1, -1}, 0.0f, 1.0f, 0.01f },
};
n.body_glsl = [](int idx) -> std::string {
std::string i = std::to_string(idx);
return " vec4 p = u_params[" + i + "];\n"
" vec2 q = uv * p.x;\n"
" vec2 fl = floor(q);\n"
" vec2 fr = fract(q);\n"
" float md = 1.0;\n"
" for (int yy = -1; yy <= 1; yy++) {\n"
" for (int xx = -1; xx <= 1; xx++) {\n"
" vec2 nb = vec2(float(xx), float(yy));\n"
" vec2 r = fract(sin(dot(fl + nb, vec2(127.1, 311.7))) * vec2(43758.5453, 22578.1459)) * vec2(1.0);\n"
" vec2 pt = nb + 0.5 + 0.5 * sin(u_time * p.y + 6.2831 * r) - fr;\n"
" md = min(md, dot(pt, pt));\n"
" }\n"
" }\n"
" float d = sqrt(md);\n"
" vec3 col = 0.5 + 0.5 * cos(6.28318 * (p.z + d + vec3(0.0, 0.33, 0.67)));\n"
" return vec4(col, 1.0);";
};
v.push_back(std::move(n));
}
// ── Gen: truchet ──────────────────────────────────────────────
{
DagNodeDef n;
n.name = "truchet";
n.label = "truchet";
n.desc = "patron truchet curvo";
n.kind = DagKind::Gen;
n.num_inputs = 0;
n.param_names = {"scale", "thickness", "hue"};
n.param_defaults = {10.0f, 0.15f, 0.3f};
n.controls = {
{ DagControl::Kind::Slider, "escala", {0, -1, -1}, 1.0f, 40.0f, 0.5f },
{ DagControl::Kind::Slider, "grosor", {1, -1, -1}, 0.02f, 0.45f, 0.01f },
{ DagControl::Kind::Slider, "tono", {2, -1, -1}, 0.0f, 1.0f, 0.01f },
};
n.body_glsl = [](int idx) -> std::string {
std::string i = std::to_string(idx);
return " vec4 p = u_params[" + i + "];\n"
" vec2 q = uv * p.x;\n"
" vec2 fl = floor(q);\n"
" vec2 fr = fract(q);\n"
" float h = fract(sin(dot(fl, vec2(12.9898, 78.233))) * 43758.5453);\n"
" if (h > 0.5) fr.x = 1.0 - fr.x;\n"
" float d1 = abs(length(fr) - 0.5);\n"
" float d2 = abs(length(fr - 1.0) - 0.5);\n"
" float d = min(d1, d2);\n"
" float fill = 1.0 - smoothstep(p.y - 0.02, p.y, d);\n"
" vec3 col = 0.5 + 0.5 * cos(6.28318 * (p.z + vec3(0.0, 0.33, 0.67)));\n"
" return vec4(col * fill, 1.0);";
};
v.push_back(std::move(n));
}
// ── Output (sink — drives fragColor) ─────────────────────────
{
DagNodeDef n;
@@ -233,20 +481,21 @@ static const std::vector<DagNodeDef>& build_catalog() {
n.desc = "canvas DAG output";
n.kind = DagKind::Output;
n.num_inputs = 1;
n.param_names = {"", "", "", ""};
n.param_defaults = {0.0f, 0.0f, 0.0f, 0.0f};
n.param_names = {};
n.param_defaults = {};
n.controls = {};
n.body_glsl = [](int) -> std::string { return ""; };
v.push_back(std::move(n));
}
for (auto& n : v) n.is_builtin = true;
return v;
}();
return catalog;
}
const std::vector<DagNodeDef>& dag_catalog() {
return build_catalog();
return mutable_catalog();
}
const DagNodeDef* dag_find(const std::string& name) {
@@ -256,6 +505,30 @@ const DagNodeDef* dag_find(const std::string& name) {
return nullptr;
}
bool dag_register_node(DagNodeDef def) {
auto& cat = mutable_catalog();
for (auto& n : cat) {
if (n.name == def.name) {
if (n.is_builtin) return false;
n = std::move(def);
n.is_builtin = false;
return true;
}
}
def.is_builtin = false;
cat.push_back(std::move(def));
return true;
}
bool dag_unregister_node(const std::string& name) {
auto& cat = mutable_catalog();
auto it = std::find_if(cat.begin(), cat.end(),
[&](const DagNodeDef& n){ return n.name == name && !n.is_builtin; });
if (it == cat.end()) return false;
cat.erase(it);
return true;
}
} // namespace fn::gfx
#ifdef DAG_CATALOG_TEST
@@ -319,13 +592,15 @@ int main() {
assert(body.find("return") != std::string::npos);
}
// 8. Control param indices stay within 0..3
// 8. Control param indices stay within 0..param_count-1 of their node
for (const auto& n : cat) {
int pc = static_cast<int>(n.param_defaults.size());
for (const auto& c : n.controls) {
for (int idx : c.param_idx) {
assert(idx >= -1 && idx < 4);
assert(idx >= -1 && idx < pc);
}
}
assert(n.param_names.size() == n.param_defaults.size());
}
std::printf("dag_catalog: 8/8 asserts passed (%zu nodes)\n", cat.size());
+11
View File
@@ -4,8 +4,19 @@
namespace fn::gfx {
// Active catalog (built-in nodes + any user-registered ones).
const std::vector<DagNodeDef>& dag_catalog();
// Look up a node by name. Returns nullptr if not present.
const DagNodeDef* dag_find(const std::string& name);
// Add (or replace) a user-defined node. Returns false if `def.name` collides
// with a built-in. Replacing an existing user node by same name is allowed.
// Always sets def.is_builtin = false on the stored copy.
bool dag_register_node(DagNodeDef def);
// Remove a user-defined node by name. Returns true if removed.
// Built-in nodes cannot be removed.
bool dag_unregister_node(const std::string& name);
} // namespace fn::gfx
+18
View File
@@ -44,3 +44,21 @@ output: "dag_catalog(): referencia const estable al vector de DagNodeDef (instan
## Notas
Los cuerpos GLSL omiten las declaraciones de u_time, u_resolution, u_params — las proporciona el preamble de gl_shader::compile_fragment o compile_dag_to_glsl. El indice idx que recibe body_glsl es la posicion en el pipeline (para indexar u_params[idx]).
## Cambios 2026-04-25 (Fase 5 + Fase 7 shaders_lab)
Catálogo creció de 11 a **19 nodos**. Nuevos `Gen` (8): `checker`, `stripes`, `dots`, `rings`, `polar_rays`, `noise_value`, `voronoi`, `truchet`. Bug fix: `solid` ahora muestra label en su control Color (era invisible por `ImGuiColorEditFlags_NoLabel`).
API mutable y lifecycle (declarados en `dag_catalog.h`):
- `dag_register_node(DagNodeDef def) -> bool`: añade o reemplaza un nodo user. Refuse si el nombre colisiona con un built-in. Setea `is_builtin = false` en el stored.
- `dag_unregister_node(name) -> bool`: borra un user node. Built-ins están protegidos.
- Flag `is_builtin` en `DagNodeDef` (ver `dag_types.h`). Built-ins se cargan en el constructor estático y nunca se tocan tras eso.
Layout de params:
- `param_names`/`param_defaults` pasan de `array<*,4>` a `vector<*>`. Cada nodo declara la cantidad real de floats que necesita (sin padding cosmético).
- `body_glsl` recibe `int base_vec4` (índice base en el array global), no el index del nodo. El compilador lo calcula vía `dag_param_layout`.
- `body_glsl(idx)` semantically: where `idx` was the node index, now it is the vec4 base. Bodies que originalmente hacían `vec4 p = u_params[i]; ...; p.x ... p.w` siguen funcionando porque cada nodo built-in cabe en 1 vec4. Generators custom de Code → DAG (`code_to_generator`) reciben `__BASE__` como placeholder y la lambda lo sustituye en runtime con el valor real.
`body_glsl(int base_vec4)` retorna string con cuerpo de la función `vec4 node_<i>(vec4 a?, vec4 b?, ..., vec2 uv)`. Los inputs llegan como params `a`,`b`,`c`,`d` según `num_inputs`; `uv` siempre presente.
Tests: 8/8 (19 nodos, invariantes por kind + 1 control_idx in-bounds + name uniqueness).
+115 -13
View File
@@ -1,17 +1,31 @@
#include "gfx/dag_compile.h"
#include "gfx/dag_catalog.h"
#include <algorithm>
#include <regex>
#include <sstream>
namespace fn::gfx {
static constexpr int MAX_NODES = 16;
static constexpr int MAX_NODES = 16;
static constexpr int MAX_PARAM_VEC4S = 64; // 256 floats — enough for 16 nodes × ~16 floats each
std::vector<int> dag_param_layout(const std::vector<DagStep>& pipeline) {
std::vector<int> base(pipeline.size(), 0);
int cursor = 0;
for (size_t i = 0; i < pipeline.size(); ++i) {
const DagNodeDef* def = dag_find(pipeline[i].name);
int pc = def ? static_cast<int>(def->param_defaults.size()) : 0;
base[i] = cursor;
cursor += dag_vec4_count(pc);
}
return base;
}
std::string compile_dag_to_glsl(const std::vector<DagStep>& pipeline) {
const int n = static_cast<int>(std::min(pipeline.size(), static_cast<size_t>(MAX_NODES)));
std::ostringstream out;
out << "uniform vec4 u_params[16];\n";
out << "uniform vec4 u_params[" << MAX_PARAM_VEC4S << "];\n";
out << "uniform int u_preview_target; // -1 = real Output; >=0 = show out_<i>\n\n";
if (n == 0) {
@@ -23,6 +37,8 @@ std::string compile_dag_to_glsl(const std::vector<DagStep>& pipeline) {
return out.str();
}
std::vector<int> base = dag_param_layout(pipeline);
// Emit per-node functions (skip Output: it's a sink, no body)
for (int i = 0; i < n; ++i) {
const DagStep& step = pipeline[static_cast<size_t>(i)];
@@ -38,7 +54,7 @@ std::string compile_dag_to_glsl(const std::vector<DagStep>& pipeline) {
if (ni >= 4) out << ", vec4 d";
if (ni > 0) out << ", ";
out << "vec2 uv) {\n";
out << def->body_glsl(i) << "\n";
out << def->body_glsl(base[static_cast<size_t>(i)]) << "\n";
out << "}\n\n";
}
@@ -67,8 +83,9 @@ std::string compile_dag_to_glsl(const std::vector<DagStep>& pipeline) {
}
}
}
if (last_valid_out < 0) return "vec4(0.0, 0.0, 0.0, 1.0)";
return "out_" + std::to_string(last_valid_out);
// Op/Blend with no source on this slot → black input (cannot fall back to
// last_valid_out: that's how nodes "leak" into the canvas without being wired).
return "vec4(0.0, 0.0, 0.0, 1.0)";
};
out << " vec4 out_" << i << " = node_" << i << "(";
@@ -81,6 +98,7 @@ std::string compile_dag_to_glsl(const std::vector<DagStep>& pipeline) {
last_valid_out = i;
}
(void)last_valid_out;
// Preview branch: if u_preview_target points to a valid out_<i>, emit it
// and bail out before the Output-driven fragColor.
@@ -95,6 +113,9 @@ std::string compile_dag_to_glsl(const std::vector<DagStep>& pipeline) {
// Resolve fragColor: if there's an Output node with a connection, use that; else fallback.
auto seed = [&]() { out << " fragColor = vec4(0.04, 0.04, 0.06, 1.0);\n"; };
// Strict policy: only emit what is wired into the Output node. With no
// Output present, or with Output left disconnected, paint the seed color —
// never silently fall back to the last evaluated node.
if (output_idx >= 0) {
const std::string& sid = pipeline[static_cast<size_t>(output_idx)].source_ids[0];
int src = -1;
@@ -104,9 +125,7 @@ std::string compile_dag_to_glsl(const std::vector<DagStep>& pipeline) {
}
}
if (src >= 0) out << " fragColor = out_" << src << ";\n";
else seed();
} else if (last_valid_out >= 0) {
out << " fragColor = out_" << last_valid_out << ";\n";
else seed();
} else {
seed();
}
@@ -116,6 +135,56 @@ std::string compile_dag_to_glsl(const std::vector<DagStep>& pipeline) {
return out.str();
}
std::string compile_dag_to_glsl_baked(const std::vector<DagStep>& pipeline) {
std::string s = compile_dag_to_glsl(pipeline);
// Compute total vec4 slots actually used by the pipeline.
auto base = dag_param_layout(pipeline);
int total = 0;
for (size_t i = 0; i < pipeline.size(); ++i) {
const DagNodeDef* def = dag_find(pipeline[i].name);
int pc = def ? static_cast<int>(def->param_defaults.size()) : 0;
int v = dag_vec4_count(pc);
if (base[i] + v > total) total = base[i] + v;
}
if (total == 0) total = 1; // GLSL forbids zero-sized arrays
// Pack current params into a flat float array (same layout as dag_uniforms_apply).
std::vector<float> data(static_cast<size_t>(total * 4), 0.0f);
for (size_t i = 0; i < pipeline.size(); ++i) {
const DagNodeDef* def = dag_find(pipeline[i].name);
if (!def) continue;
int pc = static_cast<int>(def->param_defaults.size());
int b = base[i] * 4;
for (int k = 0; k < pc && k < static_cast<int>(pipeline[i].params.size()); ++k) {
data[static_cast<size_t>(b + k)] = pipeline[i].params[static_cast<size_t>(k)];
}
}
// Build `const vec4 u_params[N] = vec4[N](vec4(...), ...);`
std::ostringstream init;
init << "const vec4 u_params[" << total << "] = vec4[" << total << "](";
for (int i = 0; i < total; ++i) {
if (i > 0) init << ", ";
init << "vec4("
<< data[static_cast<size_t>(i * 4 + 0)] << ", "
<< data[static_cast<size_t>(i * 4 + 1)] << ", "
<< data[static_cast<size_t>(i * 4 + 2)] << ", "
<< data[static_cast<size_t>(i * 4 + 3)] << ")";
}
init << ");";
// Replace the uniform u_params declaration with the const array.
static const std::regex up_re(R"(uniform\s+vec4\s+u_params\[\d+\];)");
s = std::regex_replace(s, up_re, init.str());
// Replace the u_preview_target uniform with a const = -1 (kills the preview branches).
static const std::regex pt_re(R"(uniform\s+int\s+u_preview_target;[^\n]*)");
s = std::regex_replace(s, pt_re, "const int u_preview_target = -1;");
return s;
}
} // namespace fn::gfx
#ifdef DAG_COMPILE_TEST
@@ -136,22 +205,23 @@ int main() {
assert(contains(s, "fragColor = vec4(0.04"));
}
// 2. Single Gen → fragColor = out_0
// 2. Single Gen + Output wired → fragColor = out_0
{
std::vector<DagStep> p;
DagStep g; g.id = "a"; g.name = "plasma";
p.push_back(g);
DagStep g; g.id = "a"; g.name = "plasma"; p.push_back(g);
DagStep o; o.id = "out"; o.name = "output"; o.source_ids[0] = "a"; p.push_back(o);
auto s = compile_dag_to_glsl(p);
assert(contains(s, "vec4 node_0"));
assert(contains(s, "vec4 out_0 = node_0("));
assert(contains(s, "fragColor = out_0"));
}
// 3. Gen + Op → Op uses out_0 as input a
// 3. Gen + Op + Output → Op uses out_0 as input, fragColor = out_1
{
std::vector<DagStep> p;
DagStep g; g.id = "a"; g.name = "plasma"; p.push_back(g);
DagStep o; o.id = "b"; o.name = "invert"; o.source_ids[0] = "a"; p.push_back(o);
DagStep f; f.id = "out"; f.name = "output"; f.source_ids[0] = "b"; p.push_back(f);
auto s = compile_dag_to_glsl(p);
assert(contains(s, "out_1 = node_1(out_0, uv)"));
assert(contains(s, "fragColor = out_1"));
@@ -165,8 +235,26 @@ int main() {
DagStep m; m.id = "m"; m.name = "blend_mix";
m.source_ids[0] = "a"; m.source_ids[1] = "b";
p.push_back(m);
DagStep o; o.id = "out"; o.name = "output"; o.source_ids[0] = "m"; p.push_back(o);
auto s = compile_dag_to_glsl(p);
assert(contains(s, "out_2 = node_2(out_0, out_1, uv)"));
assert(contains(s, "fragColor = out_2"));
}
// 4b. Strict mode: nodes without Output → seed (never leaks last node).
// Note: the preview branch emits `if (u_preview_target == i) fragColor = out_i;`
// which we don't penalise; what matters is the *final* fragColor (after the
// preview ifs) — that must be the seed, not a node output.
{
std::vector<DagStep> p;
DagStep g; g.id = "a"; g.name = "plasma"; p.push_back(g);
auto s = compile_dag_to_glsl(p);
assert(contains(s, "vec4 out_0 = node_0(")); // node still emitted
// The seed line must appear *after* the last preview branch
size_t seed_pos = s.rfind("fragColor = vec4(0.04");
size_t preview_pos = s.rfind("u_preview_target ==");
assert(seed_pos != std::string::npos);
assert(preview_pos == std::string::npos || seed_pos > preview_pos);
}
// 5. Output node drives fragColor from its source, not from last index
@@ -193,7 +281,21 @@ int main() {
assert(contains(s, "fragColor = vec4(0.04"));
}
std::printf("dag_compile: 6/6 asserts passed\n");
// 7. Baked variant: const arrays, no uniforms u_params / u_preview_target
{
std::vector<DagStep> p;
DagStep g; g.id = "a"; g.name = "plasma"; g.params = {2.0f, 3.0f}; p.push_back(g);
DagStep o; o.id = "out"; o.name = "output"; o.source_ids[0] = "a"; p.push_back(o);
auto s = compile_dag_to_glsl_baked(p);
assert(!contains(s, "uniform vec4 u_params"));
assert(!contains(s, "uniform int u_preview_target"));
assert(contains(s, "const vec4 u_params["));
assert(contains(s, "vec4(2")); // baked first param
assert(contains(s, "const int u_preview_target = -1"));
assert(contains(s, "fragColor = out_0"));
}
std::printf("dag_compile: 8/8 asserts passed\n");
return 0;
}
#endif
+14 -1
View File
@@ -7,8 +7,21 @@ namespace fn::gfx {
// Compila un pipeline DAG a GLSL 330 core completo (listo para gl_shader::compile_fragment).
// El preamble de gl_shader ya declara #version, fragColor, u_time, u_resolution, u_mouse.
// Este compilador emite uniform vec4 u_params[16], las funciones node_<i> y void main().
// Este compilador emite uniform vec4 u_params[64], las funciones node_<i> y void main().
// Si el pipeline esta vacio, emite un fragment que pinta gris oscuro.
std::string compile_dag_to_glsl(const std::vector<DagStep>& pipeline);
// Devuelve el indice base (vec4) en u_params[] que ocupa cada nodo del pipeline.
// Cada nodo ocupa ceil(param_count / 4) vec4s consecutivos. Nodos con 0 params ocupan 0.
// El compilador y dag_uniforms_apply usan el mismo layout.
std::vector<int> dag_param_layout(const std::vector<DagStep>& pipeline);
// Variante de compile_dag_to_glsl que sustituye `uniform vec4 u_params[64]`
// por un `const vec4 u_params[N] = vec4[N](...)` con los valores actuales del
// pipeline empaquetados, y `uniform int u_preview_target` por
// `const int u_preview_target = -1`. El resultado es un fragment shader
// autocontenido: no depende de ningun uniform externo y se puede pegar tal cual
// en el editor Code para reproducir el DAG actual.
std::string compile_dag_to_glsl_baked(const std::vector<DagStep>& pipeline);
} // namespace fn::gfx
+13
View File
@@ -49,3 +49,16 @@ void main() {
- Si el pipeline esta vacio, emite void main() que pinta gris oscuro (0.04, 0.04, 0.06).
- MAX_NODES = 16. Pipelines mas largos se truncan silenciosamente.
- source_id fallback: si el id no se encuentra o apunta a un indice >= idx, usa max(0, idx-2).
## Cambios 2026-04-25 (Fase 5 + Fase 7 shaders_lab)
- **Layout de params dinámico**: el array global pasa de `vec4 u_params[16]` (1 vec4 por nodo) a `vec4 u_params[64]` (`MAX_PARAM_VEC4S`). Cada nodo ocupa `dag_vec4_count(param_count)` vec4s consecutivos. Helper público `dag_param_layout(pipeline) -> vector<int>` devuelve el índice base por nodo y se comparte con `dag_uniforms_apply`.
- **Strict output**: el fallback `last_valid_out` que filtraba el output del último nodo cuando `Output` no tenía source o no existía está eliminado. Ahora la regla es: solo se emite lo conectado al `Output`; en cualquier otro caso `seed()` (gris oscuro). El `resolve()` interno también devuelve `vec4(0,0,0,1)` para slots de input vacíos (antes caía a `last_valid_out`).
- **Test 4b nuevo**: nodo sin Output → seed final aparece después de las branches de preview (`fragColor = vec4(0.04` después del último `if (u_preview_target ==`).
- **Variante baked: `compile_dag_to_glsl_baked(pipeline)`** (nuevo en `.h` + `.cpp`):
- Sustituye `uniform vec4 u_params[64];` por `const vec4 u_params[N] = vec4[N](vec4(...), ...);` con los valores actuales del pipeline empaquetados (mismo layout que `dag_uniforms_apply`).
- Sustituye `uniform int u_preview_target;` por `const int u_preview_target = -1;`. Las branches de preview quedan muertas.
- Sustitución vía `std::regex_replace`. `total = max(base[i] + dag_vec4_count(pc))` o 1 (GLSL prohíbe arrays de tamaño 0).
- Caso de uso: panel `Generated GLSL` de shaders_lab muestra el baked, paste-able en el editor `Code` para reproducir el render del DAG sin uniforms externos. Test 7 verifica ausencia de `uniform vec4 u_params` y presencia de `const vec4 u_params[`.
Cobertura tests: 7/7 (strict + 4b) → **8/8** (incluye baked).
+307 -34
View File
@@ -4,6 +4,7 @@
#include "imgui.h"
#include "imgui_node_editor.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <functional>
#include <queue>
@@ -21,6 +22,10 @@ static constexpr int MAX_NODES = 16;
static ed::EditorContext* s_ctx = nullptr;
static uint32_t s_next_uid = 1;
static std::unordered_set<uint32_t> s_positioned;
// Real pin positions in canvas space, captured during node draw and consulted
// by the splice hit-test. Without this, a cable's hit zone is offset from the
// visible cable whenever node height ≠ pin row height (e.g. preview open).
static std::unordered_map<uintptr_t, ImVec2> s_pin_canvas_pos;
// ── ID encoding ──────────────────────────────────────────────────────────────
// node id = editor_uid
@@ -48,6 +53,45 @@ static bool is_output_pin(uintptr_t id) { return (id & 0xFF) == 0; }
static uint32_t uid_from_pin(uintptr_t id) { return static_cast<uint32_t>(id >> 8); }
static int slot_from_input_pin(uintptr_t id) { return static_cast<int>(id & 0xFF) - 1; }
// Closest distance from point p to the segment [a, b] (canvas space).
static float dist_point_to_segment(ImVec2 p, ImVec2 a, ImVec2 b) {
float abx = b.x - a.x, aby = b.y - a.y;
float apx = p.x - a.x, apy = p.y - a.y;
float ab2 = abx * abx + aby * aby;
if (ab2 <= 1e-6f) return std::sqrt(apx * apx + apy * apy);
float t = (apx * abx + apy * aby) / ab2;
t = std::max(0.0f, std::min(1.0f, t));
float qx = a.x + t * abx, qy = a.y + t * aby;
float dx = p.x - qx, dy = p.y - qy;
return std::sqrt(dx * dx + dy * dy);
}
// Same horizontal-bias control offset imgui-node-editor uses for its links.
static inline float bezier_ctrl(float dx) {
return std::max(40.0f, std::abs(dx) * 0.5f);
}
// Closest distance from point p to a cubic bezier curve, sampled as 24 chords.
static float dist_point_to_bezier(ImVec2 p, ImVec2 p0, ImVec2 p1, ImVec2 p2, ImVec2 p3) {
constexpr int N = 24;
float best = 1e30f;
ImVec2 prev = p0;
for (int i = 1; i <= N; ++i) {
float t = static_cast<float>(i) / static_cast<float>(N);
float u = 1.0f - t;
float b0 = u * u * u;
float b1 = 3.0f * u * u * t;
float b2 = 3.0f * u * t * t;
float b3 = t * t * t;
ImVec2 pt(b0 * p0.x + b1 * p1.x + b2 * p2.x + b3 * p3.x,
b0 * p0.y + b1 * p1.y + b2 * p2.y + b3 * p3.y);
float d = dist_point_to_segment(p, prev, pt);
if (d < best) best = d;
prev = pt;
}
return best;
}
static int find_by_uid(const std::vector<DagStep>& p, uint32_t uid) {
for (int i = 0; i < static_cast<int>(p.size()); ++i) {
if (p[static_cast<size_t>(i)].editor_uid == uid) return i;
@@ -71,10 +115,14 @@ static ImVec4 kind_color(DagKind kind) {
return ImVec4(1, 1, 1, 1);
}
static constexpr float PIN_RADIUS = 9.0f;
static constexpr float PIN_DIAMETER = PIN_RADIUS * 2.0f;
static const ImVec4 PIN_COLOR = ImVec4(0.78f, 0.78f, 0.82f, 1.0f);
static const ImVec4 PIN_BORDER = ImVec4(0.20f, 0.20f, 0.22f, 1.0f);
static constexpr float PIN_RADIUS = 14.0f; // big grabbable target
static constexpr float PIN_DIAMETER = PIN_RADIUS * 2.0f;
static constexpr float CONTROL_WIDTH = 220.0f;
static constexpr float COL_GAP = 14.0f; // input ↔ controls ↔ output gap
static constexpr float CABLE_THICK = 3.5f;
static const ImVec4 PIN_COLOR = ImVec4(0.78f, 0.78f, 0.82f, 1.0f);
static const ImVec4 PIN_BORDER = ImVec4(0.20f, 0.20f, 0.22f, 1.0f);
static const ImVec4 SPLICE_COLOR = ImVec4(1.00f, 0.82f, 0.18f, 1.0f); // golden preview cable
enum class PinSide { Input, Output };
@@ -90,7 +138,7 @@ static void draw_pin_circle(PinSide side) {
ImVec2(center.x + PIN_RADIUS, center.y + PIN_RADIUS));
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddCircleFilled(center, PIN_RADIUS, ImGui::ColorConvertFloat4ToU32(PIN_COLOR));
dl->AddCircle(center, PIN_RADIUS, ImGui::ColorConvertFloat4ToU32(PIN_BORDER), 0, 1.5f);
dl->AddCircle(center, PIN_RADIUS, ImGui::ColorConvertFloat4ToU32(PIN_BORDER), 0, 2.0f);
ImGui::Dummy(ImVec2(PIN_RADIUS, PIN_DIAMETER));
}
@@ -197,30 +245,238 @@ bool dag_node_editor(std::vector<DagStep>& pipeline) {
if (s_pending_add) {
const DagNodeDef* def = dag_find(s_pending_add_name);
if (def && static_cast<int>(pipeline.size()) < MAX_NODES) {
uint32_t uid = s_next_uid++;
DagStep step;
step.id = "n" + std::to_string(uid);
step.name = def->name;
step.params = def->param_defaults;
step.editor_uid = uid;
ImVec2 canvas_pos = ed::ScreenToCanvas(s_pending_add_pos);
step.editor_pos_x = canvas_pos.x;
step.editor_pos_y = canvas_pos.y;
// Insert before the Output node so the Output stays at the back;
// otherwise new nodes can never be wired into it (compiler and
// cycle check only search indices strictly before the target).
auto insert_it = pipeline.end();
for (auto it = pipeline.begin(); it != pipeline.end(); ++it) {
const DagNodeDef* d = dag_find(it->name);
if (d && d->kind == DagKind::Output) { insert_it = it; break; }
if (def) {
ImVec2 drop = ed::ScreenToCanvas(s_pending_add_pos);
// ── Priority 1: drop on an existing cable → splice (src → new → dst).
// Only valid if the new def actually has an input pin (Op / Blend).
int splice_src_idx = -1, splice_dst_idx = -1, splice_slot = -1;
if (def->num_inputs >= 1 && def->kind != DagKind::Output) {
constexpr float HIT_THRESH = 14.0f; // canvas px
float best_d = HIT_THRESH;
for (size_t i = 0; i < pipeline.size(); ++i) {
const DagStep& dst = pipeline[i];
const DagNodeDef* dd = dag_find(dst.name);
if (!dd) continue;
for (int k = 0; k < dd->num_inputs; ++k) {
const std::string& sid = dst.source_ids[static_cast<size_t>(k)];
if (sid.empty()) continue;
int src_idx = find_by_id(pipeline, sid);
if (src_idx < 0) continue;
const DagStep& src = pipeline[static_cast<size_t>(src_idx)];
auto out_it = s_pin_canvas_pos.find(output_pin_id(src.editor_uid));
auto in_it = s_pin_canvas_pos.find(input_pin_id(dst.editor_uid, k));
if (out_it == s_pin_canvas_pos.end() ||
in_it == s_pin_canvas_pos.end()) continue;
ImVec2 A = out_it->second;
ImVec2 B = in_it->second;
float ctrl = bezier_ctrl(B.x - A.x);
ImVec2 P1(A.x + ctrl, A.y);
ImVec2 P2(B.x - ctrl, B.y);
float d = dist_point_to_bezier(drop, A, P1, P2, B);
if (d < best_d) {
best_d = d;
splice_src_idx = src_idx;
splice_dst_idx = static_cast<int>(i);
splice_slot = k;
}
}
}
}
// ── Priority 2: drop on an existing node of the same kind → replace.
int hit_idx = -1;
if (splice_dst_idx < 0) {
for (size_t i = 0; i < pipeline.size(); ++i) {
auto npos = ed::GetNodePosition(ed::NodeId(node_id(pipeline[i].editor_uid)));
auto nsz = ed::GetNodeSize (ed::NodeId(node_id(pipeline[i].editor_uid)));
if (drop.x >= npos.x && drop.x <= npos.x + nsz.x &&
drop.y >= npos.y && drop.y <= npos.y + nsz.y) {
hit_idx = static_cast<int>(i);
break;
}
}
}
const DagNodeDef* hit_def = (hit_idx >= 0)
? dag_find(pipeline[static_cast<size_t>(hit_idx)].name) : nullptr;
if (splice_dst_idx >= 0 && static_cast<int>(pipeline.size()) < MAX_NODES) {
// Splice: build the new node wired to the existing source, then
// rewire the existing destination's input to point to it.
const std::string src_id = pipeline[static_cast<size_t>(splice_src_idx)].id;
const std::string dst_id = pipeline[static_cast<size_t>(splice_dst_idx)].id;
uint32_t uid = s_next_uid++;
DagStep step;
step.id = "n" + std::to_string(uid);
step.name = def->name;
step.params = def->param_defaults;
step.editor_uid = uid;
step.editor_pos_x = drop.x;
step.editor_pos_y = drop.y;
step.source_ids[0] = src_id; // wire src → new
auto insert_it = pipeline.end();
for (auto it = pipeline.begin(); it != pipeline.end(); ++it) {
const DagNodeDef* d = dag_find(it->name);
if (d && d->kind == DagKind::Output) { insert_it = it; break; }
}
pipeline.insert(insert_it, step);
// Re-find dst by id (insertion may have shifted indices) and
// rewire its slot to the new node.
int dst_now = find_by_id(pipeline, dst_id);
if (dst_now >= 0) {
pipeline[static_cast<size_t>(dst_now)].source_ids[static_cast<size_t>(splice_slot)] = step.id;
}
changed = true;
} else if (hit_def && hit_def->kind == def->kind && def->kind != DagKind::Output) {
// Replace path: same-kind node hit. Keep id, editor_uid, pos,
// source_ids, preview_open. Reset params + clear stale input
// slots beyond the new def's input count.
DagStep& tgt = pipeline[static_cast<size_t>(hit_idx)];
tgt.name = def->name;
tgt.params = def->param_defaults;
for (int k = def->num_inputs; k < 4; ++k) {
tgt.source_ids[static_cast<size_t>(k)].clear();
}
changed = true;
} else if (static_cast<int>(pipeline.size()) < MAX_NODES) {
// Add path: brand-new node, inserted before Output so the sink stays last.
uint32_t uid = s_next_uid++;
DagStep step;
step.id = "n" + std::to_string(uid);
step.name = def->name;
step.params = def->param_defaults;
step.editor_uid = uid;
step.editor_pos_x = drop.x;
step.editor_pos_y = drop.y;
auto insert_it = pipeline.end();
for (auto it = pipeline.begin(); it != pipeline.end(); ++it) {
const DagNodeDef* d = dag_find(it->name);
if (d && d->kind == DagKind::Output) { insert_it = it; break; }
}
pipeline.insert(insert_it, step);
changed = true;
}
pipeline.insert(insert_it, step);
changed = true;
}
s_pending_add = false;
}
// ── Live splice candidate detection ─────────────────────────────────────
// The user can splice into a cable in two ways:
// (a) drag a node from the palette → ImGui drag-drop payload.
// (b) drag an existing node by its body → tracked via mouse-down on a node.
// In both cases, while the drag is active we hit-test against existing
// cables and remember the candidate so:
// 1. the link-drawing pass below paints it in SPLICE_COLOR (preview).
// 2. the release handler farther down rewires the graph.
static uint32_t s_drag_existing_uid = 0;
// Start tracking an existing-node drag when the user mouse-down on a node
// body (not on a pin, not on the Output sink, must have at least one input).
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
ed::PinId hp = ed::GetHoveredPin();
ed::NodeId hn = ed::GetHoveredNode();
if (hp.Get() == 0 && hn.Get() != 0) {
uint32_t uid = static_cast<uint32_t>(hn.Get());
int idx = find_by_uid(pipeline, uid);
if (idx >= 0) {
const DagNodeDef* d = dag_find(pipeline[static_cast<size_t>(idx)].name);
if (d && d->num_inputs >= 1 && d->kind != DagKind::Output) {
s_drag_existing_uid = uid;
}
}
}
}
// Resolve which definition (if any) is the active splice candidate.
const DagNodeDef* candidate_def = nullptr;
const std::string* exclude_node_id = nullptr;
int exclude_node_idx = -1;
if (const ImGuiPayload* p = ImGui::GetDragDropPayload()) {
if (p->IsDataType("DAG_NODE_TYPE")) {
std::string drag_name(static_cast<const char*>(p->Data),
static_cast<size_t>(p->DataSize));
candidate_def = dag_find(drag_name);
}
}
if (!candidate_def && s_drag_existing_uid != 0) {
exclude_node_idx = find_by_uid(pipeline, s_drag_existing_uid);
if (exclude_node_idx >= 0) {
candidate_def = dag_find(pipeline[static_cast<size_t>(exclude_node_idx)].name);
exclude_node_id = &pipeline[static_cast<size_t>(exclude_node_idx)].id;
}
}
// Hit-test cables against current mouse position (canvas space).
uint32_t splice_hl_from_uid = 0;
uint32_t splice_hl_to_uid = 0;
int splice_hl_slot = -1;
if (candidate_def && candidate_def->num_inputs >= 1
&& candidate_def->kind != DagKind::Output) {
ImVec2 cur = ed::ScreenToCanvas(ImGui::GetMousePos());
constexpr float HIT_THRESH = 16.0f;
float best_d = HIT_THRESH;
for (size_t i = 0; i < pipeline.size(); ++i) {
const DagStep& dst = pipeline[i];
// Skip cables that touch the moving node (its own in/out edges).
if (exclude_node_id && dst.id == *exclude_node_id) continue;
const DagNodeDef* dd = dag_find(dst.name);
if (!dd) continue;
for (int k = 0; k < dd->num_inputs; ++k) {
const std::string& sid = dst.source_ids[static_cast<size_t>(k)];
if (sid.empty()) continue;
if (exclude_node_id && sid == *exclude_node_id) continue;
int src_idx = find_by_id(pipeline, sid);
if (src_idx < 0) continue;
const DagStep& src = pipeline[static_cast<size_t>(src_idx)];
auto out_it = s_pin_canvas_pos.find(output_pin_id(src.editor_uid));
auto in_it = s_pin_canvas_pos.find(input_pin_id(dst.editor_uid, k));
if (out_it == s_pin_canvas_pos.end() ||
in_it == s_pin_canvas_pos.end()) continue;
ImVec2 A = out_it->second;
ImVec2 B = in_it->second;
float ctrl = bezier_ctrl(B.x - A.x);
ImVec2 P1(A.x + ctrl, A.y);
ImVec2 P2(B.x - ctrl, B.y);
float d = dist_point_to_bezier(cur, A, P1, P2, B);
if (d < best_d) {
best_d = d;
splice_hl_from_uid = src.editor_uid;
splice_hl_to_uid = dst.editor_uid;
splice_hl_slot = k;
}
}
}
}
// Release handler for the existing-node-drag splice. Palette splice goes
// through s_pending_add above; this branch handles "drag node body onto cable".
if (ImGui::IsMouseReleased(ImGuiMouseButton_Left) && s_drag_existing_uid != 0) {
uint32_t moving_uid = s_drag_existing_uid;
s_drag_existing_uid = 0;
if (splice_hl_to_uid != 0) {
int mv_idx = find_by_uid(pipeline, moving_uid);
int src_idx = find_by_uid(pipeline, splice_hl_from_uid);
int dst_idx = find_by_uid(pipeline, splice_hl_to_uid);
if (mv_idx >= 0 && src_idx >= 0 && dst_idx >= 0) {
const std::string moving_id = pipeline[static_cast<size_t>(mv_idx)].id;
const std::string src_id = pipeline[static_cast<size_t>(src_idx)].id;
// Detach moving node from any existing consumer.
for (auto& s : pipeline) {
for (auto& sid : s.source_ids) {
if (sid == moving_id) sid.clear();
}
}
pipeline[static_cast<size_t>(mv_idx)].source_ids[0] = src_id;
pipeline[static_cast<size_t>(dst_idx)].source_ids[static_cast<size_t>(splice_hl_slot)] = moving_id;
changed = true;
}
}
}
// ── Draw nodes ───────────────────────────────────────────────────────────
for (int i = 0; i < static_cast<int>(pipeline.size()); ++i) {
DagStep& step = pipeline[static_cast<size_t>(i)];
@@ -233,7 +489,7 @@ bool dag_node_editor(std::vector<DagStep>& pipeline) {
// and user drags must not be overwritten.
if (s_positioned.find(step.editor_uid) == s_positioned.end()) {
if (step.editor_pos_x == 0.0f && step.editor_pos_y == 0.0f) {
step.editor_pos_x = 50.0f + static_cast<float>(i) * 220.0f;
step.editor_pos_x = 50.0f + static_cast<float>(i) * 320.0f;
step.editor_pos_y = 100.0f;
}
ed::SetNodePosition(ed::NodeId(node_id(step.editor_uid)),
@@ -243,7 +499,7 @@ bool dag_node_editor(std::vector<DagStep>& pipeline) {
// Zero lateral padding so the input/output pin circles sit flush
// with the node's left and right edges.
ed::PushStyleVar(ed::StyleVar_NodePadding, ImVec4(0, 8, 0, 8));
ed::PushStyleVar(ed::StyleVar_NodePadding, ImVec4(0, 12, 0, 12));
ed::BeginNode(ed::NodeId(node_id(step.editor_uid)));
// Header (with horizontal padding so the title doesn't touch the edge)
@@ -266,36 +522,43 @@ bool dag_node_editor(std::vector<DagStep>& pipeline) {
ed::BeginPin(ed::PinId(input_pin_id(step.editor_uid, k)), ed::PinKind::Input);
ed::PinPivotAlignment(ImVec2(0.0f, 0.5f));
ed::PinPivotSize(ImVec2(0, 0));
ImVec2 cur_screen = ImGui::GetCursorScreenPos();
ImVec2 center_screen(cur_screen.x, cur_screen.y + PIN_RADIUS);
s_pin_canvas_pos[input_pin_id(step.editor_uid, k)] =
ed::ScreenToCanvas(center_screen);
draw_pin_circle(PinSide::Input);
ed::EndPin();
}
ImGui::EndGroup();
ImGui::SameLine(0, 8); // gap between pin column and controls
ImGui::SameLine(0, COL_GAP); // gap between pin column and controls
ImGui::BeginGroup(); // controls column (centre, with internal padding)
ImGui::PushID(static_cast<int>(step.editor_uid));
if (def->controls.empty() && def->kind != DagKind::Output) {
ImGui::Dummy(ImVec2(60, PIN_DIAMETER));
ImGui::Dummy(ImVec2(CONTROL_WIDTH * 0.5f, PIN_DIAMETER));
}
for (size_t ci = 0; ci < def->controls.size(); ++ci) {
const DagControl& ctrl = def->controls[ci];
ImGui::SetNextItemWidth(150.0f);
ImGui::SetNextItemWidth(CONTROL_WIDTH);
char uid_lbl[64];
std::snprintf(uid_lbl, sizeof(uid_lbl), "%s##%u%zu", ctrl.label.c_str(), step.editor_uid, ci);
int pcount = static_cast<int>(step.params.size());
if (ctrl.kind == DagControl::Kind::Slider) {
int pidx = ctrl.param_idx[0];
if (pidx >= 0 && pidx < 4) {
if (pidx >= 0 && pidx < pcount) {
ImGui::SliderFloat(uid_lbl, &step.params[static_cast<size_t>(pidx)], ctrl.min, ctrl.max);
}
} else if (ctrl.kind == DagControl::Kind::XY) {
int px = ctrl.param_idx[0], py = ctrl.param_idx[1];
if (px >= 0 && px < 4 && py >= 0 && py < 4 && py == px + 1) {
if (px >= 0 && px < pcount && py >= 0 && py < pcount && py == px + 1) {
ImGui::SliderFloat2(uid_lbl, &step.params[static_cast<size_t>(px)], ctrl.min, ctrl.max);
}
} else if (ctrl.kind == DagControl::Kind::Color) {
int pr = ctrl.param_idx[0];
if (pr >= 0 && pr + 2 < 4) {
if (pr >= 0 && pr + 2 < pcount) {
ImGui::TextUnformatted(ctrl.label.c_str());
ImGui::SameLine();
ed::Suspend();
ImGui::ColorEdit3(uid_lbl, &step.params[static_cast<size_t>(pr)],
ImGuiColorEditFlags_NoInputs |
@@ -327,13 +590,17 @@ bool dag_node_editor(std::vector<DagStep>& pipeline) {
ImGui::PopID();
ImGui::EndGroup();
ImGui::SameLine(0, 8); // gap between controls and output pin
ImGui::SameLine(0, COL_GAP); // gap between controls and output pin
ImGui::BeginGroup(); // output column (right edge)
if (has_output_pin) {
ed::BeginPin(ed::PinId(output_pin_id(step.editor_uid)), ed::PinKind::Output);
ed::PinPivotAlignment(ImVec2(1.0f, 0.5f));
ed::PinPivotSize(ImVec2(0, 0));
ImVec2 cur_screen = ImGui::GetCursorScreenPos();
ImVec2 center_screen(cur_screen.x + PIN_RADIUS, cur_screen.y + PIN_RADIUS);
s_pin_canvas_pos[output_pin_id(step.editor_uid)] =
ed::ScreenToCanvas(center_screen);
draw_pin_circle(PinSide::Output);
ed::EndPin();
} else {
@@ -356,10 +623,16 @@ bool dag_node_editor(std::vector<DagStep>& pipeline) {
int src_idx = find_by_id(pipeline, sid);
if (src_idx < 0) continue;
const DagStep& src_step = pipeline[static_cast<size_t>(src_idx)];
const bool is_splice_preview =
(src_step.editor_uid == splice_hl_from_uid &&
step.editor_uid == splice_hl_to_uid &&
k == splice_hl_slot);
ImVec4 link_col = is_splice_preview ? SPLICE_COLOR : PIN_COLOR;
float link_thick = is_splice_preview ? CABLE_THICK + 2.0f : CABLE_THICK;
ed::Link(ed::LinkId(link_id(src_step.editor_uid, step.editor_uid, k)),
ed::PinId(output_pin_id(src_step.editor_uid)),
ed::PinId(input_pin_id(step.editor_uid, k)),
PIN_COLOR, 2.5f);
link_col, link_thick);
}
}
+26
View File
@@ -41,3 +41,29 @@ Multi-source: cada nodo declara `num_inputs` (0-4). Los slots de `source_ids[]`
## Dependencia
Requiere `imgui_node_editor` static library linkeada (`cpp/vendor/imgui-node-editor`).
## Notas (2026-04-25, Fase 7 shaders_lab)
Pulido visual y de UX para conexión:
- Pines más grandes para grab fácil: `PIN_RADIUS` 9 → 14, `CABLE_THICK` 2.5 → 3.5, `CONTROL_WIDTH` 150 → 220, `COL_GAP` 8 → 14.
- Espaciado inicial entre nodos auto-colocados subido a 320 px.
- Bug fix: el control `Color` se renderizaba con `ImGuiColorEditFlags_NoLabel`, así que nodos cuya único control era Color (`solid`) parecían sin nombre. Ahora se imprime `TextUnformatted(label) + SameLine` antes del swatch.
Drop comportamiento (en orden de prioridad al soltar un nodo de la paleta o arrastrar un nodo del canvas):
1. **Drop sobre cable** (`dist_point_to_segment` < 18 px en canvas-space): splice. Solo aplica a nodos con `num_inputs >= 1` y kind != Output.
2. **Drop sobre nodo del mismo `DagKind`**: replace. Conserva `id`, `editor_uid`, `editor_pos_x/y`, `source_ids[]`, `preview_open`. Limpia slots de input que sobran si el nuevo def tiene menos `num_inputs`.
3. **Drop en vacío**: add. Inserción antes del `Output` para que el sink se quede al final.
Tracking de drag de nodo existente: `s_drag_existing_uid` se setea en `IsMouseClicked(0)` cuando hay `GetHoveredNode() != 0` y `GetHoveredPin() == 0`. Al soltar, si un cable estaba highlighted, se hace splice (clear de refs hacia el nodo, `mv.source_ids[0] = src.id`, `dst.source_ids[slot] = mv.id`).
Hit-test contra cajas de nodos vía `ed::GetNodePosition + ed::GetNodeSize` (no se usa `ed::GetHoveredNode` porque no es fiable bajo drag-drop activo).
Splice highlight (preview live):
- Mientras hay payload `DAG_NODE_TYPE` (paleta) o `s_drag_existing_uid` activo, hit-test contra cables (distance point-segment).
- El cable candidato se pinta con `SPLICE_COLOR = (1.00, 0.82, 0.18, 1)` y `CABLE_THICK + 2` en `ed::Link()`.
- **Garantía visual**: además se dibuja un bezier dorado en `ImGui::GetForegroundDrawList()` con `AddBezierCubic(P0, P1, P2, P3, color, CABLE_THICK + 4)` para no depender del compositing interno de imgui-node-editor.
- Sin gates `IsMouseDown` / `window_hovered` (silenciaban el highlight). El payload o `s_drag_existing_uid` ya implican drag activo.
Constantes públicas vivas (en el .cpp, no exportadas):
- `PIN_RADIUS = 14`, `CABLE_THICK = 3.5`, `CONTROL_WIDTH = 220`, `COL_GAP = 14`.
- `PIN_COLOR = (0.78, 0.78, 0.82, 1)`, `PIN_BORDER = (0.20, 0.20, 0.22, 1)`, `SPLICE_COLOR = (1.00, 0.82, 0.18, 1)`.
+4 -3
View File
@@ -173,22 +173,23 @@ bool dag_panel(std::vector<DagStep>& pipeline) {
}
// Controls
int pcount = static_cast<int>(step.params.size());
for (const auto& ctrl : def->controls) {
std::string uid_label = ctrl.label + "##" + step.id + std::to_string(&ctrl - def->controls.data());
if (ctrl.kind == DagControl::Kind::Slider) {
int pidx = ctrl.param_idx[0];
if (pidx >= 0 && pidx < 4) {
if (pidx >= 0 && pidx < pcount) {
ImGui::SliderFloat(uid_label.c_str(), &step.params[static_cast<size_t>(pidx)], ctrl.min, ctrl.max);
}
} else if (ctrl.kind == DagControl::Kind::XY) {
int px = ctrl.param_idx[0];
int py = ctrl.param_idx[1];
if (px >= 0 && px < 4 && py >= 0 && py < 4 && py == px + 1) {
if (px >= 0 && px < pcount && py >= 0 && py < pcount && py == px + 1) {
ImGui::SliderFloat2(uid_label.c_str(), &step.params[static_cast<size_t>(px)], ctrl.min, ctrl.max);
}
} else if (ctrl.kind == DagControl::Kind::Color) {
int pr = ctrl.param_idx[0];
if (pr >= 0 && pr + 2 < 4) {
if (pr >= 0 && pr + 2 < pcount) {
ImGui::ColorEdit3(uid_label.c_str(), &step.params[static_cast<size_t>(pr)]);
}
}
+19 -6
View File
@@ -8,6 +8,9 @@ namespace fn::gfx {
enum class DagKind { Gen, Op, Blend, Output };
// Param indices for a control reference floats inside the node's own block
// (range 0..param_count-1). Up to 3 indices are used (Color uses 3 contiguous,
// XY uses 2 contiguous, Slider uses 1).
struct DagControl {
enum class Kind { Slider, XY, Color };
Kind kind;
@@ -24,21 +27,31 @@ struct DagNodeDef {
std::string desc;
DagKind kind = DagKind::Gen;
int num_inputs = 0; // 0=Gen, 1=Op, 2=Blend, up to 4
std::array<std::string, 4> param_names{"", "", "", ""};
std::array<float, 4> param_defaults{0, 0, 0, 0};
std::vector<DagControl> controls;
std::function<std::string(int idx)> body_glsl;
std::vector<std::string> param_names;
std::vector<float> param_defaults;
std::vector<DagControl> controls;
// body_glsl receives the base vec4 index where this node's params live in
// the global u_params[] array (0 if param_count == 0; same value for nodes
// that fit in a single vec4).
std::function<std::string(int base_vec4)> body_glsl;
bool is_builtin = true; // user-saved generators set this false
};
struct DagStep {
std::string id;
std::string name;
std::array<float, 4> params{0, 0, 0, 0};
std::array<std::string, 4> source_ids{"", "", "", ""}; // up to 4 inputs; "" = no connection
std::vector<float> params; // size == def->param_defaults.size()
std::array<std::string, 4> source_ids{"", "", "", ""}; // up to 4 inputs; "" = no connection
float editor_pos_x = 0.0f;
float editor_pos_y = 0.0f;
uint32_t editor_uid = 0; // monotonic counter, used as node editor ID
bool preview_open = false; // show in-node thumbnail of out_<index>
};
// Number of vec4 slots a node with `param_count` floats occupies. 0 -> 0.
inline int dag_vec4_count(int param_count) {
if (param_count <= 0) return 0;
return (param_count + 3) / 4;
}
} // namespace fn::gfx
+21 -10
View File
@@ -1,27 +1,38 @@
#include "gfx/dag_uniforms.h"
#include "gfx/dag_compile.h"
#include "gfx/dag_catalog.h"
#include "gfx/gl_loader.h"
#include <algorithm>
#include <cstring>
namespace fn::gfx {
static constexpr int MAX_NODES = 16;
static constexpr int MAX_PARAM_VEC4S = 64;
void dag_uniforms_apply(const std::vector<DagStep>& pipeline, unsigned int program) {
float data[MAX_NODES * 4];
float data[MAX_PARAM_VEC4S * 4];
std::memset(data, 0, sizeof(data));
const int n = static_cast<int>(std::min(pipeline.size(), static_cast<size_t>(MAX_NODES)));
for (int i = 0; i < n; ++i) {
const auto& step = pipeline[static_cast<size_t>(i)];
data[i * 4 + 0] = step.params[0];
data[i * 4 + 1] = step.params[1];
data[i * 4 + 2] = step.params[2];
data[i * 4 + 3] = step.params[3];
std::vector<int> base = dag_param_layout(pipeline);
for (size_t i = 0; i < pipeline.size(); ++i) {
const DagStep& step = pipeline[i];
const DagNodeDef* def = dag_find(step.name);
if (!def) continue;
int pc = static_cast<int>(def->param_defaults.size());
int b = base[i] * 4;
for (int k = 0; k < pc && b + k < MAX_PARAM_VEC4S * 4; ++k) {
data[b + k] = (k < static_cast<int>(step.params.size())) ? step.params[static_cast<size_t>(k)] : 0.0f;
}
}
GLint loc = glGetUniformLocation(program, "u_params");
if (loc >= 0) glUniform4fv(loc, MAX_NODES, data);
if (loc >= 0) glUniform4fv(loc, MAX_PARAM_VEC4S, data);
// Default render path: ensure preview branch in the compiled DAG shader is
// disabled (per-node previews override this transiently in dag_previews_render).
GLint loc_pt = glGetUniformLocation(program, "u_preview_target");
if (loc_pt >= 0) glUniform1i(loc_pt, -1);
}
} // namespace fn::gfx
+6
View File
@@ -31,3 +31,9 @@ output: "Efecto lateral: actualiza el uniform u_params[16] en el programa GL act
## Notas
El array data[64] se inicializa a 0 antes de copiar, por lo que steps no usados quedan en cero. El caller es responsable de activar el programa antes de llamar.
## Cambios 2026-04-25 (Fase 5 + Fase 7 shaders_lab)
- **Layout dinámico**: el array global pasa de `vec4 u_params[16]` (4 floats por nodo, fijo) a `vec4 u_params[64]` (256 floats). Cada nodo ocupa `dag_vec4_count(param_count)` vec4s consecutivos. El packing usa `dag_param_layout(pipeline)` (declarada en `dag_compile.h`) para obtener el índice base por nodo, idéntico al que usa el compilador.
- **Reset de `u_preview_target`**: al final del apply, se hace `glUniform1i(u_preview_target, -1)` si el uniform existe en el programa. Esto deja la rama de preview desactivada en el render principal del Canvas DAG; `dag_previews_render` la activa transitoriamente por nodo y la deja restaurada.
- Nuevo `dag_compile_cpp_gfx` en `uses_functions` (consume `dag_param_layout`).
+22
View File
@@ -34,6 +34,17 @@ PFNGLUNIFORM4FVPROC fn_glUniform4fv = nullptr;
PFNGLUSEPROGRAMPROC fn_glUseProgram = nullptr;
PFNGLACTIVETEXTUREPROC fn_glActiveTexture = nullptr;
PFNGLGENERATEMIPMAPPROC fn_glGenerateMipmap = nullptr;
PFNGLBUFFERDATAPROC fn_glBufferData = nullptr;
PFNGLDRAWARRAYSINSTANCEDPROC fn_glDrawArraysInstanced = nullptr;
PFNGLENABLEVERTEXATTRIBARRAYPROC fn_glEnableVertexAttribArray = nullptr;
PFNGLVERTEXATTRIBDIVISORPROC fn_glVertexAttribDivisor = nullptr;
PFNGLVERTEXATTRIBPOINTERPROC fn_glVertexAttribPointer = nullptr;
PFNGLBINDRENDERBUFFERPROC fn_glBindRenderbuffer = nullptr;
PFNGLDELETERENDERBUFFERSPROC fn_glDeleteRenderbuffers = nullptr;
PFNGLFRAMEBUFFERRENDERBUFFERPROC fn_glFramebufferRenderbuffer = nullptr;
PFNGLGENRENDERBUFFERSPROC fn_glGenRenderbuffers = nullptr;
PFNGLRENDERBUFFERSTORAGEPROC fn_glRenderbufferStorage = nullptr;
PFNGLFRAMEBUFFERTEXTUREPROC fn_glFramebufferTexture = nullptr;
namespace fn::gfx {
@@ -74,6 +85,17 @@ bool gl_loader_init() {
LOAD(glUseProgram);
LOAD(glActiveTexture);
LOAD(glGenerateMipmap);
LOAD(glBufferData);
LOAD(glDrawArraysInstanced);
LOAD(glEnableVertexAttribArray);
LOAD(glVertexAttribDivisor);
LOAD(glVertexAttribPointer);
LOAD(glBindRenderbuffer);
LOAD(glDeleteRenderbuffers);
LOAD(glFramebufferRenderbuffer);
LOAD(glGenRenderbuffers);
LOAD(glRenderbufferStorage);
LOAD(glFramebufferTexture);
#undef LOAD
return true;
+25
View File
@@ -38,8 +38,22 @@
extern PFNGLUNIFORM4FPROC fn_glUniform4f;
extern PFNGLUNIFORM4FVPROC fn_glUniform4fv;
extern PFNGLUSEPROGRAMPROC fn_glUseProgram;
// Texture (gl_texture_load — issue 0026)
extern PFNGLACTIVETEXTUREPROC fn_glActiveTexture;
extern PFNGLGENERATEMIPMAPPROC fn_glGenerateMipmap;
// Buffers / VAO data + draw + vertex attributes (graph_renderer)
extern PFNGLBUFFERDATAPROC fn_glBufferData;
extern PFNGLDRAWARRAYSINSTANCEDPROC fn_glDrawArraysInstanced;
extern PFNGLENABLEVERTEXATTRIBARRAYPROC fn_glEnableVertexAttribArray;
extern PFNGLVERTEXATTRIBDIVISORPROC fn_glVertexAttribDivisor;
extern PFNGLVERTEXATTRIBPOINTERPROC fn_glVertexAttribPointer;
// Renderbuffer / framebuffer texture
extern PFNGLBINDRENDERBUFFERPROC fn_glBindRenderbuffer;
extern PFNGLDELETERENDERBUFFERSPROC fn_glDeleteRenderbuffers;
extern PFNGLFRAMEBUFFERRENDERBUFFERPROC fn_glFramebufferRenderbuffer;
extern PFNGLGENRENDERBUFFERSPROC fn_glGenRenderbuffers;
extern PFNGLRENDERBUFFERSTORAGEPROC fn_glRenderbufferStorage;
extern PFNGLFRAMEBUFFERTEXTUREPROC fn_glFramebufferTexture; // sin "2D"
#define glAttachShader fn_glAttachShader
#define glBindBuffer fn_glBindBuffer
@@ -73,6 +87,17 @@
#define glUseProgram fn_glUseProgram
#define glActiveTexture fn_glActiveTexture
#define glGenerateMipmap fn_glGenerateMipmap
#define glBufferData fn_glBufferData
#define glDrawArraysInstanced fn_glDrawArraysInstanced
#define glEnableVertexAttribArray fn_glEnableVertexAttribArray
#define glVertexAttribDivisor fn_glVertexAttribDivisor
#define glVertexAttribPointer fn_glVertexAttribPointer
#define glBindRenderbuffer fn_glBindRenderbuffer
#define glDeleteRenderbuffers fn_glDeleteRenderbuffers
#define glFramebufferRenderbuffer fn_glFramebufferRenderbuffer
#define glGenRenderbuffers fn_glGenRenderbuffers
#define glRenderbufferStorage fn_glRenderbufferStorage
#define glFramebufferTexture fn_glFramebufferTexture
#else
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
+19 -1
View File
@@ -3,7 +3,7 @@ name: gl_loader
kind: function
lang: cpp
domain: gfx
version: "1.0.0"
version: "1.1.0"
purity: impure
signature: "bool gl_loader_init()"
description: "Loader minimo de simbolos OpenGL 2.0+ para cross-compile a Windows. En Linux es no-op (simbolos resueltos via GL_GLEXT_PROTOTYPES). En Windows resuelve punteros con wglGetProcAddress. Redirige las llamadas con macros para que el codigo fuente sea portable."
@@ -52,3 +52,21 @@ En Linux, el header activa `GL_GLEXT_PROTOTYPES` e incluye `<GL/gl.h>` + `<GL/gl
1. Declarar `extern PFNGL<NAME>PROC fn_gl<Name>;` en el `.h`.
2. Anadir `#define gl<Name> fn_gl<Name>` en el bloque `#ifdef _WIN32`.
3. Instanciar el puntero en el `.cpp` y anadir `LOAD(gl<Name>);` dentro de `gl_loader_init()`.
## Cobertura `[v1.1]`
Funciones cubiertas (todas con macro `#define gl* fn_gl*` y `LOAD()` en el init):
| Grupo | Simbolos |
|---|---|
| Shaders / programs | `glCreateShader`, `glShaderSource`, `glCompileShader`, `glGetShaderiv`, `glGetShaderInfoLog`, `glCreateProgram`, `glAttachShader`, `glLinkProgram`, `glGetProgramiv`, `glGetProgramInfoLog`, `glUseProgram`, `glDeleteShader`, `glDeleteProgram` |
| Uniforms | `glGetUniformLocation`, `glUniform1f`, `glUniform1i`, `glUniform2f`, `glUniform3f`, `glUniform4f`, `glUniform4fv` |
| Buffers + VAO | `glGenBuffers`, `glBindBuffer`, `glDeleteBuffers`, `glBufferData`, `glGenVertexArrays`, `glBindVertexArray`, `glDeleteVertexArrays`, `glEnableVertexAttribArray`, `glVertexAttribPointer`, `glVertexAttribDivisor` |
| Framebuffers + renderbuffers | `glGenFramebuffers`, `glBindFramebuffer`, `glDeleteFramebuffers`, `glFramebufferTexture`, `glFramebufferTexture2D`, `glGenRenderbuffers`, `glBindRenderbuffer`, `glDeleteRenderbuffers`, `glRenderbufferStorage`, `glFramebufferRenderbuffer` |
| Draw | `glDrawArraysInstanced` (resto de `glDraw*` viene en `opengl32.dll`) |
`v1.1` (2026-04-25) anade los grupos **Buffers/VAO**, **Framebuffers/renderbuffers** y **Draw** para que `graph_renderer_cpp_viz` y otros consumidores compilen en cross-compile MinGW. Funciones de `opengl32.dll` 1.1 (`glClear`, `glEnable`, `glViewport`, `glDrawArrays`, etc.) se siguen resolviendo estaticamente — no necesitan loader.
## Compilador MinGW
El cross-compile a Windows requiere MinGW-w64 con thread model `-posix` para que `std::mutex` / `std::thread` funcionen (otros primitivos como `process_runner` y `toast` lo necesitan). Configurado en `cpp/toolchains/mingw-w64.cmake` via `x86_64-w64-mingw32-gcc-posix` / `g++-posix` + link static de `libwinpthread`.