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 37e8139c5b
commit 53402d84d5
35 changed files with 1621 additions and 336 deletions
+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