#include "gfx/dag_compile.h" #include "gfx/dag_catalog.h" #include #include #include 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 dag_param_layout(const std::vector& pipeline) { std::vector 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(def->param_defaults.size()) : 0; base[i] = cursor; cursor += dag_vec4_count(pc); } return base; } std::string compile_dag_to_glsl(const std::vector& pipeline) { const int n = static_cast(std::min(pipeline.size(), static_cast(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_\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 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(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(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(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(k)]; if (!sid.empty()) { for (int j = 0; j < i; ++j) { if (pipeline[static_cast(j)].id == sid) { const DagNodeDef* jdef = dag_find(pipeline[static_cast(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_, emit it // and bail out before the Output-driven fragColor. for (int i = 0; i < n; ++i) { const DagStep& step = pipeline[static_cast(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(output_idx)].source_ids[0]; int src = -1; if (!sid.empty()) { for (int j = 0; j < output_idx; ++j) { if (pipeline[static_cast(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& 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(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 data(static_cast(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(def->param_defaults.size()); int b = base[i] * 4; for (int k = 0; k < pc && k < static_cast(pipeline[i].params.size()); ++k) { data[static_cast(b + k)] = pipeline[i].params[static_cast(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(i * 4 + 0)] << ", " << data[static_cast(i * 4 + 1)] << ", " << data[static_cast(i * 4 + 2)] << ", " << data[static_cast(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 #include 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 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 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 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 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 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 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 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 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