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