#include "gfx/shaderlab_db.h" #include #include #include #include #include #include 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& 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 floats_from_csv(const std::string& s) { std::vector 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& 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 strings_from_lf(const std::string& s) { std::vector 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& v) { std::ostringstream ss; for (size_t i = 0; i < v.size(); ++i) { if (i) ss << '\n'; ss << static_cast(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 controls_from_string(const std::string& s) { std::vector 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 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(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(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 shaderlab_db_list_generators() { std::vector 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 #include 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