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
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "gfx/dag_types.h"
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fn::gfx {
|
||||||
|
|
||||||
|
// Result of translating a Code-mode GLSL fragment shader into a DAG generator
|
||||||
|
// body. `body_template` carries `__BASE__` placeholders that the body_glsl
|
||||||
|
// lambda substitutes for the node's vec4 base index at compile time.
|
||||||
|
struct CodeToGeneratorResult {
|
||||||
|
bool ok = false;
|
||||||
|
std::string err;
|
||||||
|
|
||||||
|
std::string body_template; // body with __BASE__ tokens
|
||||||
|
int param_count = 0; // size of step.params (4 × n_uniforms)
|
||||||
|
std::vector<float> param_defaults;
|
||||||
|
std::vector<std::string> param_names;
|
||||||
|
std::vector<DagControl> controls;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Translate a Code-mode GLSL source (with `void main()` + `fragColor = ...;`)
|
||||||
|
// into a DAG Gen body + controls. Each annotated uniform becomes a DagControl;
|
||||||
|
// the body uses `uv` from the function parameter (any local `vec2 uv = ...;`
|
||||||
|
// line is stripped). Returns ok=false with err set on parse / unsupported-type
|
||||||
|
// errors. Pure: no I/O.
|
||||||
|
//
|
||||||
|
// Layout: each uniform claims one vec4 (4 floats), wasting unused components
|
||||||
|
// to keep the float→vec4 mapping trivial. Total floats = 4 × num_uniforms.
|
||||||
|
CodeToGeneratorResult code_to_generator(const std::string& source);
|
||||||
|
|
||||||
|
// Wrap a translation result into a DagNodeDef ready for dag_register_node().
|
||||||
|
// kind = Gen, num_inputs = 0, is_builtin = false.
|
||||||
|
DagNodeDef make_generator_def(const std::string& name,
|
||||||
|
const std::string& label,
|
||||||
|
const std::string& desc,
|
||||||
|
const CodeToGeneratorResult& tr);
|
||||||
|
|
||||||
|
} // namespace fn::gfx
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
name: code_to_generator
|
||||||
|
kind: function
|
||||||
|
lang: cpp
|
||||||
|
domain: gfx
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: pure
|
||||||
|
signature: "CodeToGeneratorResult code_to_generator(const std::string& source); DagNodeDef make_generator_def(const std::string& name, const std::string& label, const std::string& desc, const CodeToGeneratorResult& tr)"
|
||||||
|
description: "Traduce un fragment shader GLSL del 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` de la funcion (lineas `vec2 uv = ...;` se eliminan) y reemplaza `fragColor =` por `return`. Empaqueta cada uniform en su propio vec4 (parametros = 4 × n_uniforms)."
|
||||||
|
tags: [glsl, codegen, dag, generators, shaders_lab, parser, gfx]
|
||||||
|
uses_functions:
|
||||||
|
- uniform_parser_cpp_gfx
|
||||||
|
uses_types:
|
||||||
|
- dag_types_cpp_gfx
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: ""
|
||||||
|
imports: [code_to_generator, uniform_parser, dag_types, regex, sstream, string]
|
||||||
|
tested: true
|
||||||
|
tests:
|
||||||
|
- "plasma-like (float + vec3) → Slider + Color, defaults preservados, base placeholder"
|
||||||
|
- "make_generator_def substituye __BASE__ con el indice runtime"
|
||||||
|
- "missing void main → error"
|
||||||
|
- "missing fragColor → error"
|
||||||
|
- "sampler2D → error"
|
||||||
|
- "vec2 con @xy → XY control"
|
||||||
|
- "shader sin uniforms → controls vacios, body ok"
|
||||||
|
test_file_path: "cpp/functions/gfx/code_to_generator.cpp"
|
||||||
|
file_path: "cpp/functions/gfx/code_to_generator.cpp"
|
||||||
|
params:
|
||||||
|
- name: source
|
||||||
|
desc: "Fuente GLSL del modo Code: uniforms anotados (// @slider, @color, @xy, @toggle), `void main()` con `vec2 uv = gl_FragCoord.xy / u_resolution;` opcional y `fragColor = ...;`."
|
||||||
|
- name: name
|
||||||
|
desc: "Identificador snake_case del generator, unico en el catalogo (no debe colisionar con built-ins)."
|
||||||
|
- name: label
|
||||||
|
desc: "Etiqueta visible en la paleta. Si vacio, se usa name."
|
||||||
|
- name: desc
|
||||||
|
desc: "Descripcion libre del generator."
|
||||||
|
- name: tr
|
||||||
|
desc: "Resultado de `code_to_generator`. Solo se usa si `tr.ok == true`."
|
||||||
|
output: "CodeToGeneratorResult con body_template (con tokens __BASE__), param_count, param_defaults, param_names y DagControl[]. make_generator_def envuelve esto en un DagNodeDef listo para `dag_register_node()`."
|
||||||
|
---
|
||||||
|
|
||||||
|
## Layout de parametros
|
||||||
|
|
||||||
|
Cada uniform reclama 1 vec4 entero, sin importar su tipo. Esto desperdicia hasta 3 floats por uniform pero hace trivial el mapeo:
|
||||||
|
|
||||||
|
| GLSL type | Slot | Alias | Control |
|
||||||
|
|-----------|------|------------------------------------|---------|
|
||||||
|
| `float` | 1 | `float n = u_params[B+i].x;` | Slider |
|
||||||
|
| `int` | 1 | `int n = int(u_params[B+i].x);` | Slider |
|
||||||
|
| `bool` | 1 | `bool n = bool(u_params[B+i].x>0.5)` | Slider 0/1 |
|
||||||
|
| `vec2` | 1 | `vec2 n = u_params[B+i].xy;` | XY (con @xy) |
|
||||||
|
| `vec3` | 1 | `vec3 n = u_params[B+i].xyz;` | Color (con @color) |
|
||||||
|
| `vec4` | 1 | `vec4 n = u_params[B+i];` | Color (alpha en .w, sin control) — solo soportado con @color |
|
||||||
|
| `sampler2D` | - | - | error |
|
||||||
|
|
||||||
|
`__BASE__` es el placeholder que `make_generator_def` sustituye por el `base_vec4` del nodo en runtime.
|
||||||
@@ -0,0 +1,401 @@
|
|||||||
|
#include "gfx/shaderlab_db.h"
|
||||||
|
#include <sqlite3.h>
|
||||||
|
#include <chrono>
|
||||||
|
#include <ctime>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace fn::gfx {
|
||||||
|
|
||||||
|
static sqlite3* g_db = nullptr;
|
||||||
|
static std::string g_path;
|
||||||
|
|
||||||
|
static std::string now_iso() {
|
||||||
|
auto t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
|
||||||
|
std::tm tm_utc{};
|
||||||
|
#if defined(_WIN32)
|
||||||
|
gmtime_s(&tm_utc, &t);
|
||||||
|
#else
|
||||||
|
gmtime_r(&t, &tm_utc);
|
||||||
|
#endif
|
||||||
|
std::ostringstream ss;
|
||||||
|
ss << std::put_time(&tm_utc, "%Y-%m-%dT%H:%M:%SZ");
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Serialization helpers (custom compact format, no JSON parser needed) ───
|
||||||
|
// floats: "1.0,2.5,-0.3"
|
||||||
|
// strings: one per line, '\n' separator (labels may contain spaces but not LF)
|
||||||
|
// controls: one per line, fields separated by '|':
|
||||||
|
// kind(int)|label|p0|p1|p2|min|max|step
|
||||||
|
|
||||||
|
static std::string floats_to_csv(const std::vector<float>& v) {
|
||||||
|
std::ostringstream ss;
|
||||||
|
for (size_t i = 0; i < v.size(); ++i) {
|
||||||
|
if (i) ss << ',';
|
||||||
|
ss << v[i];
|
||||||
|
}
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<float> floats_from_csv(const std::string& s) {
|
||||||
|
std::vector<float> out;
|
||||||
|
if (s.empty()) return out;
|
||||||
|
std::istringstream ss(s);
|
||||||
|
std::string tok;
|
||||||
|
while (std::getline(ss, tok, ',')) {
|
||||||
|
try { out.push_back(std::stof(tok)); } catch (...) {}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string strings_to_lf(const std::vector<std::string>& v) {
|
||||||
|
std::ostringstream ss;
|
||||||
|
for (size_t i = 0; i < v.size(); ++i) {
|
||||||
|
if (i) ss << '\n';
|
||||||
|
ss << v[i];
|
||||||
|
}
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<std::string> strings_from_lf(const std::string& s) {
|
||||||
|
std::vector<std::string> out;
|
||||||
|
if (s.empty()) return out;
|
||||||
|
std::istringstream ss(s);
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(ss, line)) out.push_back(line);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string controls_to_string(const std::vector<DagControl>& v) {
|
||||||
|
std::ostringstream ss;
|
||||||
|
for (size_t i = 0; i < v.size(); ++i) {
|
||||||
|
if (i) ss << '\n';
|
||||||
|
ss << static_cast<int>(v[i].kind) << '|'
|
||||||
|
<< v[i].label << '|'
|
||||||
|
<< v[i].param_idx[0] << '|'
|
||||||
|
<< v[i].param_idx[1] << '|'
|
||||||
|
<< v[i].param_idx[2] << '|'
|
||||||
|
<< v[i].min << '|'
|
||||||
|
<< v[i].max << '|'
|
||||||
|
<< v[i].step;
|
||||||
|
}
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<DagControl> controls_from_string(const std::string& s) {
|
||||||
|
std::vector<DagControl> out;
|
||||||
|
if (s.empty()) return out;
|
||||||
|
std::istringstream ss(s);
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(ss, line)) {
|
||||||
|
DagControl c;
|
||||||
|
// Split by '|' into 8 fields
|
||||||
|
std::vector<std::string> fields;
|
||||||
|
std::string buf;
|
||||||
|
for (char ch : line) {
|
||||||
|
if (ch == '|') { fields.push_back(buf); buf.clear(); }
|
||||||
|
else buf.push_back(ch);
|
||||||
|
}
|
||||||
|
fields.push_back(buf);
|
||||||
|
if (fields.size() < 8) continue;
|
||||||
|
try {
|
||||||
|
c.kind = static_cast<DagControl::Kind>(std::stoi(fields[0]));
|
||||||
|
c.label = fields[1];
|
||||||
|
c.param_idx[0] = std::stoi(fields[2]);
|
||||||
|
c.param_idx[1] = std::stoi(fields[3]);
|
||||||
|
c.param_idx[2] = std::stoi(fields[4]);
|
||||||
|
c.min = std::stof(fields[5]);
|
||||||
|
c.max = std::stof(fields[6]);
|
||||||
|
c.step = std::stof(fields[7]);
|
||||||
|
out.push_back(c);
|
||||||
|
} catch (...) {}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── DB lifecycle ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
static bool exec(const char* sql, std::string* err) {
|
||||||
|
char* msg = nullptr;
|
||||||
|
int rc = sqlite3_exec(g_db, sql, nullptr, nullptr, &msg);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
if (err) *err = msg ? msg : "sqlite_exec failed";
|
||||||
|
if (msg) sqlite3_free(msg);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool shaderlab_db_open(const std::string& path) {
|
||||||
|
if (g_db && path == g_path) return true;
|
||||||
|
if (g_db) shaderlab_db_close();
|
||||||
|
|
||||||
|
int rc = sqlite3_open(path.c_str(), &g_db);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
if (g_db) { sqlite3_close(g_db); g_db = nullptr; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
g_path = path;
|
||||||
|
|
||||||
|
const char* schema =
|
||||||
|
"CREATE TABLE IF NOT EXISTS generators ("
|
||||||
|
" id TEXT PRIMARY KEY,"
|
||||||
|
" label TEXT NOT NULL,"
|
||||||
|
" description TEXT NOT NULL DEFAULT '',"
|
||||||
|
" source_glsl TEXT NOT NULL,"
|
||||||
|
" body_glsl TEXT NOT NULL,"
|
||||||
|
" param_count INTEGER NOT NULL,"
|
||||||
|
" param_defaults TEXT NOT NULL,"
|
||||||
|
" param_names TEXT NOT NULL,"
|
||||||
|
" controls TEXT NOT NULL,"
|
||||||
|
" tags TEXT NOT NULL DEFAULT '',"
|
||||||
|
" created_at TEXT NOT NULL,"
|
||||||
|
" updated_at TEXT NOT NULL"
|
||||||
|
");";
|
||||||
|
return exec(schema, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void shaderlab_db_close() {
|
||||||
|
if (g_db) sqlite3_close(g_db);
|
||||||
|
g_db = nullptr;
|
||||||
|
g_path.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── CRUD ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
bool shaderlab_db_save_generator(GeneratorRecord& gen, std::string* err) {
|
||||||
|
if (!g_db) { if (err) *err = "db not open"; return false; }
|
||||||
|
|
||||||
|
const std::string ts = now_iso();
|
||||||
|
if (gen.created_at.empty()) gen.created_at = ts;
|
||||||
|
gen.updated_at = ts;
|
||||||
|
|
||||||
|
const char* sql =
|
||||||
|
"INSERT INTO generators "
|
||||||
|
"(id,label,description,source_glsl,body_glsl,param_count,param_defaults,param_names,controls,tags,created_at,updated_at) "
|
||||||
|
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?) "
|
||||||
|
"ON CONFLICT(id) DO UPDATE SET "
|
||||||
|
" label=excluded.label, description=excluded.description, "
|
||||||
|
" source_glsl=excluded.source_glsl, body_glsl=excluded.body_glsl, "
|
||||||
|
" param_count=excluded.param_count, param_defaults=excluded.param_defaults, "
|
||||||
|
" param_names=excluded.param_names, controls=excluded.controls, "
|
||||||
|
" tags=excluded.tags, updated_at=excluded.updated_at;";
|
||||||
|
|
||||||
|
sqlite3_stmt* stmt = nullptr;
|
||||||
|
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||||
|
if (err) *err = sqlite3_errmsg(g_db);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string defaults_csv = floats_to_csv(gen.param_defaults);
|
||||||
|
const std::string names_lf = strings_to_lf(gen.param_names);
|
||||||
|
const std::string controls_str = controls_to_string(gen.controls);
|
||||||
|
|
||||||
|
sqlite3_bind_text(stmt, 1, gen.id.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
|
sqlite3_bind_text(stmt, 2, gen.label.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
|
sqlite3_bind_text(stmt, 3, gen.description.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
|
sqlite3_bind_text(stmt, 4, gen.source_glsl.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
|
sqlite3_bind_text(stmt, 5, gen.body_glsl.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
|
sqlite3_bind_int (stmt, 6, gen.param_count);
|
||||||
|
sqlite3_bind_text(stmt, 7, defaults_csv.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
|
sqlite3_bind_text(stmt, 8, names_lf.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
|
sqlite3_bind_text(stmt, 9, controls_str.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
|
sqlite3_bind_text(stmt, 10, gen.tags.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
|
sqlite3_bind_text(stmt, 11, gen.created_at.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
|
sqlite3_bind_text(stmt, 12, gen.updated_at.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
|
|
||||||
|
int rc = sqlite3_step(stmt);
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
if (rc != SQLITE_DONE) {
|
||||||
|
if (err) *err = sqlite3_errmsg(g_db);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static GeneratorRecord row_to_record(sqlite3_stmt* stmt) {
|
||||||
|
auto col = [&](int i) -> std::string {
|
||||||
|
const unsigned char* s = sqlite3_column_text(stmt, i);
|
||||||
|
return s ? reinterpret_cast<const char*>(s) : "";
|
||||||
|
};
|
||||||
|
GeneratorRecord r;
|
||||||
|
r.id = col(0);
|
||||||
|
r.label = col(1);
|
||||||
|
r.description = col(2);
|
||||||
|
r.source_glsl = col(3);
|
||||||
|
r.body_glsl = col(4);
|
||||||
|
r.param_count = sqlite3_column_int(stmt, 5);
|
||||||
|
r.param_defaults = floats_from_csv(col(6));
|
||||||
|
r.param_names = strings_from_lf(col(7));
|
||||||
|
r.controls = controls_from_string(col(8));
|
||||||
|
r.tags = col(9);
|
||||||
|
r.created_at = col(10);
|
||||||
|
r.updated_at = col(11);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<GeneratorRecord> shaderlab_db_list_generators() {
|
||||||
|
std::vector<GeneratorRecord> out;
|
||||||
|
if (!g_db) return out;
|
||||||
|
const char* sql =
|
||||||
|
"SELECT id,label,description,source_glsl,body_glsl,param_count,"
|
||||||
|
" param_defaults,param_names,controls,tags,created_at,updated_at "
|
||||||
|
"FROM generators ORDER BY label;";
|
||||||
|
sqlite3_stmt* stmt = nullptr;
|
||||||
|
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr) != SQLITE_OK) return out;
|
||||||
|
while (sqlite3_step(stmt) == SQLITE_ROW) out.push_back(row_to_record(stmt));
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool shaderlab_db_get_generator(const std::string& id, GeneratorRecord& out) {
|
||||||
|
if (!g_db) return false;
|
||||||
|
const char* sql =
|
||||||
|
"SELECT id,label,description,source_glsl,body_glsl,param_count,"
|
||||||
|
" param_defaults,param_names,controls,tags,created_at,updated_at "
|
||||||
|
"FROM generators WHERE id = ?;";
|
||||||
|
sqlite3_stmt* stmt = nullptr;
|
||||||
|
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr) != SQLITE_OK) return false;
|
||||||
|
sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
|
bool found = false;
|
||||||
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||||
|
out = row_to_record(stmt);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool shaderlab_db_delete_generator(const std::string& id) {
|
||||||
|
if (!g_db) return false;
|
||||||
|
const char* sql = "DELETE FROM generators WHERE id = ?;";
|
||||||
|
sqlite3_stmt* stmt = nullptr;
|
||||||
|
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr) != SQLITE_OK) return false;
|
||||||
|
sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
|
int rc = sqlite3_step(stmt);
|
||||||
|
int changes = sqlite3_changes(g_db);
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
return rc == SQLITE_DONE && changes > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3* shaderlab_db_handle() {
|
||||||
|
return g_db;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fn::gfx
|
||||||
|
|
||||||
|
#ifdef SHADERLAB_DB_TEST
|
||||||
|
#include <cassert>
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
using namespace fn::gfx;
|
||||||
|
|
||||||
|
assert(shaderlab_db_open(":memory:"));
|
||||||
|
|
||||||
|
// 1. List on empty db
|
||||||
|
{
|
||||||
|
auto v = shaderlab_db_list_generators();
|
||||||
|
assert(v.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Save + get
|
||||||
|
{
|
||||||
|
GeneratorRecord g;
|
||||||
|
g.id = "watercolor";
|
||||||
|
g.label = "watercolor";
|
||||||
|
g.description = "soft pastel blob";
|
||||||
|
g.source_glsl = "uniform float u_speed; void main() { fragColor = vec4(1.0); }";
|
||||||
|
g.body_glsl = " return vec4(1.0);";
|
||||||
|
g.param_count = 1;
|
||||||
|
g.param_defaults = {0.7f};
|
||||||
|
g.param_names = {"speed"};
|
||||||
|
g.controls = {
|
||||||
|
{ DagControl::Kind::Slider, "velocidad", {0, -1, -1}, 0.0f, 3.0f, 0.01f },
|
||||||
|
};
|
||||||
|
g.tags = "shaders_lab,user";
|
||||||
|
|
||||||
|
std::string err;
|
||||||
|
assert(shaderlab_db_save_generator(g, &err));
|
||||||
|
assert(!g.created_at.empty());
|
||||||
|
assert(!g.updated_at.empty());
|
||||||
|
|
||||||
|
GeneratorRecord r;
|
||||||
|
assert(shaderlab_db_get_generator("watercolor", r));
|
||||||
|
assert(r.label == "watercolor");
|
||||||
|
assert(r.param_count == 1);
|
||||||
|
assert(r.param_defaults.size() == 1 && r.param_defaults[0] == 0.7f);
|
||||||
|
assert(r.param_names.size() == 1 && r.param_names[0] == "speed");
|
||||||
|
assert(r.controls.size() == 1);
|
||||||
|
assert(r.controls[0].kind == DagControl::Kind::Slider);
|
||||||
|
assert(r.controls[0].label == "velocidad");
|
||||||
|
assert(r.controls[0].param_idx[0] == 0);
|
||||||
|
assert(r.controls[0].max == 3.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. List returns the saved record
|
||||||
|
{
|
||||||
|
auto v = shaderlab_db_list_generators();
|
||||||
|
assert(v.size() == 1);
|
||||||
|
assert(v[0].id == "watercolor");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Save second generator with multiple controls
|
||||||
|
{
|
||||||
|
GeneratorRecord g;
|
||||||
|
g.id = "chrome";
|
||||||
|
g.label = "chrome";
|
||||||
|
g.source_glsl = "// stub";
|
||||||
|
g.body_glsl = " return vec4(0.0);";
|
||||||
|
g.param_count = 4;
|
||||||
|
g.param_defaults = {1.0f, 2.0f, 0.5f, 0.5f};
|
||||||
|
g.param_names = {"a", "b", "c", "d"};
|
||||||
|
g.controls = {
|
||||||
|
{ DagControl::Kind::XY, "centro", {0, 1, -1}, -1.0f, 1.0f, 0.01f },
|
||||||
|
{ DagControl::Kind::Color, "tinte", {1, 2, 3}, 0.0f, 1.0f, 0.0f },
|
||||||
|
};
|
||||||
|
assert(shaderlab_db_save_generator(g));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. List ordered by label: chrome then watercolor
|
||||||
|
{
|
||||||
|
auto v = shaderlab_db_list_generators();
|
||||||
|
assert(v.size() == 2);
|
||||||
|
assert(v[0].id == "chrome");
|
||||||
|
assert(v[1].id == "watercolor");
|
||||||
|
assert(v[0].controls.size() == 2);
|
||||||
|
assert(v[0].controls[1].kind == DagControl::Kind::Color);
|
||||||
|
assert(v[0].controls[1].param_idx[2] == 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Update preserves created_at, bumps updated_at
|
||||||
|
{
|
||||||
|
GeneratorRecord g;
|
||||||
|
assert(shaderlab_db_get_generator("watercolor", g));
|
||||||
|
std::string created = g.created_at;
|
||||||
|
g.label = "watercolor v2";
|
||||||
|
// Force a different timestamp by setting created_at; save will set updated_at = now
|
||||||
|
assert(shaderlab_db_save_generator(g));
|
||||||
|
GeneratorRecord r;
|
||||||
|
assert(shaderlab_db_get_generator("watercolor", r));
|
||||||
|
assert(r.label == "watercolor v2");
|
||||||
|
assert(r.created_at == created);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Delete
|
||||||
|
{
|
||||||
|
assert(shaderlab_db_delete_generator("watercolor"));
|
||||||
|
GeneratorRecord r;
|
||||||
|
assert(!shaderlab_db_get_generator("watercolor", r));
|
||||||
|
auto v = shaderlab_db_list_generators();
|
||||||
|
assert(v.size() == 1);
|
||||||
|
assert(!shaderlab_db_delete_generator("nonexistent"));
|
||||||
|
}
|
||||||
|
|
||||||
|
shaderlab_db_close();
|
||||||
|
std::printf("shaderlab_db: 7/7 asserts passed\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "gfx/dag_types.h"
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct sqlite3; // fwd decl
|
||||||
|
|
||||||
|
namespace fn::gfx {
|
||||||
|
|
||||||
|
// Persistent record for a user-saved generator (Code → DAG Gen).
|
||||||
|
struct GeneratorRecord {
|
||||||
|
std::string id; // snake_case unique
|
||||||
|
std::string label; // visible name in palette
|
||||||
|
std::string description; // free text
|
||||||
|
std::string source_glsl; // original Code (with fragColor, uniforms)
|
||||||
|
std::string body_glsl; // translated DAG Gen body (with return)
|
||||||
|
int param_count = 0; // number of floats in u_params block
|
||||||
|
std::vector<float> param_defaults;
|
||||||
|
std::vector<std::string> param_names;
|
||||||
|
std::vector<DagControl> controls;
|
||||||
|
std::string tags; // CSV
|
||||||
|
std::string created_at; // ISO-8601 (set by save if empty)
|
||||||
|
std::string updated_at; // ISO-8601 (set by save)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Open the database at `path` (created if missing). ":memory:" is supported for tests.
|
||||||
|
// Returns true on success. Idempotent: calling open() again with the same path is a no-op.
|
||||||
|
bool shaderlab_db_open(const std::string& path);
|
||||||
|
|
||||||
|
// Close the active connection (no-op if not open).
|
||||||
|
void shaderlab_db_close();
|
||||||
|
|
||||||
|
// Insert or replace a generator. Sets gen.created_at if empty and updated_at unconditionally
|
||||||
|
// (caller's struct is updated in place too). Returns false on SQL error.
|
||||||
|
bool shaderlab_db_save_generator(GeneratorRecord& gen, std::string* err = nullptr);
|
||||||
|
|
||||||
|
// Load all generators ordered by label.
|
||||||
|
std::vector<GeneratorRecord> shaderlab_db_list_generators();
|
||||||
|
|
||||||
|
// Load a single generator by id. Returns false if not found.
|
||||||
|
bool shaderlab_db_get_generator(const std::string& id, GeneratorRecord& out);
|
||||||
|
|
||||||
|
// Remove a generator by id. Returns true if a row was deleted.
|
||||||
|
bool shaderlab_db_delete_generator(const std::string& id);
|
||||||
|
|
||||||
|
// Returns the underlying sqlite3* handle (or nullptr if not open).
|
||||||
|
// For composing additional tables (e.g. ui_layouts) on the same connection.
|
||||||
|
sqlite3* shaderlab_db_handle();
|
||||||
|
|
||||||
|
} // namespace fn::gfx
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
name: shaderlab_db
|
||||||
|
kind: function
|
||||||
|
lang: cpp
|
||||||
|
domain: gfx
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "bool shaderlab_db_save_generator(GeneratorRecord& gen, std::string* err); std::vector<GeneratorRecord> shaderlab_db_list_generators(); bool shaderlab_db_get_generator(const std::string& id, GeneratorRecord& out); bool shaderlab_db_delete_generator(const std::string& id);"
|
||||||
|
description: "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."
|
||||||
|
tags: [sqlite, shaders, glsl, dag, gfx, persistence, crud, shaders_lab]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types:
|
||||||
|
- dag_types_cpp_gfx
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [sqlite3, string, vector, chrono, ctime, iomanip, sstream]
|
||||||
|
tested: true
|
||||||
|
tests:
|
||||||
|
- "open in-memory + list empty"
|
||||||
|
- "save + get roundtrip preserva controls/params/defaults"
|
||||||
|
- "list ordenado por label"
|
||||||
|
- "update preserva created_at y bumps updated_at"
|
||||||
|
- "delete devuelve true solo si existia"
|
||||||
|
test_file_path: "cpp/functions/gfx/shaderlab_db.cpp"
|
||||||
|
file_path: "cpp/functions/gfx/shaderlab_db.cpp"
|
||||||
|
params:
|
||||||
|
- name: gen
|
||||||
|
desc: "Registro del generator. Debe llevar id (snake_case), label, source_glsl original, body_glsl traducido, param_count y los param_defaults/controls coherentes."
|
||||||
|
- name: id
|
||||||
|
desc: "ID snake_case del generator (clave primaria de la tabla)."
|
||||||
|
- name: out
|
||||||
|
desc: "Registro de salida en get; queda relleno solo si la funcion devuelve true."
|
||||||
|
- name: err
|
||||||
|
desc: "Mensaje de error opcional si save falla (constraint, schema, IO)."
|
||||||
|
output: "Persistencia en la tabla generators de shaders_lab.db. Las listas vienen ordenadas por label."
|
||||||
|
---
|
||||||
|
|
||||||
|
## Schema
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE generators (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
label TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL DEFAULT '',
|
||||||
|
source_glsl TEXT NOT NULL,
|
||||||
|
body_glsl TEXT NOT NULL,
|
||||||
|
param_count INTEGER NOT NULL,
|
||||||
|
param_defaults TEXT NOT NULL, -- CSV de floats
|
||||||
|
param_names TEXT NOT NULL, -- LF-separated strings
|
||||||
|
controls TEXT NOT NULL, -- LF-separated, fields '|': kind|label|p0|p1|p2|min|max|step
|
||||||
|
tags TEXT NOT NULL DEFAULT '',
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
- Formato de serializacion custom (no JSON): CSV para floats, LF para strings, `|` para fields de control. Sin escape de separadores; los labels no deben contener `\n` ni `|`.
|
||||||
|
- `shaderlab_db_open` es idempotente: re-abrir con el mismo path es no-op.
|
||||||
|
- `:memory:` como path crea una BD temporal en memoria (usado en tests).
|
||||||
|
- `save_generator` hace upsert (`ON CONFLICT DO UPDATE`). Setea `created_at` solo si esta vacio.
|
||||||
|
- Tests inline activables con `-DSHADERLAB_DB_TEST` y linkando sqlite3 (amalgamation o `-lsqlite3`).
|
||||||
Reference in New Issue
Block a user