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:
@@ -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
|
||||
Reference in New Issue
Block a user