feat(cpp/gfx): code_to_generator + shaderlab_db

Dos primitivas del pipeline de shaders_lab que ya estaban en uso pero sin
indexar al registry:

- code_to_generator_cpp_gfx (function, pure)
  Traduce un fragment shader GLSL escrito a mano (modo Code, con void main()
  + fragColor = ...) en un body de DAG Gen + DagControl[]. Cada uniform
  anotado se convierte en un control; el body usa el parametro uv y reemplaza
  fragColor= por return. Empaqueta uniforms en vec4 (4 x n_uniforms).

- shaderlab_db_cpp_gfx (function, impure)
  CRUD persistente para generators custom de shaders_lab via sqlite3.
  Guarda el GLSL original, el body traducido para el DAG, los DagControl y
  los param_defaults en una BD local (shaders_lab.db). Soporta open(:memory:)
  para tests.

Ambas se indexan ahora en registry.db y son reusables fuera de shaders_lab
si en el futuro hay otra app que componga DAGs de shaders.
This commit is contained in:
2026-04-25 21:26:03 +02:00
parent 8d28faf3e8
commit a9045d45a0
6 changed files with 1012 additions and 0 deletions
+400
View File
@@ -0,0 +1,400 @@
#include "gfx/code_to_generator.h"
#include "gfx/uniform_parser.h"
#include <regex>
#include <sstream>
#include <string>
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<size_t>(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 = <expr>;` (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<int>(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 <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. 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