Phase 4: Add modular graph editor and transactional graph edits
This commit is contained in:
parent
2ecc03b482
commit
5c41d7f6da
27 changed files with 1023 additions and 64 deletions
|
|
@ -63,3 +63,28 @@ CFLAGS/CXXFLAGS and LDFLAGS, using the baseline warning/include/standard flags
|
|||
from BUILD.md. Keep FLTK's `-I` and `-D` flags from `fltk-config --cxxflags`,
|
||||
but exclude the distribution's GCC-specific LTO/object-code flags for Clang.
|
||||
No suppressions are used for core/backend/realtime sanitizer tests.
|
||||
|
||||
## Phase 4 graph editing coverage
|
||||
|
||||
The same `make test-gui` target now adds nodes through toolbar callbacks,
|
||||
connects the exact rendered graph, validates expected oscillator/gain samples
|
||||
in the submitted backend plan, checks cycle/direction/duplicate rejection,
|
||||
checks backend I/O/pending-capacity rollback, deletes wires and incident edges,
|
||||
and verifies stable IDs, preserved positions, prepared/queued/active feedback
|
||||
and empty-graph silence. The fake backend compiles submitted C snapshots rather
|
||||
than accepting an unrelated GUI model. Move-only actions do not submit plans.
|
||||
|
||||
`make test-gui-jack` exercises the same widget against real JACK: it builds a
|
||||
graph while stopped, starts it, adds/deletes nodes during active processing,
|
||||
waits for revision acknowledgement, and rejects cycles without changing the
|
||||
model. Start/stop/reconnect and callback-safe active close remain covered. No
|
||||
routes are created by this test. Core tests separately check known sample
|
||||
changes after deleting/reconnecting nodes and meter lookup after ID compaction.
|
||||
|
||||
A live xdotool mouse run under a 1280x800 Xvfb display created oscillator → gain
|
||||
→ meter → output by dragging ports, observed a rejected feedback cycle, moved
|
||||
a source with its wire attached, deleted a wire and node, and closed while
|
||||
running. Visual inspection confirmed actual wires and active-state feedback,
|
||||
with zero xruns reported during that smoke run. Zoom, multiselection, editable
|
||||
parameters, project save/load, realtime meter visualization and hardware routing
|
||||
controls are outside this phase. Node layout currently lasts only for a session.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "../../src/gui/main_window.hpp"
|
||||
#include "../../src/gui/graph_widget.hpp"
|
||||
#include <FL/Fl.H>
|
||||
#include <FL/Fl_Box.H>
|
||||
#include <FL/Fl_Button.H>
|
||||
|
|
@ -8,15 +9,17 @@
|
|||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cmath>
|
||||
#include <initializer_list>
|
||||
|
||||
/* Fake only the public C backend boundary. Real FLTK widgets, callbacks and
|
||||
* timers run under Xvfb; no server/device is required or touched. */
|
||||
struct xtc_backend { int running = 0; };
|
||||
struct xtc_backend { int running = 0; xtc_plan *plan = nullptr; };
|
||||
namespace {
|
||||
xtc_backend handle;
|
||||
xtc_result open_result = XTC_OK, start_result = XTC_OK, stop_result = XTC_OK, close_result = XTC_OK;
|
||||
xtc_result status_result = XTC_OK;
|
||||
xtc_result status_result = XTC_OK, submit_result = XTC_OK;
|
||||
unsigned int submissions = 0U;
|
||||
xtc_backend_status engine_status = {};
|
||||
bool partial_open = false;
|
||||
unsigned int opens = 0, starts = 0, stops = 0, closes = 0, collects = 0, alive = 0;
|
||||
|
|
@ -55,6 +58,7 @@ void reset()
|
|||
{
|
||||
CHECK(alive == 0U);
|
||||
open_result = start_result = stop_result = close_result = status_result = XTC_OK;
|
||||
submit_result = XTC_OK;
|
||||
engine_status = {};
|
||||
engine_status.sample_rate = 48000U;
|
||||
engine_status.buffer_frames = 1024U;
|
||||
|
|
@ -67,14 +71,14 @@ xtc_result xtc_backend_open(const xtc_backend_config *config, xtc_backend **out)
|
|||
CHECK(config->kind == XTC_BACKEND_JACK && config->channel_count == 1U);
|
||||
++opens;
|
||||
*out = open_result == XTC_OK || partial_open ? &handle : nullptr;
|
||||
if (*out != nullptr) { ++alive; handle.running = 0; }
|
||||
if (*out != nullptr) { ++alive; handle.running = 0; CHECK(handle.plan == nullptr); engine_status.active_revision = engine_status.submitted_revision = 0U; }
|
||||
return open_result;
|
||||
}
|
||||
xtc_result xtc_backend_close(xtc_backend *backend)
|
||||
{
|
||||
if (backend == nullptr) return XTC_OK;
|
||||
++closes;
|
||||
if (close_result == XTC_OK) { CHECK(alive == 1U); --alive; handle.running = 0; }
|
||||
if (close_result == XTC_OK) { CHECK(alive == 1U); --alive; handle.running = 0; xtc_plan_destroy(handle.plan); handle.plan = nullptr; }
|
||||
return close_result;
|
||||
}
|
||||
xtc_result xtc_backend_start(xtc_backend *backend)
|
||||
|
|
@ -88,6 +92,18 @@ xtc_result xtc_backend_get_status(const xtc_backend *backend, xtc_backend_status
|
|||
status->running = handle.running && !status->server_lost;
|
||||
return status_result;
|
||||
}
|
||||
xtc_result xtc_backend_submit(xtc_backend *backend, const xtc_graph *graph)
|
||||
{
|
||||
CHECK(backend == &handle && graph != nullptr);
|
||||
++submissions;
|
||||
if (submit_result != XTC_OK) return submit_result;
|
||||
xtc_plan *plan = nullptr;
|
||||
xtc_result result = xtc_graph_compile(graph, 48000.0, 32U, &plan);
|
||||
if (result != XTC_OK) return result;
|
||||
xtc_plan_destroy(handle.plan); handle.plan = plan;
|
||||
++engine_status.submitted_revision;
|
||||
return XTC_OK;
|
||||
}
|
||||
void xtc_backend_collect(xtc_backend *backend)
|
||||
{ CHECK(backend == &handle && alive == 1U); ++collects; }
|
||||
}
|
||||
|
|
@ -106,7 +122,7 @@ int main(int argc, char **argv)
|
|||
CHECK(contains(&window, "ready / stopped"));
|
||||
press(play);
|
||||
CHECK(starts == 1U && !play->active() && stop->active());
|
||||
CHECK(contains(&window, "running (silence"));
|
||||
CHECK(contains(&window, "running — modular graph"));
|
||||
press(stop);
|
||||
CHECK(stops == 1U && play->active() && !stop->active());
|
||||
start_result = XTC_ERR_IO;
|
||||
|
|
@ -138,6 +154,68 @@ int main(int argc, char **argv)
|
|||
CHECK(std::strcmp(tabs->value()->label(), std::strchr(view, '/') + 1) == 0);
|
||||
}
|
||||
}
|
||||
GraphWidget *canvas = nullptr;
|
||||
auto *graph_group = dynamic_cast<Fl_Group *>(find(&window, "Modular Graph"));
|
||||
CHECK(graph_group != nullptr);
|
||||
if (graph_group != nullptr) {
|
||||
for (int i = 0; i < graph_group->children(); ++i) if (auto *scroll = dynamic_cast<Fl_Group *>(graph_group->child(i)))
|
||||
for (int j = 0; j < scroll->children(); ++j)
|
||||
if (auto *found = dynamic_cast<GraphWidget *>(scroll->child(j))) canvas = found;
|
||||
}
|
||||
CHECK(canvas != nullptr);
|
||||
if (canvas == nullptr) return 1;
|
||||
menu->picked(menu->find_item("View/Modular Graph"));
|
||||
press(find(&window, "Add oscillator"));
|
||||
press(find(&window, "Add gain"));
|
||||
press(find(&window, "Add meter"));
|
||||
press(find(&window, "Add output"));
|
||||
size_t nodes, edges;
|
||||
CHECK(xtc_graph_get_counts(canvas->graph(), &nodes, &edges) == XTC_OK && nodes == 4U && edges == 0U);
|
||||
CHECK(canvas->connect_ports({0U, 1U, 1U, 0U}) == XTC_OK);
|
||||
CHECK(canvas->connect_ports({1U, 1U, 2U, 0U}) == XTC_OK);
|
||||
CHECK(canvas->connect_ports({2U, 1U, 3U, 0U}) == XTC_OK);
|
||||
float rendered[32];
|
||||
CHECK(xtc_plan_process(handle.plan, nullptr, rendered, 32U) == XTC_OK);
|
||||
CHECK(std::fabs(rendered[1] - static_cast<float>(std::sin(6.283185307179586 * 440.0 / 48000.0) * 0.1)) < 0.00001f);
|
||||
const xtc_graph *before_invalid = canvas->graph();
|
||||
unsigned int before_submit = submissions;
|
||||
CHECK(canvas->connect_ports({2U, 1U, 1U, 0U}) == XTC_ERR_GRAPH_CYCLE);
|
||||
CHECK(canvas->graph() == before_invalid && submissions == before_submit && contains(&window, "graph contains a cycle"));
|
||||
CHECK(canvas->connect_ports({1U, 0U, 2U, 0U}) == XTC_ERR_INVALID_ARGUMENT);
|
||||
CHECK(canvas->connect_ports({0U, 1U, 1U, 0U}) == XTC_ERR_INVALID_STATE);
|
||||
submit_result = XTC_ERR_CAPACITY;
|
||||
xtc_plan *before_plan = handle.plan;
|
||||
CHECK(canvas->add_node(XTC_NODE_GAIN) == XTC_ERR_CAPACITY);
|
||||
CHECK(canvas->graph() == before_invalid && handle.plan == before_plan && contains(&window, "Graph unchanged: capacity"));
|
||||
submit_result = XTC_ERR_IO;
|
||||
CHECK(canvas->add_node(XTC_NODE_GAIN) == XTC_ERR_IO && canvas->graph() == before_invalid);
|
||||
submit_result = XTC_OK;
|
||||
CHECK(canvas->select_node(0U) && canvas->move_node(0U, 65, 90));
|
||||
CHECK(submissions == before_submit + 2U); // Moving is GUI state, no compilation.
|
||||
CHECK(canvas->add_node(XTC_NODE_GAIN) == XTC_OK);
|
||||
int left, top;
|
||||
CHECK(canvas->node_position(0U, left, top) && left == 65 && top == 90);
|
||||
CHECK(canvas->delete_selected() == XTC_OK); // Newly added node.
|
||||
CHECK(canvas->select_connection(1U, 2U) && canvas->delete_selected() == XTC_OK);
|
||||
CHECK(xtc_plan_process(handle.plan, nullptr, rendered, 32U) == XTC_OK && rendered[1] == 0.0f);
|
||||
CHECK(canvas->connect_ports({1U, 1U, 2U, 0U}) == XTC_OK);
|
||||
CHECK(canvas->select_node(1U) && canvas->delete_selected() == XTC_OK);
|
||||
CHECK(xtc_graph_get_counts(canvas->graph(), &nodes, &edges) == XTC_OK && nodes == 3U && edges == 1U);
|
||||
xtc_node_info surviving{};
|
||||
CHECK(xtc_graph_get_node(canvas->graph(), 1U, &surviving) == XTC_OK && surviving.id == 2U);
|
||||
engine_status.active_revision = engine_status.submitted_revision;
|
||||
press(play);
|
||||
pump();
|
||||
CHECK(contains(&window, "Graph: active in engine"));
|
||||
CHECK(canvas->add_node(XTC_NODE_GAIN) == XTC_OK);
|
||||
CHECK(contains(&window, "Graph: queued"));
|
||||
engine_status.active_revision = engine_status.submitted_revision;
|
||||
pump();
|
||||
CHECK(contains(&window, "Graph: active in engine"));
|
||||
press(stop);
|
||||
for (size_t id : {0U, 2U, 3U, 5U}) { CHECK(canvas->select_node(id)); CHECK(canvas->delete_selected() == XTC_OK); }
|
||||
CHECK(xtc_graph_get_counts(canvas->graph(), &nodes, &edges) == XTC_OK && nodes == 0U && edges == 0U);
|
||||
CHECK(xtc_plan_process(handle.plan, nullptr, rendered, 32U) == XTC_OK && rendered[1] == 0.0f);
|
||||
engine_status.xruns = 3U;
|
||||
engine_status.processed_blocks = 123U;
|
||||
pump();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "../../src/gui/main_window.hpp"
|
||||
#include "../../src/gui/graph_widget.hpp"
|
||||
#include <FL/Fl.H>
|
||||
#include <FL/Fl_Group.H>
|
||||
#include <chrono>
|
||||
|
|
@ -15,6 +16,14 @@ Fl_Widget *find(Fl_Group &group, const char *label)
|
|||
}
|
||||
return nullptr;
|
||||
}
|
||||
GraphWidget *canvas_in(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 = canvas_in(*nested)) return canvas;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
void pump()
|
||||
{
|
||||
const auto end = std::chrono::steady_clock::now() + std::chrono::milliseconds(400);
|
||||
|
|
@ -33,11 +42,28 @@ int main(int argc, char **argv)
|
|||
auto *stop = find(window, "Stop");
|
||||
auto *reconnect = find(window, "Reconnect");
|
||||
CHECK(play != nullptr && stop != nullptr && reconnect != nullptr);
|
||||
auto *canvas = canvas_in(window);
|
||||
CHECK(canvas != nullptr);
|
||||
CHECK(canvas->add_node(XTC_NODE_OSCILLATOR) == XTC_OK);
|
||||
CHECK(canvas->add_node(XTC_NODE_GAIN) == XTC_OK);
|
||||
CHECK(canvas->add_node(XTC_NODE_OUTPUT) == XTC_OK);
|
||||
CHECK(canvas->connect_ports({0U, 1U, 1U, 0U}) == XTC_OK);
|
||||
CHECK(canvas->connect_ports({1U, 1U, 2U, 0U}) == XTC_OK);
|
||||
for (int restart = 0; restart < 3; ++restart) {
|
||||
CHECK(play->active());
|
||||
play->do_callback();
|
||||
pump();
|
||||
CHECK(find(window, "running (silence") != nullptr);
|
||||
CHECK(find(window, "running — modular graph") != nullptr);
|
||||
CHECK(find(window, "Graph: active in engine") != nullptr);
|
||||
const xtc_graph *unchanged = canvas->graph();
|
||||
CHECK(canvas->connect_ports({1U, 1U, 1U, 0U}) == XTC_ERR_GRAPH_CYCLE);
|
||||
CHECK(canvas->graph() == unchanged);
|
||||
CHECK(canvas->add_node(XTC_NODE_METER) == XTC_OK);
|
||||
pump();
|
||||
CHECK(find(window, "Graph: active in engine") != nullptr);
|
||||
CHECK(canvas->delete_selected() == XTC_OK);
|
||||
pump();
|
||||
CHECK(find(window, "Graph: active in engine") != nullptr);
|
||||
auto *status = find(window, "JACK |");
|
||||
CHECK(status != nullptr);
|
||||
unsigned int rate = 0U, frames = 0U, blocks = 0U;
|
||||
|
|
@ -50,11 +76,11 @@ int main(int argc, char **argv)
|
|||
CHECK(find(window, "ready / stopped") != nullptr);
|
||||
play->do_callback();
|
||||
pump();
|
||||
CHECK(find(window, "running (silence") != nullptr);
|
||||
CHECK(find(window, "running — modular graph") != nullptr);
|
||||
window.do_callback(); // Close with an active client; synchronizes callbacks.
|
||||
CHECK(!window.shown());
|
||||
pump(); // No timer may access the closed client.
|
||||
}
|
||||
std::puts("Live GUI/JACK start/stop/reconnect/active-close tests passed.");
|
||||
std::puts("Live GUI/JACK graph edits/revision acknowledgements/start/stop/reconnect/active-close tests passed.");
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue