chore: sync from fn-registry agent

This commit is contained in:
fn-registry agent
2026-05-08 00:07:55 +02:00
commit 4a09ed9404
5 changed files with 654 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
add_imgui_app(shaders_lab
main.cpp
compiler.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/gl_loader.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/gl_shader.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/gl_framebuffer.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/fullscreen_quad.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/shader_canvas.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/uniform_parser.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/uniform_panel.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/dag_catalog.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/dag_compile.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/dag_uniforms.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/dag_panel.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/dag_node_editor.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/dag_palette.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/dag_node_previews.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/shaderlab_db.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/code_to_generator.cpp
# Primitivos UI usados por el modal Save-as-generator.
${CMAKE_SOURCE_DIR}/functions/core/modal_dialog.cpp
${CMAKE_SOURCE_DIR}/functions/core/text_input.cpp
${CMAKE_SOURCE_DIR}/functions/core/button.cpp
# fps_overlay, panel_menu, layouts_menu, app_menubar, layout_storage ya
# viven en fn_framework.
)
target_include_directories(shaders_lab PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(shaders_lab PRIVATE imgui_node_editor SQLite::SQLite3)
if(WIN32)
# GUI app: sin consola al lanzar (subsystem:windows / -mwindows)
set_target_properties(shaders_lab PROPERTIES WIN32_EXECUTABLE TRUE)
endif()
+104
View File
@@ -0,0 +1,104 @@
---
name: shaders_lab
lang: cpp
domain: gfx
description: "Live GLSL shader playground con DAG pipeline. Editor de codigo con compilacion en caliente, panel DAG con paleta de generadores/filtros/output, dos canvas (Code y DAG), parseo de uniforms anotados (// @slider, @color, @xy) que se convierten en controles, persistencia de generators en shaders_lab.db, y guardado/carga de layouts ImGui."
tags: [imgui, opengl, glsl, shaders, dag, live-coding, playground, sqlite]
uses_functions:
# gfx
- gl_loader_cpp_gfx
- gl_shader_cpp_gfx
- gl_framebuffer_cpp_gfx
- fullscreen_quad_cpp_gfx
- shader_canvas_cpp_gfx
- uniform_parser_cpp_gfx
- uniform_panel_cpp_gfx
- dag_catalog_cpp_gfx
- dag_compile_cpp_gfx
- dag_uniforms_cpp_gfx
- dag_panel_cpp_gfx
- dag_node_editor_cpp_gfx
- dag_palette_cpp_gfx
- dag_node_previews_cpp_gfx
- shaderlab_db_cpp_gfx
- code_to_generator_cpp_gfx
# core (modal Save-as-generator)
- modal_dialog_cpp_core
- text_input_cpp_core
- button_cpp_core
uses_types:
- dag_types_cpp_gfx
framework: "imgui"
entry_point: "main.cpp"
dir_path: "cpp/apps/shaders_lab"
repo_url: ""
---
## Arquitectura
App ImGui de live-coding GLSL con dos modos en paralelo:
1. **Code panel** — editor de fragment shader libre. Las anotaciones en
uniforms (`// @slider`, `// @color`, `// @xy`, `// @toggle`) se parsean y
convierten en controles del panel **Controls** que escriben en un
`UniformStore` aplicado al programa cada frame.
2. **DAG panel** — pipeline node-based con catalogo de generadores
(plasma, voronoi, etc.) y filtros (blur, threshold, etc.) que se
compilan a un fragment shader unificado y se renderizan en **Canvas DAG**.
Al guardar un Code shader como "generator" se traduce a un `DagNodeDef` y se
persiste en `shaders_lab.db` (tabla via `shaderlab_db`), apareciendo en la
paleta del DAG junto a los builtins.
## Capas
| Archivo | Responsabilidad |
|---|---|
| `main.cpp` | UI shell, paneles, modal save-as, layouts, AppConfig |
| `compiler.cpp` | `compile_code()`, `compile_dag()`, `mark_code_dirty()` con debounce 250ms |
`main.cpp` mantiene estado global de sesion (g_source, g_pipeline, g_descs,
g_store, g_layouts...) — ImGui retained-mode obliga a que persista entre
frames. Toda la logica pura de compilacion vive en `compiler.cpp` y en las
funciones `dag_compile`, `code_to_generator`, `uniform_parser` del registry.
## Persistencia
- **`shaders_lab.db`** (junto al .exe) — tabla de generators de usuario via
`shaderlab_db_*`, ademas de `imgui_layouts` (creada por `layout_storage`).
- `imgui.ini` y `app_settings.ini` — gestionados por `fn::run_app` en
`<exe_dir>/local_files/`.
## Paneles
| Panel | Atajo | Que muestra |
|---|---|---|
| Code | Ctrl+1 | Editor del fragment shader + boton "Save as generator" |
| DAG Pipeline | Ctrl+2 | Node editor con la pipeline |
| Canvas Code | Ctrl+3 | Render del Code shader |
| Canvas DAG | Ctrl+4 | Render del shader compilado del DAG |
| Controls | Ctrl+5 | Sliders/color pickers de uniforms anotados |
| Functions | Ctrl+6 | Paleta del DAG (generators + filters + output) |
| Generated GLSL | Ctrl+7 | GLSL final del DAG con uniforms baked como const array |
## Build
```bash
# Linux
cd cpp && cmake -B build/linux -S . && cmake --build build/linux --target shaders_lab
# Windows (cross-compile)
cd cpp && cmake -B build/windows -S . -DCMAKE_TOOLCHAIN_FILE=toolchains/mingw-w64.cmake \
&& cmake --build build/windows --target shaders_lab
```
## Decisiones
- `init_gl_loader = true` (via `fn::run_app` por default cuando se enlaza
con OpenGL) — `shader_canvas`, `gl_shader`, `gl_framebuffer` llaman gl*.
- `viewports = true` — los Canvas se pueden arrastrar fuera del main.
- DAG default: arranca con un nodo "plasma" + "output" si la paleta los
encuentra; persiste el INI con `layout_storage`.
- El boton "Save as generator" valida snake_case, evita colisionar con
builtins, traduce con `code_to_generator`, persiste con `shaderlab_db_save_generator`,
y registra el nodo nuevo en el catalogo en vivo (`dag_register_node`).
+63
View File
@@ -0,0 +1,63 @@
#include "compiler.h"
#include "gfx/shader_canvas.h"
#include "gfx/gl_shader.h"
#include "gfx/uniform_parser.h"
#include "gfx/uniform_panel.h"
#include "gfx/dag_compile.h"
#include "gfx/dag_uniforms.h"
#include <chrono>
#include <string>
#include <vector>
// ── Globals declarados en main.cpp (single source of truth) ─────────────────
extern fn::gfx::ShaderCanvas g_canvas_code;
extern fn::gfx::ShaderCanvas g_canvas_dag;
extern std::string g_source;
extern std::string g_code_err;
extern int g_code_err_line;
extern std::chrono::steady_clock::time_point g_code_last_edit;
extern bool g_code_dirty;
extern std::vector<fn::gfx::UniformDescriptor> g_descs;
extern fn::gfx::UniformStore g_store;
extern std::vector<fn::gfx::DagStep> g_pipeline;
extern std::string g_dag_glsl;
extern std::string g_dag_err;
extern int g_dag_err_line;
namespace shaders_lab {
void compile_code() {
auto r = fn::gfx::compile_fragment(g_source);
if (r.ok) {
g_descs = fn::gfx::parse_uniforms(g_source);
fn::gfx::uniforms_sync(g_store, g_descs);
fn::gfx::canvas_set_program(g_canvas_code, r.program);
g_code_err.clear();
g_code_err_line = -1;
} else {
g_code_err = r.err_msg;
g_code_err_line = r.err_line;
}
}
void compile_dag() {
g_dag_glsl = fn::gfx::compile_dag_to_glsl(g_pipeline);
auto r = fn::gfx::compile_fragment(g_dag_glsl);
if (r.ok) {
fn::gfx::canvas_set_program(g_canvas_dag, r.program);
g_dag_err.clear();
g_dag_err_line = -1;
} else {
g_dag_err = r.err_msg;
g_dag_err_line = r.err_line;
}
}
void mark_code_dirty() {
g_code_last_edit = std::chrono::steady_clock::now();
g_code_dirty = true;
}
} // namespace shaders_lab
+24
View File
@@ -0,0 +1,24 @@
#pragma once
// shaders_lab/compiler — extrae las rutinas impuras de compilacion del shader
// (compile_code, compile_dag, mark_code_dirty) desde main.cpp para que el
// archivo principal quede acotado a la composicion de paneles ImGui.
//
// Las globals (g_source, g_descs, g_store, g_pipeline, etc.) se declaran
// extern y viven en main.cpp; aqui solo orquestamos compilacion.
namespace shaders_lab {
// Compila g_source -> programa OpenGL para g_canvas_code, refresca g_descs
// y sincroniza g_store. Actualiza g_code_err / g_code_err_line.
void compile_code();
// Compila g_pipeline -> g_dag_glsl -> programa OpenGL para g_canvas_dag.
// Actualiza g_dag_err / g_dag_err_line.
void compile_dag();
// Marca el shader Code como dirty y registra el timestamp del ultimo edit
// (para debounce de 250ms en el render loop).
void mark_code_dirty();
} // namespace shaders_lab
+428
View File
@@ -0,0 +1,428 @@
#include "app_base.h"
#include "imgui.h"
#include "gfx/shader_canvas.h"
#include "gfx/gl_shader.h"
#include "gfx/uniform_parser.h"
#include "gfx/uniform_panel.h"
#include "gfx/dag_catalog.h"
#include "gfx/dag_compile.h"
#include "gfx/dag_uniforms.h"
#include "gfx/dag_panel.h"
#include "gfx/dag_node_editor.h"
#include "gfx/dag_palette.h"
#include "gfx/dag_node_previews.h"
#include "gfx/code_to_generator.h"
#include "gfx/shaderlab_db.h"
#include "core/panel_menu.h"
#include "core/layouts_menu.h"
#include "core/layout_storage.h"
#include "core/modal_dialog.h"
#include "core/text_input.h"
#include "core/button.h"
#include "core/tokens.h"
#include "compiler.h"
#include <chrono>
#include <cctype>
#include <cstring>
#include <string>
#include <utility>
#include <vector>
// Globals: linked extern desde compiler.cpp. NO `static` aqui.
fn::gfx::ShaderCanvas g_canvas_code;
fn::gfx::ShaderCanvas g_canvas_dag;
// Default placeholder so the Code panel does something useful on first launch
// without committing to one specific look.
static const char* CODE_PLACEHOLDER = R"glsl(// Escribe tu fragment shader aqui.
// Declara uniforms con anotaciones (// @slider, @color, @xy)
// para que aparezcan como controles al guardar como generator.
uniform vec3 u_color; // @color default=0.5,0.2,0.8
uniform float u_speed; // @slider min=0.1 max=5 default=1
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";
std::string g_source = CODE_PLACEHOLDER;
std::string g_code_err;
int g_code_err_line = -1;
std::chrono::steady_clock::time_point g_code_last_edit;
bool g_code_dirty = true;
std::vector<fn::gfx::UniformDescriptor> g_descs;
fn::gfx::UniformStore g_store;
std::vector<fn::gfx::DagStep> g_pipeline;
std::string g_dag_glsl;
std::string g_dag_err;
int g_dag_err_line = -1;
static bool g_dag_dirty = true; // solo lo usa main.cpp
// ── Panel visibility (toggled from View menu and panel close button) ──────
static bool g_show_code = true;
static bool g_show_dag = true;
static bool g_show_canvas_c = true;
static bool g_show_canvas_d = true;
static bool g_show_controls = true;
static bool g_show_functions = true;
static bool g_show_generated = true;
// Tabla de paneles toggleables que fn::run_app pasa a app_menubar cada frame.
// Vive en el ambito del archivo para poder referenciarse desde main().
static constexpr fn_ui::PanelToggle k_panels[] = {
{"Code", "Ctrl+1", &g_show_code},
{"DAG Pipeline", "Ctrl+2", &g_show_dag},
{"Canvas Code", "Ctrl+3", &g_show_canvas_c},
{"Canvas DAG", "Ctrl+4", &g_show_canvas_d},
{"Controls", "Ctrl+5", &g_show_controls},
{"Functions", "Ctrl+6", &g_show_functions},
{"Generated GLSL","Ctrl+7", &g_show_generated},
};
// ── Layouts (named ImGui ini snapshots persisted in shaders_lab.db) ───────
// El storage opaco encapsula la BD y el blob pendiente. Los callbacks
// envuelven save/apply/delete/reset y se pasan a app_menubar tal cual.
static fn_ui::LayoutStorage* g_layouts = nullptr;
static fn_ui::LayoutCallbacks g_layout_cb;
// ── Save-as-generator modal state ─────────────────────────────────────────
static bool g_save_modal_open = false;
static char g_save_name[64] = "my_shader";
static char g_save_label[64] = "my shader";
static char g_save_desc[256] = "";
static char g_save_tags[128] = "shaders_lab,user";
static std::string g_save_err;
// compile_code, compile_dag, mark_code_dirty viven en compiler.cpp
using shaders_lab::compile_code;
using shaders_lab::compile_dag;
using shaders_lab::mark_code_dirty;
static void ensure_dag_default() {
if (g_pipeline.empty()) {
const fn::gfx::DagNodeDef* plasma = fn::gfx::dag_find("plasma");
if (plasma) {
fn::gfx::DagStep s;
s.id = "n_plasma";
s.name = plasma->name;
s.params = plasma->param_defaults;
g_pipeline.push_back(s);
}
}
bool has_output = false;
for (const auto& s : g_pipeline) {
const fn::gfx::DagNodeDef* d = fn::gfx::dag_find(s.name);
if (d && d->kind == fn::gfx::DagKind::Output) { has_output = true; break; }
}
if (!has_output) {
const fn::gfx::DagNodeDef* out = fn::gfx::dag_find("output");
if (out) {
fn::gfx::DagStep s;
s.id = "n_output";
s.name = out->name;
s.editor_pos_x = 500.0f;
if (!g_pipeline.empty()) s.source_ids[0] = g_pipeline.front().id;
g_pipeline.push_back(s);
}
}
}
static void draw_err(const std::string& msg, int line) {
if (msg.empty()) return;
ImGui::Separator();
if (line > 0) {
ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "line %d: %s", line, msg.c_str());
} else {
ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "%s", msg.c_str());
}
}
// snake_case validation: lowercase letters, digits, underscores; first char a-z.
static bool valid_id(const char* s) {
if (!s || !*s) return false;
if (!(*s >= 'a' && *s <= 'z')) return false;
for (const char* p = s; *p; ++p) {
char c = *p;
if (!((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_')) return false;
}
return true;
}
// Build a DagNodeDef from current Code source + form fields, persist it, and
// register in the live catalog. Returns "" on success or an error message.
static std::string save_current_as_generator() {
if (!valid_id(g_save_name)) return "name must be snake_case (a-z, 0-9, _) and start with a letter";
if (fn::gfx::dag_find(g_save_name)) {
const fn::gfx::DagNodeDef* existing = fn::gfx::dag_find(g_save_name);
if (existing && existing->is_builtin) {
return std::string("name '") + g_save_name + "' collides with a built-in node";
}
// user node with same name → overwrite is allowed
}
auto tr = fn::gfx::code_to_generator(g_source);
if (!tr.ok) return tr.err;
fn::gfx::GeneratorRecord rec;
rec.id = g_save_name;
rec.label = g_save_label[0] ? g_save_label : g_save_name;
rec.description = g_save_desc;
rec.source_glsl = g_source;
rec.body_glsl = tr.body_template;
rec.param_count = tr.param_count;
rec.param_defaults = tr.param_defaults;
rec.param_names = tr.param_names;
rec.controls = tr.controls;
rec.tags = g_save_tags;
std::string err;
if (!fn::gfx::shaderlab_db_save_generator(rec, &err)) {
return std::string("db save failed: ") + err;
}
fn::gfx::DagNodeDef def = fn::gfx::make_generator_def(rec.id, rec.label, rec.description, tr);
if (!fn::gfx::dag_register_node(def)) {
return std::string("could not register node '") + rec.id + "'";
}
return "";
}
// Reconstitute every persisted generator and inject it into the live catalog.
static void load_user_generators_into_catalog() {
for (const auto& rec : fn::gfx::shaderlab_db_list_generators()) {
// Re-translate body_template from source to keep the lambda fresh.
// (We could trust rec.body_glsl, but re-running ensures forward-compat
// when we tweak the translator.)
auto tr = fn::gfx::code_to_generator(rec.source_glsl);
if (!tr.ok) continue; // skip broken records
fn::gfx::DagNodeDef def = fn::gfx::make_generator_def(rec.id, rec.label, rec.description, tr);
fn::gfx::dag_register_node(def);
}
}
static void render() {
// Apply pending layout BEFORE any ImGui::Begin this frame.
// (LoadIniSettingsFromMemory must happen before windows are submitted.)
std::string applied = fn_ui::layout_storage_apply_pending(g_layouts);
if (!applied.empty()) g_layout_cb.active_name = applied;
if (!g_canvas_code.initialized) fn::gfx::canvas_init(g_canvas_code);
if (!g_canvas_dag.initialized) fn::gfx::canvas_init(g_canvas_dag);
if (g_code_dirty) {
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - g_code_last_edit).count();
if (elapsed > 250) {
compile_code();
g_code_dirty = false;
}
}
if (g_dag_dirty) {
compile_dag();
g_dag_dirty = false;
}
ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport());
// Menubar (View + Layouts + Settings + About) la invoca fn::run_app a
// partir de AppConfig::panels y AppConfig::layouts_cb.
// --- Code window ---
if (g_show_code) {
if (ImGui::Begin("Code", &g_show_code)) {
if (fn_ui::button("Save as generator...", fn_ui::ButtonVariant::Secondary)) {
g_save_modal_open = true;
g_save_err.clear();
}
if (fn_ui::modal_dialog_begin("Save as generator", &g_save_modal_open,
ImVec2(420, 0))) {
ImGui::TextUnformatted("Guardar shader actual como nodo Gen del DAG.");
ImGui::Spacing();
fn_ui::text_input("name (snake_case)", g_save_name, sizeof(g_save_name),
"ej: my_shader");
fn_ui::text_input("label", g_save_label, sizeof(g_save_label));
ImGui::InputTextMultiline("description", g_save_desc, sizeof(g_save_desc),
ImVec2(380, 60));
fn_ui::text_input("tags (CSV)", g_save_tags, sizeof(g_save_tags));
if (!g_save_err.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error);
ImGui::TextWrapped("%s", g_save_err.c_str());
ImGui::PopStyleColor();
}
ImGui::Separator();
if (fn_ui::button("Cancel", fn_ui::ButtonVariant::Subtle)) {
g_save_modal_open = false;
}
ImGui::SameLine();
if (fn_ui::button("Save", fn_ui::ButtonVariant::Primary)) {
g_save_err = save_current_as_generator();
if (g_save_err.empty()) {
g_save_modal_open = false;
}
}
}
fn_ui::modal_dialog_end();
ImVec2 avail = ImGui::GetContentRegionAvail();
float footer_h = g_code_err.empty() ? 0.0f : ImGui::GetTextLineHeightWithSpacing() + 8.0f;
ImVec2 editor_size(avail.x, avail.y - footer_h);
char buf[1 << 16];
size_t copy_len = g_source.size() < sizeof(buf) - 1 ? g_source.size() : sizeof(buf) - 1;
memcpy(buf, g_source.c_str(), copy_len);
buf[copy_len] = '\0';
if (ImGui::InputTextMultiline("##code", buf, sizeof(buf), editor_size,
ImGuiInputTextFlags_AllowTabInput)) {
g_source = buf;
mark_code_dirty();
}
draw_err(g_code_err, g_code_err_line);
}
ImGui::End();
}
// --- DAG Pipeline window ---
if (g_show_dag) {
if (ImGui::Begin("DAG Pipeline", &g_show_dag)) {
if (fn::gfx::dag_node_editor(g_pipeline)) {
g_dag_dirty = true;
}
draw_err(g_dag_err, g_dag_err_line);
}
ImGui::End();
}
// --- Canvas Code ---
// Code is fully independent from the DAG: only the uniforms declared in
// the Code source itself (parsed via parse_uniforms) get fed. To reproduce
// a DAG render here, paste the *baked* "Generated GLSL" — its u_params live
// as a const array inside the source.
if (g_show_canvas_c) {
if (ImGui::Begin("Canvas Code", &g_show_canvas_c)) {
fn::gfx::canvas_render(g_canvas_code, static_cast<float>(ImGui::GetTime()),
[](unsigned int program) {
fn::gfx::uniforms_apply(g_store, g_descs, program);
});
}
ImGui::End();
}
// --- Canvas DAG ---
if (g_show_canvas_d) {
if (ImGui::Begin("Canvas DAG", &g_show_canvas_d)) {
fn::gfx::canvas_render(g_canvas_dag, static_cast<float>(ImGui::GetTime()),
[](unsigned int program) {
fn::gfx::dag_uniforms_apply(g_pipeline, program);
});
}
ImGui::End();
}
if (g_canvas_dag.program) {
fn::gfx::dag_previews_render(g_pipeline, g_canvas_dag.program);
}
// --- Controls window (Code uniforms) ---
if (g_show_controls) {
if (ImGui::Begin("Controls", &g_show_controls)) {
if (g_descs.empty()) {
ImGui::TextDisabled("No uniforms declared in Code.");
ImGui::TextDisabled("Use // @slider, @color, @toggle, @xy annotations.");
} else {
fn::gfx::uniforms_panel(g_store, g_descs);
}
// fps_overlay ahora se renderiza desde fn::run_app cuando el usuario
// lo activa en Settings → Show FPS overlay.
}
ImGui::End();
}
// --- Functions palette (drag into DAG Pipeline) ---
if (g_show_functions) {
if (ImGui::Begin("Functions", &g_show_functions)) {
fn::gfx::dag_palette();
}
ImGui::End();
}
// --- Generated GLSL window (self-contained DAG → paste-able into Code) ---
// We bake the live params into a `const vec4 u_params[]` so the displayed
// text is a complete shader: copy-pasting it into the Code editor yields
// the same render at the moment of the copy, and nothing in the DAG can
// change the Code canvas afterwards.
if (g_show_generated) {
if (ImGui::Begin("Generated GLSL", &g_show_generated)) {
if (g_pipeline.empty()) {
ImGui::TextDisabled("(DAG not compiled yet)");
} else {
static std::string s_baked;
s_baked = fn::gfx::compile_dag_to_glsl_baked(g_pipeline);
ImVec2 avail = ImGui::GetContentRegionAvail();
ImGui::InputTextMultiline("##dag_glsl",
const_cast<char*>(s_baked.c_str()),
s_baked.size() + 1,
avail,
ImGuiInputTextFlags_ReadOnly);
}
}
ImGui::End();
}
}
int main() {
fn::gfx::shaderlab_db_open("shaders_lab.db");
load_user_generators_into_catalog();
ensure_dag_default();
// Layout persistence: handle opaco que crea su propia tabla
// imgui_layouts en shaders_lab.db (CREATE IF NOT EXISTS, no toca la
// tabla ui_layouts heredada). Cualquier app del registry puede usar
// este patron.
g_layouts = fn_ui::layout_storage_open("shaders_lab.db");
fn_ui::layout_storage_make_callbacks(g_layouts, g_layout_cb);
// Override de on_reset: ademas de limpiar el INI, re-mostrar todos
// los paneles especificos de shaders_lab.
g_layout_cb.on_reset = []() {
g_show_code = g_show_dag = g_show_canvas_c = g_show_canvas_d =
g_show_controls = g_show_functions = g_show_generated = true;
ImGui::LoadIniSettingsFromMemory("", 0);
ImGui::GetIO().WantSaveIniSettings = true;
g_layout_cb.active_name.clear();
};
fn::AppConfig cfg;
cfg.title = "shaders_lab";
cfg.width = 1600;
cfg.height = 900;
cfg.about = {.name = "shaders_lab",
.version = "0.3.0",
.description = "Live GLSL shader playground with DAG pipeline. layout_storage publico, compiler extraido, AppConfig estandar, multi-viewport, modal save-as via modal_dialog."};
cfg.panels = k_panels;
cfg.panel_count = sizeof(k_panels) / sizeof(k_panels[0]);
cfg.layouts_cb = &g_layout_cb;
int rc = fn::run_app(cfg, render);
fn::gfx::canvas_destroy(g_canvas_code);
fn::gfx::canvas_destroy(g_canvas_dag);
fn::gfx::dag_node_editor_destroy();
fn::gfx::dag_previews_destroy();
fn_ui::layout_storage_close(g_layouts);
fn::gfx::shaderlab_db_close();
return rc;
}