#include "gfx/code_to_generator.h" #include "gfx/uniform_parser.h" #include #include #include namespace fn::gfx { // Replace every occurrence of `from` with `to` in `s`. static void replace_all(std::string& s, const std::string& from, const std::string& to) { if (from.empty()) return; size_t p = 0; while ((p = s.find(from, p)) != std::string::npos) { s.replace(p, from.size(), to); p += to.size(); } } // Extract the body of `void main() { ... }` (content between the outermost braces). // Returns empty string if no balanced body is found. static std::string extract_main_body(const std::string& source) { static const std::regex main_re(R"(\bvoid\s+main\s*\(\s*\)\s*\{)"); std::smatch m; if (!std::regex_search(source, m, main_re)) return ""; size_t start = static_cast(m.position(0)) + m.length(0); // after the '{' int depth = 1; size_t end = start; for (; end < source.size() && depth > 0; ++end) { if (source[end] == '{') ++depth; else if (source[end] == '}') --depth; if (depth == 0) break; } if (depth != 0) return ""; return source.substr(start, end - start); } // Strip lines that match `vec2 uv = ;` (the function already provides uv). static std::string strip_uv_decl(const std::string& body) { static const std::regex uv_re(R"(^\s*vec2\s+uv\s*=\s*[^;]+;\s*$)"); std::ostringstream out; std::istringstream in(body); std::string line; bool first = true; while (std::getline(in, line)) { if (std::regex_match(line, uv_re)) continue; if (!first) out << '\n'; out << line; first = false; } return out.str(); } // Replace `fragColor = X;` with `return X;`. Tolerant to whitespace; one-shot regex. static std::string fragcolor_to_return(const std::string& body) { static const std::regex fc_re(R"(\bfragColor\s*=\s*([^;]+);)"); return std::regex_replace(body, fc_re, "return $1;"); } // GLSL type info for a uniform: alias declaration, control kind, slot count. struct TypeInfo { bool ok = true; const char* alias_type = "float"; // GLSL keyword const char* alias_swizzle = ".x"; // suffix on u_params[k] DagControl::Kind ctrl_kind = DagControl::Kind::Slider; int components = 1; // floats consumed }; static TypeInfo type_info_for(GLSLType t, WidgetKind w) { TypeInfo ti; switch (t) { case GLSLType::Float: ti.alias_type = "float"; ti.alias_swizzle = ".x"; ti.ctrl_kind = DagControl::Kind::Slider; ti.components = 1; break; case GLSLType::Int: ti.alias_type = "int"; ti.alias_swizzle = ".x"; ti.ctrl_kind = DagControl::Kind::Slider; ti.components = 1; break; case GLSLType::Bool: ti.alias_type = "bool"; ti.alias_swizzle = ".x"; ti.ctrl_kind = DagControl::Kind::Slider; // 0/1 slider ti.components = 1; break; case GLSLType::Vec2: ti.alias_type = "vec2"; ti.alias_swizzle = ".xy"; ti.ctrl_kind = DagControl::Kind::XY; ti.components = 2; break; case GLSLType::Vec3: ti.alias_type = "vec3"; ti.alias_swizzle = ".xyz"; ti.ctrl_kind = DagControl::Kind::Color; ti.components = 3; break; case GLSLType::Vec4: // Treat as Color if widget says so, otherwise unsupported (KISS). if (w == WidgetKind::Color) { ti.alias_type = "vec4"; ti.alias_swizzle = ""; ti.ctrl_kind = DagControl::Kind::Color; ti.components = 3; // we render only RGB; alpha sits unused } else { ti.ok = false; } break; } // Widget overrides (when annotated) if (ti.ok) { if (w == WidgetKind::Color && t != GLSLType::Vec3 && t != GLSLType::Vec4) { ti.ctrl_kind = DagControl::Kind::Slider; // can't color a non-vec3/4 } if (w == WidgetKind::Toggle) { ti.ctrl_kind = DagControl::Kind::Slider; } if (w == WidgetKind::XY && t == GLSLType::Vec2) { ti.ctrl_kind = DagControl::Kind::XY; } } return ti; } // Special-case: a vec4 typed as Color has alias `.xyz` for the local declared as vec3? // We keep the alias as the full vec4 to avoid changing the user's body, and treat the // control as Color over the first 3 components. static const char* alias_swizzle_for(GLSLType t) { switch (t) { case GLSLType::Float: case GLSLType::Int: case GLSLType::Bool: return ".x"; case GLSLType::Vec2: return ".xy"; case GLSLType::Vec3: return ".xyz"; case GLSLType::Vec4: return ""; } return ""; } CodeToGeneratorResult code_to_generator(const std::string& source) { CodeToGeneratorResult r; auto descs = parse_uniforms(source); // Reject sampler2D and other unsupported forms by inspecting raw source. // parse_uniforms already drops sampler2D silently; warn the user explicitly. if (source.find("sampler2D") != std::string::npos) { r.err = "sampler2D uniforms are not supported in saved generators"; return r; } if (descs.empty() && source.find("uniform") != std::string::npos) { // Has uniforms but parser found none → unsupported types. // (Could be more granular; for now just warn.) } if (descs.size() > 16) { r.err = "too many uniforms (max 16 per generator)"; return r; } std::string body_main = extract_main_body(source); if (body_main.empty()) { r.err = "could not find `void main() { ... }` body"; return r; } if (body_main.find("fragColor") == std::string::npos) { r.err = "body must assign `fragColor` at least once"; return r; } // Build aliases (preamble) and DagControls. std::ostringstream pre; int slot = 0; for (const auto& d : descs) { TypeInfo ti = type_info_for(d.glsl_type, d.widget); if (!ti.ok) { r.err = "uniform '" + d.name + "': vec4 only supported with @color"; return r; } // Local alias: float u_speed = u_params[__BASE__+slot].x; pre << " " << ti.alias_type << ' ' << d.name << " = " << ti.alias_type << "(u_params[__BASE__+" << slot << "]" << alias_swizzle_for(d.glsl_type) << ");\n"; // Defaults: pack components into the 4 floats of this slot const int base_float = slot * 4; for (int c = 0; c < ti.components && c < 4; ++c) { r.param_defaults.push_back(d.defaults[c]); } for (int c = ti.components; c < 4; ++c) { r.param_defaults.push_back(0.0f); } // Param names: NAME, NAME.y, NAME.z... (for debug only) r.param_names.push_back(d.name); for (int c = 1; c < 4; ++c) r.param_names.push_back(""); // Build DagControl DagControl ctrl; ctrl.kind = ti.ctrl_kind; ctrl.label = d.name; ctrl.min = d.min[0]; ctrl.max = d.max[0]; ctrl.step = d.step; if (ctrl.kind == DagControl::Kind::Slider) { ctrl.param_idx = { base_float, -1, -1 }; } else if (ctrl.kind == DagControl::Kind::XY) { ctrl.param_idx = { base_float, base_float + 1, -1 }; } else if (ctrl.kind == DagControl::Kind::Color) { ctrl.param_idx = { base_float, base_float + 1, base_float + 2 }; ctrl.min = 0.0f; ctrl.max = 1.0f; } r.controls.push_back(std::move(ctrl)); ++slot; } r.param_count = static_cast(r.param_defaults.size()); // Transform body: strip uv decl + fragColor → return. std::string body = strip_uv_decl(body_main); body = fragcolor_to_return(body); // Final body_template: preamble (aliases) + transformed body. std::ostringstream tpl; tpl << pre.str() << body; r.body_template = tpl.str(); r.ok = true; return r; } DagNodeDef make_generator_def(const std::string& name, const std::string& label, const std::string& desc, const CodeToGeneratorResult& tr) { DagNodeDef d; d.name = name; d.label = label.empty() ? name : label; d.desc = desc; d.kind = DagKind::Gen; d.num_inputs = 0; d.param_names = tr.param_names; d.param_defaults = tr.param_defaults; d.controls = tr.controls; d.is_builtin = false; const std::string body_template = tr.body_template; d.body_glsl = [body_template](int base) -> std::string { std::string s = body_template; replace_all(s, "__BASE__", std::to_string(base)); return s; }; return d; } } // namespace fn::gfx #ifdef CODE_TO_GENERATOR_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. Plasma-like Code: float speed + vec3 color → 2 controls (slider + color) { const char* src = R"glsl( uniform float u_speed; // @slider min=0.1 max=5 default=1 uniform vec3 u_color; // @color default=0.5,0.2,0.8 void main() { vec2 uv = gl_FragCoord.xy / u_resolution; float t = u_time * u_speed; vec3 c = u_color * (0.5 + 0.5 * cos(t + uv.xyx + vec3(0.0, 2.0, 4.0))); fragColor = vec4(c, 1.0); } )glsl"; auto r = code_to_generator(src); assert(r.ok); assert(r.controls.size() == 2); assert(r.controls[0].kind == DagControl::Kind::Slider); assert(r.controls[0].label == "u_speed"); assert(r.controls[0].param_idx[0] == 0); assert(r.controls[0].max == 5.0f); assert(r.controls[1].kind == DagControl::Kind::Color); assert(r.controls[1].label == "u_color"); assert(r.controls[1].param_idx[0] == 4); assert(r.controls[1].param_idx[2] == 6); assert(r.param_count == 8); // 2 uniforms × 4 floats assert(r.param_defaults.size() == 8); assert(r.param_defaults[0] == 1.0f); // u_speed default assert(r.param_defaults[4] == 0.5f); // u_color.r assert(r.param_defaults[5] == 0.2f); assert(r.param_defaults[6] == 0.8f); // body_template: should declare aliases referencing __BASE__ assert(contains(r.body_template, "float u_speed")); assert(contains(r.body_template, "u_params[__BASE__+0]")); assert(contains(r.body_template, "vec3 u_color")); assert(contains(r.body_template, "u_params[__BASE__+1]")); // uv = ... line stripped assert(!contains(r.body_template, "vec2 uv = gl_FragCoord")); // fragColor → return assert(contains(r.body_template, "return vec4(c, 1.0);")); assert(!contains(r.body_template, "fragColor =")); } // 2. make_generator_def + lambda substitution { const char* src = R"glsl( uniform float u_x; // @slider min=0 max=1 default=0.5 void main() { fragColor = vec4(u_x, 0.0, 0.0, 1.0); } )glsl"; auto tr = code_to_generator(src); assert(tr.ok); DagNodeDef def = make_generator_def("test_gen", "Test", "desc", tr); assert(!def.is_builtin); assert(def.kind == DagKind::Gen); assert(def.num_inputs == 0); std::string b3 = def.body_glsl(3); assert(contains(b3, "u_params[3+0]")); assert(!contains(b3, "__BASE__")); std::string b0 = def.body_glsl(0); assert(contains(b0, "u_params[0+0]")); } // 3. Missing void main → error { auto r = code_to_generator("uniform float u_x;\n"); assert(!r.ok); assert(r.err.find("void main") != std::string::npos); } // 4. No fragColor → error { auto r = code_to_generator("void main() { }\n"); assert(!r.ok); assert(r.err.find("fragColor") != std::string::npos); } // 5. sampler2D rejected { const char* src = R"glsl( uniform sampler2D u_tex; void main() { fragColor = vec4(1.0); } )glsl"; auto r = code_to_generator(src); assert(!r.ok); assert(r.err.find("sampler2D") != std::string::npos); } // 6. vec2 with @xy → XY control { const char* src = R"glsl( uniform vec2 u_pos; // @xy min=-1 max=1 default=0,0 void main() { fragColor = vec4(u_pos, 0.0, 1.0); } )glsl"; auto r = code_to_generator(src); assert(r.ok); assert(r.controls.size() == 1); assert(r.controls[0].kind == DagControl::Kind::XY); assert(r.controls[0].param_idx[0] == 0); assert(r.controls[0].param_idx[1] == 1); assert(r.controls[0].min == -1.0f); assert(r.controls[0].max == 1.0f); assert(contains(r.body_template, "vec2 u_pos")); assert(contains(r.body_template, "u_params[__BASE__+0].xy")); } // 7. No uniforms at all → empty controls, body_template still ok { const char* src = R"glsl( void main() { vec2 uv = gl_FragCoord.xy / u_resolution; fragColor = vec4(uv, 0.0, 1.0); } )glsl"; auto r = code_to_generator(src); assert(r.ok); assert(r.controls.empty()); assert(r.param_count == 0); assert(contains(r.body_template, "return vec4(uv, 0.0, 1.0);")); } std::printf("code_to_generator: 7/7 asserts passed\n"); return 0; } #endif