feat(shaders_lab): two simultaneous canvases (Code + DAG)

Canvas Code and Canvas DAG are now independent windows, each with its
own ShaderCanvas, FBO and compiled program. Both render every frame in
parallel. No more focus-based recompile — each source compiles when its
own content changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-24 21:50:29 +02:00
parent 1a6e3cbeaf
commit bf5011de93
+77 -86
View File
@@ -13,67 +13,62 @@
#include "seed_shaders.h" #include "seed_shaders.h"
#include <chrono> #include <chrono>
#include <cstdio>
#include <cstring> #include <cstring>
#include <string> #include <string>
#include <vector> #include <vector>
enum class Source { Code, Dag }; static fn::gfx::ShaderCanvas g_canvas_code;
static fn::gfx::ShaderCanvas g_canvas_dag;
static Source g_active = Source::Code;
static fn::gfx::ShaderCanvas g_canvas;
static std::string g_source = PLASMA; static std::string g_source = PLASMA;
static std::string g_last_err; static std::string g_code_err;
static int g_last_err_line = -1; static int g_code_err_line = -1;
static std::chrono::steady_clock::time_point g_last_edit; static std::chrono::steady_clock::time_point g_code_last_edit;
static bool g_dirty = true; static bool g_code_dirty = true;
static std::vector<fn::gfx::UniformDescriptor> g_descs; static std::vector<fn::gfx::UniformDescriptor> g_descs;
static fn::gfx::UniformStore g_store; static fn::gfx::UniformStore g_store;
static std::vector<fn::gfx::DagStep> g_pipeline; static std::vector<fn::gfx::DagStep> g_pipeline;
static std::string g_dag_glsl; static std::string g_dag_glsl;
static std::string g_dag_err;
static int g_dag_err_line = -1;
static bool g_dag_dirty = true;
static void try_compile() { static void compile_code() {
if (g_active == Source::Code) { auto r = fn::gfx::compile_fragment(g_source);
auto r = fn::gfx::compile_fragment(g_source); if (r.ok) {
if (r.ok) { g_descs = fn::gfx::parse_uniforms(g_source);
g_descs = fn::gfx::parse_uniforms(g_source); fn::gfx::uniforms_sync(g_store, g_descs);
fn::gfx::uniforms_sync(g_store, g_descs); fn::gfx::canvas_set_program(g_canvas_code, r.program);
fn::gfx::canvas_set_program(g_canvas, r.program); g_code_err.clear();
g_last_err.clear(); g_code_err_line = -1;
g_last_err_line = -1;
} else {
g_last_err = r.err_msg;
g_last_err_line = r.err_line;
}
} else { } else {
g_dag_glsl = fn::gfx::compile_dag_to_glsl(g_pipeline); g_code_err = r.err_msg;
auto r = fn::gfx::compile_fragment(g_dag_glsl); g_code_err_line = r.err_line;
if (r.ok) {
fn::gfx::canvas_set_program(g_canvas, r.program);
g_last_err.clear();
g_last_err_line = -1;
} else {
g_last_err = r.err_msg;
g_last_err_line = r.err_line;
}
} }
} }
static void mark_dirty() { static void compile_dag() {
g_last_edit = std::chrono::steady_clock::now(); g_dag_glsl = fn::gfx::compile_dag_to_glsl(g_pipeline);
g_dirty = true; 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;
}
} }
static void set_active(Source s) { static void mark_code_dirty() {
if (g_active != s) { g_code_last_edit = std::chrono::steady_clock::now();
g_active = s; g_code_dirty = true;
try_compile();
}
} }
static void load_preset(const char* src) { static void load_preset(const char* src) {
g_source = src; g_source = src;
mark_dirty(); mark_code_dirty();
} }
static void ensure_dag_default() { static void ensure_dag_default() {
@@ -89,47 +84,46 @@ static void ensure_dag_default() {
} }
} }
static void draw_err_footer() { static void draw_err(const std::string& msg, int line) {
if (g_last_err.empty()) return; if (msg.empty()) return;
ImGui::Separator(); ImGui::Separator();
if (g_last_err_line > 0) { if (line > 0) {
ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "line %d: %s", ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "line %d: %s", line, msg.c_str());
g_last_err_line, g_last_err.c_str());
} else { } else {
ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "%s", g_last_err.c_str()); ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "%s", msg.c_str());
} }
} }
static void render() { static void render() {
if (!g_canvas.initialized) fn::gfx::canvas_init(g_canvas); 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_dirty) { if (g_code_dirty) {
auto now = std::chrono::steady_clock::now(); auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - g_last_edit).count(); auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - g_code_last_edit).count();
if (elapsed > 250) { if (elapsed > 250) {
try_compile(); compile_code();
g_dirty = false; g_code_dirty = false;
} }
} }
if (g_dag_dirty) {
compile_dag();
g_dag_dirty = false;
}
ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport()); ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport());
// --- Code window --- // --- Code window ---
if (ImGui::Begin("Code")) { if (ImGui::Begin("Code")) {
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
set_active(Source::Code);
}
if (ImGui::Button("Plasma")) { load_preset(PLASMA); } if (ImGui::Button("Plasma")) { load_preset(PLASMA); }
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Button("Circle")) { load_preset(CIRCLE); } if (ImGui::Button("Circle")) { load_preset(CIRCLE); }
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Button("Checker")) { load_preset(CHECKER); } if (ImGui::Button("Checker")) { load_preset(CHECKER); }
bool has_err_here = !g_last_err.empty() && g_active == Source::Code;
ImVec2 avail = ImGui::GetContentRegionAvail(); ImVec2 avail = ImGui::GetContentRegionAvail();
float footer_height = has_err_here ? ImGui::GetTextLineHeightWithSpacing() + 8.0f : 0.0f; float footer_h = g_code_err.empty() ? 0.0f : ImGui::GetTextLineHeightWithSpacing() + 8.0f;
ImVec2 editor_size(avail.x, avail.y - footer_height); ImVec2 editor_size(avail.x, avail.y - footer_h);
char buf[1 << 16]; char buf[1 << 16];
size_t copy_len = g_source.size() < sizeof(buf) - 1 ? g_source.size() : sizeof(buf) - 1; size_t copy_len = g_source.size() < sizeof(buf) - 1 ? g_source.size() : sizeof(buf) - 1;
@@ -139,41 +133,37 @@ static void render() {
if (ImGui::InputTextMultiline("##code", buf, sizeof(buf), editor_size, if (ImGui::InputTextMultiline("##code", buf, sizeof(buf), editor_size,
ImGuiInputTextFlags_AllowTabInput)) { ImGuiInputTextFlags_AllowTabInput)) {
g_source = buf; g_source = buf;
mark_dirty(); mark_code_dirty();
} }
if (has_err_here) draw_err_footer(); draw_err(g_code_err, g_code_err_line);
} }
ImGui::End(); ImGui::End();
// --- DAG Pipeline window --- // --- DAG Pipeline window ---
if (ImGui::Begin("DAG Pipeline")) { if (ImGui::Begin("DAG Pipeline")) {
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) { if (fn::gfx::dag_panel(g_pipeline)) {
set_active(Source::Dag); g_dag_dirty = true;
} }
bool topo_changed = fn::gfx::dag_panel(g_pipeline); draw_err(g_dag_err, g_dag_err_line);
if (topo_changed) mark_dirty();
if (!g_last_err.empty() && g_active == Source::Dag) draw_err_footer();
} }
ImGui::End(); ImGui::End();
// --- Canvas window --- // --- Canvas Code ---
char canvas_title[64]; if (ImGui::Begin("Canvas Code")) {
snprintf(canvas_title, sizeof(canvas_title), "Canvas [%s]###canvas", fn::gfx::canvas_render(g_canvas_code, static_cast<float>(ImGui::GetTime()),
g_active == Source::Code ? "Code" : "DAG"); [](unsigned int program) {
if (ImGui::Begin(canvas_title)) { fn::gfx::uniforms_apply(g_store, g_descs, program);
if (g_active == Source::Code) { });
fn::gfx::canvas_render(g_canvas, static_cast<float>(ImGui::GetTime()), }
[](unsigned int program) { ImGui::End();
fn::gfx::uniforms_apply(g_store, g_descs, program);
}); // --- Canvas DAG ---
} else { if (ImGui::Begin("Canvas DAG")) {
fn::gfx::canvas_render(g_canvas, static_cast<float>(ImGui::GetTime()), fn::gfx::canvas_render(g_canvas_dag, static_cast<float>(ImGui::GetTime()),
[](unsigned int program) { [](unsigned int program) {
fn::gfx::dag_uniforms_apply(g_pipeline, program); fn::gfx::dag_uniforms_apply(g_pipeline, program);
}); });
}
} }
ImGui::End(); ImGui::End();
@@ -193,7 +183,7 @@ static void render() {
// --- Generated GLSL window (DAG compiled output, read-only) --- // --- Generated GLSL window (DAG compiled output, read-only) ---
if (ImGui::Begin("Generated GLSL")) { if (ImGui::Begin("Generated GLSL")) {
if (g_dag_glsl.empty()) { if (g_dag_glsl.empty()) {
ImGui::TextDisabled("(DAG not compiled yet — focus the DAG Pipeline window)"); ImGui::TextDisabled("(DAG not compiled yet)");
} else { } else {
ImVec2 avail = ImGui::GetContentRegionAvail(); ImVec2 avail = ImGui::GetContentRegionAvail();
ImGui::InputTextMultiline("##dag_glsl", ImGui::InputTextMultiline("##dag_glsl",
@@ -210,9 +200,10 @@ int main() {
ensure_dag_default(); ensure_dag_default();
fn::AppConfig cfg; fn::AppConfig cfg;
cfg.title = "shaders_lab"; cfg.title = "shaders_lab";
cfg.width = 1400; cfg.width = 1600;
cfg.height = 860; cfg.height = 900;
int rc = fn::run_app(cfg, render); int rc = fn::run_app(cfg, render);
fn::gfx::canvas_destroy(g_canvas); fn::gfx::canvas_destroy(g_canvas_code);
fn::gfx::canvas_destroy(g_canvas_dag);
return rc; return rc;
} }