b093c898a8
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>
302 lines
12 KiB
C++
302 lines
12 KiB
C++
#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_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[" << MAX_PARAM_VEC4S << "];\n";
|
||
out << "uniform int u_preview_target; // -1 = real Output; >=0 = show out_<i>\n\n";
|
||
|
||
if (n == 0) {
|
||
out << "void main() {\n";
|
||
out << " vec2 uv = gl_FragCoord.xy / u_resolution;\n";
|
||
out << " (void)uv;\n";
|
||
out << " fragColor = vec4(0.04, 0.04, 0.06, 1.0);\n";
|
||
out << "}\n";
|
||
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)];
|
||
const DagNodeDef* def = dag_find(step.name);
|
||
if (!def) continue;
|
||
if (def->kind == DagKind::Output) continue;
|
||
|
||
int ni = def->num_inputs;
|
||
out << "vec4 node_" << i << "(";
|
||
if (ni >= 1) out << "vec4 a";
|
||
if (ni >= 2) out << ", vec4 b";
|
||
if (ni >= 3) out << ", vec4 c";
|
||
if (ni >= 4) out << ", vec4 d";
|
||
if (ni > 0) out << ", ";
|
||
out << "vec2 uv) {\n";
|
||
out << def->body_glsl(base[static_cast<size_t>(i)]) << "\n";
|
||
out << "}\n\n";
|
||
}
|
||
|
||
out << "void main() {\n";
|
||
out << " vec2 uv = gl_FragCoord.xy / u_resolution;\n";
|
||
|
||
int last_valid_out = -1;
|
||
int output_idx = -1;
|
||
|
||
for (int i = 0; i < n; ++i) {
|
||
const DagStep& step = pipeline[static_cast<size_t>(i)];
|
||
const DagNodeDef* def = dag_find(step.name);
|
||
if (!def) continue;
|
||
if (def->kind == DagKind::Output) { output_idx = i; continue; }
|
||
|
||
int ni = def->num_inputs;
|
||
|
||
auto resolve = [&](int k) -> std::string {
|
||
const std::string& sid = step.source_ids[static_cast<size_t>(k)];
|
||
if (!sid.empty()) {
|
||
for (int j = 0; j < i; ++j) {
|
||
if (pipeline[static_cast<size_t>(j)].id == sid) {
|
||
const DagNodeDef* jdef = dag_find(pipeline[static_cast<size_t>(j)].name);
|
||
if (jdef && jdef->kind == DagKind::Output) continue; // Output has no out_j
|
||
return "out_" + std::to_string(j);
|
||
}
|
||
}
|
||
}
|
||
// 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 << "(";
|
||
for (int k = 0; k < ni; ++k) {
|
||
if (k > 0) out << ", ";
|
||
out << resolve(k);
|
||
}
|
||
if (ni > 0) out << ", ";
|
||
out << "uv);\n";
|
||
|
||
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.
|
||
for (int i = 0; i < n; ++i) {
|
||
const DagStep& step = pipeline[static_cast<size_t>(i)];
|
||
const DagNodeDef* def = dag_find(step.name);
|
||
if (!def) continue;
|
||
if (def->kind == DagKind::Output) continue;
|
||
out << " if (u_preview_target == " << i << ") { fragColor = out_" << i << "; return; }\n";
|
||
}
|
||
|
||
// 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;
|
||
if (!sid.empty()) {
|
||
for (int j = 0; j < output_idx; ++j) {
|
||
if (pipeline[static_cast<size_t>(j)].id == sid) { src = j; break; }
|
||
}
|
||
}
|
||
if (src >= 0) out << " fragColor = out_" << src << ";\n";
|
||
else seed();
|
||
} else {
|
||
seed();
|
||
}
|
||
|
||
out << "}\n";
|
||
|
||
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
|
||
#include <cassert>
|
||
#include <cstdio>
|
||
|
||
static bool contains(const std::string& hay, const std::string& needle) {
|
||
return hay.find(needle) != std::string::npos;
|
||
}
|
||
|
||
int main() {
|
||
using namespace fn::gfx;
|
||
|
||
// 1. Empty pipeline → seed color
|
||
{
|
||
std::vector<DagStep> p;
|
||
auto s = compile_dag_to_glsl(p);
|
||
assert(contains(s, "fragColor = vec4(0.04"));
|
||
}
|
||
|
||
// 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 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 + 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"));
|
||
}
|
||
|
||
// 4. Blend with multi-source → both inputs resolved
|
||
{
|
||
std::vector<DagStep> p;
|
||
DagStep a; a.id = "a"; a.name = "plasma"; p.push_back(a);
|
||
DagStep b; b.id = "b"; b.name = "solid"; p.push_back(b);
|
||
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
|
||
{
|
||
std::vector<DagStep> p;
|
||
DagStep g1; g1.id = "g1"; g1.name = "plasma"; p.push_back(g1);
|
||
DagStep g2; g2.id = "g2"; g2.name = "solid"; p.push_back(g2);
|
||
DagStep o; o.id = "o"; o.name = "output";
|
||
o.source_ids[0] = "g1"; // connect Output to the plasma (first gen), not last
|
||
p.push_back(o);
|
||
auto s = compile_dag_to_glsl(p);
|
||
// Output must NOT emit a node_2 function
|
||
assert(!contains(s, "vec4 node_2("));
|
||
// fragColor must come from out_0 (plasma), not out_1 (solid)
|
||
assert(contains(s, "fragColor = out_0"));
|
||
}
|
||
|
||
// 6. Output with no connection → seed fallback
|
||
{
|
||
std::vector<DagStep> p;
|
||
DagStep g; g.id = "g"; g.name = "plasma"; p.push_back(g);
|
||
DagStep o; o.id = "o"; o.name = "output"; p.push_back(o); // no source
|
||
auto s = compile_dag_to_glsl(p);
|
||
assert(contains(s, "fragColor = vec4(0.04"));
|
||
}
|
||
|
||
// 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
|