Phase 23: refine routing inspection, signal-flow layout and atomic wire insertion

This commit is contained in:
Johannes Findeisen 2026-10-27 16:00:29 +01:00
commit ba5c2503a7
15 changed files with 533 additions and 22 deletions

View file

@ -63,6 +63,15 @@ SequencerWidget *timeline(Fl_Group *group)
}
return nullptr;
}
GraphWidget *graph_canvas(Fl_Group *group)
{
for (int i = 0; i < group->children(); ++i) {
if (auto *canvas = dynamic_cast<GraphWidget *>(group->child(i))) return canvas;
if (auto *nested = dynamic_cast<Fl_Group *>(group->child(i)))
if (auto *canvas = graph_canvas(nested)) return canvas;
}
return nullptr;
}
void gesture(Fl_Widget *s, int left, int top, int dx, int dy)
{
int old_x = Fl::e_x, old_y = Fl::e_y, old_key = Fl::e_keysym;
@ -549,6 +558,50 @@ int main(int argc, char **argv)
CHECK(xtc_project_get_track(s->project(),0,&t) == XTC_OK && !std::strcmp(t.name,"Keys"));
window.do_callback();
}
{
reset();
MainWindow window(false);
auto *canvas = graph_canvas(&window);
CHECK(canvas != nullptr);
CHECK(canvas->add_node(XTC_NODE_OSCILLATOR) == XTC_OK);
CHECK(canvas->add_node(XTC_NODE_OUTPUT) == XTC_OK);
CHECK(canvas->connect_ports({0,1,1,0}) == XTC_OK);
CHECK(canvas->select_connection(0,1) && contains(&window, "Wire: #0 audio out -> #1 sum in"));
const xtc_graph *original = canvas->graph();
unsigned int submitted = submissions;
submit_result = XTC_ERR_CAPACITY;
CHECK(canvas->insert_gain() == XTC_ERR_CAPACITY && canvas->graph() == original && submissions == submitted+1);
submit_result = XTC_OK;
CHECK(canvas->insert_gain() == XTC_OK);
size_t inserted = canvas->selected_node(), nodes, edges;
CHECK(inserted == 2);
CHECK(xtc_graph_get_counts(canvas->graph(), &nodes, &edges) == XTC_OK && nodes == 3 && edges == 2);
xtc_routing_info route{};
CHECK(canvas->routing(inserted,route) && route.level == 1 && route.reaches_output && route.inputs == 1);
submitted = submissions;
CHECK(canvas->auto_layout() && submissions == submitted);
int sx = 0, sy = 0, gx = 0, gy = 0, ox = 0, oy = 0;
CHECK(canvas->node_position(0,sx,sy) && canvas->node_position(inserted,gx,gy) && canvas->node_position(1,ox,oy));
CHECK(sx < gx && gx < ox);
CHECK(canvas->select_node(inserted) && contains(&window,"1 incoming, 1 outgoing"));
CHECK(canvas->select_connection(0,inserted) && canvas->delete_selected() == XTC_OK);
CHECK(canvas->select_node(inserted) && contains(&window,"unwired input uses external audio"));
CHECK(window.undo() == XTC_OK); // Disconnect is independently undoable.
CHECK(window.undo() == XTC_OK); // Whole insertion is one undo step.
CHECK(xtc_graph_get_counts(canvas->graph(), &nodes, &edges) == XTC_OK && nodes == 2 && edges == 1);
CHECK(window.undo(true) == XTC_OK);
CHECK(xtc_graph_get_counts(canvas->graph(), &nodes, &edges) == XTC_OK && nodes == 3 && edges == 2);
CHECK(canvas->select_node(inserted));
int key = Fl::e_keysym, state = Fl::e_state;
Fl::e_keysym = FL_Right; Fl::e_state = 0;
CHECK(canvas->handle(FL_KEYDOWN) == 1 && canvas->selected_node() == 0);
Fl::e_keysym = key; Fl::e_state = state;
// Whole-project binding must give every new node a distinct position.
canvas->reset_layout(); canvas->set_graph(canvas->graph());
CHECK(canvas->node_position(0,sx,sy) && canvas->node_position(inserted,gx,gy) && canvas->node_position(1,ox,oy));
CHECK((sx != gx || sy != gy) && (sx != ox || sy != oy) && (gx != ox || gy != oy));
window.do_callback();
}
if (const char *module = std::getenv("XTC_TEST_PLUGIN")) {
reset();
MainWindow window(false);

View file

@ -72,6 +72,22 @@ int main(int argc, char **argv)
const xtc_graph *unchanged = canvas->graph();
CHECK(canvas->connect_ports({1U, 1U, 1U, 0U}) == XTC_ERR_GRAPH_CYCLE);
CHECK(canvas->graph() == unchanged);
// Replace a live wire by a complete unity-gain chain at one publication.
CHECK(canvas->select_connection(1,2));
CHECK(canvas->insert_gain() == XTC_OK);
size_t inserted = canvas->selected_node();
pump();
CHECK(find(window, "Graph: active in engine") != nullptr);
CHECK(find(window, "running — modular graph") != nullptr);
xtc_routing_info route{};
CHECK(canvas->routing(inserted, route) && route.inputs == 1 && route.outputs == 1 && route.reaches_output);
CHECK(canvas->connect_ports({inserted,1,1,0}) == XTC_ERR_GRAPH_CYCLE);
CHECK(canvas->select_connection(1,inserted) && canvas->delete_selected() == XTC_OK); pump();
CHECK(canvas->select_connection(inserted,2) && canvas->delete_selected() == XTC_OK); pump();
CHECK(canvas->connect_ports({1,1,2,0}) == XTC_OK); pump();
CHECK(canvas->select_node(inserted) && canvas->delete_selected() == XTC_OK);
pump();
CHECK(find(window, "Graph: active in engine") != nullptr);
CHECK(canvas->add_node(XTC_NODE_METER) == XTC_OK);
pump();
CHECK(find(window, "Graph: active in engine") != nullptr);

84
tests/unit/test_routing.c Normal file
View file

@ -0,0 +1,84 @@
#include "xtc/xtc_engine.h"
#include "xtc/xtc_realtime.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
xtc_graph *graph = NULL, *inserted = NULL, *bad = NULL, *legacy = NULL, *full = NULL;
xtc_plan *old = NULL, *fresh = NULL;
xtc_realtime *engine = NULL;
xtc_graph_edit edit = {0};
xtc_routing_info info[256] = {{0}}, sentinel[256];
xtc_realtime_status status;
size_t source, gain, output, unused, added = SIZE_MAX, count = 999, nodes, edges, i;
float a[32], b[32];
assert(xtc_graph_create(8, &graph) == XTC_OK);
assert(xtc_graph_require_output_nodes(graph, 1) == XTC_OK);
assert(xtc_graph_add_oscillator(graph, 12000, &source) == XTC_OK);
assert(xtc_graph_add_gain(graph, 0.25f, &gain) == XTC_OK);
assert(xtc_graph_add_output(graph, &output) == XTC_OK);
assert(xtc_graph_add_meter(graph, &unused) == XTC_OK);
assert(xtc_graph_connect(graph, source, gain) == XTC_OK && xtc_graph_connect(graph, gain, output) == XTC_OK);
assert(xtc_graph_analyze(graph, info, 256, &count) == XTC_OK && count == 4);
assert(info[0].id == source && info[0].inputs == 0 && info[0].outputs == 1 && info[0].level == 0 && info[0].reaches_output && !info[0].external_input);
assert(info[1].inputs == 1 && info[1].outputs == 1 && info[1].level == 1 && info[1].reaches_output && !info[1].external_input);
assert(info[2].level == 2 && info[2].reaches_output && !info[2].external_input);
assert(!info[3].reaches_output && info[3].external_input);
memcpy(sentinel, info, sizeof(info)); count = 999;
assert(xtc_graph_analyze(graph, info, 3, &count) == XTC_ERR_CAPACITY && count == 999 && !memcmp(info, sentinel, sizeof(info)));
assert(xtc_graph_analyze(NULL, info, 256, &count) == XTC_ERR_INVALID_ARGUMENT);
assert(xtc_graph_analyze(graph, NULL, 256, &count) == XTC_ERR_INVALID_ARGUMENT);
edit.kind = XTC_EDIT_INSERT_GAIN;
edit.data.connection = (xtc_connection_info){source, 1, gain, 0};
assert(xtc_graph_apply_edit(graph, &edit, &inserted, &added) == XTC_OK && added > unused);
assert(xtc_graph_get_counts(graph, &nodes, &edges) == XTC_OK && nodes == 4 && edges == 2);
assert(xtc_graph_get_counts(inserted, &nodes, &edges) == XTC_OK && nodes == 5 && edges == 3);
assert(xtc_graph_analyze(inserted, info, 256, &count) == XTC_OK && count == 5);
assert(info[4].id == added && info[4].level == 1 && info[1].level == 2 && info[2].level == 3);
assert(xtc_graph_compile(graph, 48000, 32, &old) == XTC_OK);
assert(xtc_graph_compile(inserted, 48000, 32, &fresh) == XTC_OK);
assert(xtc_plan_process(old, NULL, a, 32) == XTC_OK && xtc_plan_process(fresh, NULL, b, 32) == XTC_OK && !memcmp(a, b, sizeof(a)));
xtc_plan_destroy(old); xtc_plan_destroy(fresh);
/* Missing/invalid wire never consumes IDs or returns a partial graph. */
size_t inserted_id = added;
added = 999;
edit.data.connection.destination_node = unused;
assert(xtc_graph_apply_edit(graph, &edit, &bad, &added) == XTC_ERR_NOT_FOUND && !bad && added == 999);
edit.data.connection = (xtc_connection_info){source, 0, gain, 0};
assert(xtc_graph_apply_edit(graph, &edit, &bad, &added) != XTC_OK && !bad && added == 999);
assert(xtc_graph_create(2, &full) == XTC_OK);
assert(xtc_graph_add_gain(full, 1, &source) == XTC_OK && xtc_graph_add_output(full, &output) == XTC_OK);
assert(xtc_graph_connect(full, source, output) == XTC_OK);
edit.data.connection = (xtc_connection_info){source, 1, output, 0};
assert(xtc_graph_apply_edit(full, &edit, &bad, &added) == XTC_ERR_CAPACITY && !bad && added == 999);
assert(xtc_graph_get_counts(full, &nodes, &edges) == XTC_OK && nodes == 2 && edges == 1);
xtc_graph_destroy(full);
/* Rejected cycle and disconnect leave an independently compiled plan usable. */
edit.kind = XTC_EDIT_CONNECT;
edit.data.connection = (xtc_connection_info){gain, 1, inserted_id, 0};
assert(xtc_graph_apply_edit(inserted, &edit, &bad, NULL) == XTC_ERR_GRAPH_CYCLE && !bad);
/* Pending/active plan handoff publishes the entire inserted chain at once. */
assert(xtc_realtime_create(48000, 32, &engine) == XTC_OK);
assert(xtc_realtime_submit(engine, graph) == XTC_OK);
assert(xtc_realtime_process(engine, NULL, a, 32) == XTC_OK);
for (i = 0; i < 100; ++i) {
assert(xtc_realtime_submit(engine, i % 2 ? graph : inserted) == XTC_OK);
assert(xtc_realtime_process(engine, NULL, b, 32) == XTC_OK && !memcmp(a, b, sizeof(a)));
xtc_realtime_collect(engine);
assert(xtc_realtime_get_status(engine, &status) == XTC_OK && status.active_revision == status.submitted_revision);
}
xtc_realtime_destroy(engine);
/* Legacy terminal fallback is disabled whenever an explicit output exists. */
assert(xtc_graph_create(4, &legacy) == XTC_OK);
assert(xtc_graph_add_gain(legacy, 1, &gain) == XTC_OK);
assert(xtc_graph_analyze(legacy, info, 256, &count) == XTC_OK && info[0].reaches_output && info[0].external_input);
assert(xtc_graph_add_output(legacy, &output) == XTC_OK);
assert(xtc_graph_analyze(legacy, info, 256, &count) == XTC_OK && !info[0].reaches_output && info[1].external_input);
assert(xtc_graph_connect(legacy, gain, output) == XTC_OK);
assert(xtc_graph_analyze(legacy, info, 256, &count) == XTC_OK && info[0].reaches_output && !info[1].external_input);
xtc_graph_destroy(legacy); xtc_graph_destroy(graph); xtc_graph_destroy(inserted);
puts("Routing reachability/levels/external input, atomic wire insertion/rollback and publication passed.");
return 0;
}