Phase 4: Add modular graph editor and transactional graph edits

This commit is contained in:
Johannes Findeisen 2026-10-26 23:55:35 +01:00
commit 5c41d7f6da
27 changed files with 1023 additions and 64 deletions

View file

@ -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.

View file

@ -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();

View file

@ -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;
}

View file

@ -78,7 +78,7 @@ int main(void)
xtc_backend_config config = {XTC_BACKEND_JACK, "test", 64U, 1U};
xtc_backend *backend = NULL;
xtc_backend_status status;
xtc_graph *graph = NULL;
xtc_graph *graph = NULL, *invalid_graph = NULL;
size_t node, i;
const char *name;
int visits = 0, iteration;
@ -110,7 +110,11 @@ int main(void)
CHECK(xtc_graph_add_gain(graph, 0.5f, &node) == XTC_OK);
CHECK(xtc_backend_open(&config, &backend) == XTC_OK);
CHECK(xtc_backend_submit(backend, graph) == XTC_OK);
CHECK(xtc_backend_submit(backend, graph) == XTC_ERR_CAPACITY);
CHECK(xtc_backend_submit(backend, graph) == XTC_OK); /* Stopped edits replace pending plan. */
CHECK(xtc_graph_create(1U, &invalid_graph) == XTC_OK);
CHECK(xtc_graph_add_oscillator(invalid_graph, 25000.0f, &node) == XTC_OK);
CHECK(xtc_backend_submit(backend, invalid_graph) == XTC_ERR_INVALID_ARGUMENT);
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.submitted_revision == 1U && status.active_revision == 0U);
CHECK(xtc_backend_port_name(backend, XTC_PORT_OUTPUT, &name) == XTC_OK && strcmp(name,"test:output") == 0);
CHECK(xtc_backend_visit_ports(backend, XTC_PORT_INPUT, visit, &visits) == XTC_OK && visits == 1);
CHECK(xtc_backend_connect(backend, XTC_PORT_OUTPUT, "fake:in") == XTC_ERR_INVALID_STATE);
@ -124,6 +128,11 @@ int main(void)
CHECK(xtc_backend_disconnect(backend, XTC_PORT_OUTPUT, "fake:in") == XTC_OK);
for (i = 0U; i < 128U; ++i) input[i] = 1.0f;
CHECK(process_cb(32U, context) == 0 && output[0] == 0.5f);
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.active_revision == status.submitted_revision);
CHECK(xtc_backend_submit(backend, invalid_graph) == XTC_ERR_INVALID_ARGUMENT);
CHECK(xtc_backend_submit(backend, graph) == XTC_OK);
CHECK(xtc_backend_submit(backend, graph) == XTC_ERR_CAPACITY);
CHECK(process_cb(32U, context) == 0 && output[0] == 0.5f);
CHECK(size_cb(128U, context) == 0);
CHECK(process_cb(128U, context) == 0 && output[127] == 0.0f);
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.process_result == XTC_ERR_CAPACITY);
@ -166,6 +175,7 @@ int main(void)
}
CHECK(closed >= 104);
CHECK(xtc_backend_close(NULL) == XTC_OK);
xtc_graph_destroy(invalid_graph);
xtc_graph_destroy(graph);
puts("JACK backend fault-injection tests passed.");
return 0;

View file

@ -0,0 +1,83 @@
#include "xtc/xtc_engine.h"
#include <math.h>
#include <stdio.h>
#define CHECK(c) do { if (!(c)) { fprintf(stderr,"FAIL %s:%d: %s\n",__FILE__,__LINE__,#c); return 1; } } while (0)
static xtc_result apply(xtc_graph **graph, xtc_graph_edit *edit, size_t *id)
{
xtc_graph *candidate = NULL;
xtc_result result = xtc_graph_apply_edit(*graph, edit, &candidate, id);
if (result == XTC_OK) { xtc_graph_destroy(*graph); *graph = candidate; }
return result;
}
int main(void)
{
xtc_graph *graph = NULL, *candidate = NULL;
xtc_plan *old = NULL, *fresh = NULL;
xtc_graph_edit edit = {0};
xtc_node_info info;
xtc_connection_info connection;
size_t osc, gain, meter, output, added = 77U, nodes, edges;
float samples[4] = {9,9,9,9}, peak;
CHECK(xtc_graph_create(8U, &graph) == XTC_OK);
CHECK(xtc_graph_require_output_nodes(graph, 2) == XTC_ERR_INVALID_ARGUMENT);
CHECK(xtc_graph_require_output_nodes(graph, 1) == XTC_OK);
CHECK(xtc_graph_compile(graph, 48000.0, 4U, &fresh) == XTC_OK);
CHECK(xtc_plan_process(fresh, NULL, samples, 4U) == XTC_OK && samples[0] == 0.0f && samples[3] == 0.0f);
xtc_plan_destroy(fresh);
edit.kind = XTC_EDIT_ADD_NODE; edit.data.add.type = XTC_NODE_OSCILLATOR; edit.data.add.value = 12000.0f;
CHECK(apply(&graph, &edit, &osc) == XTC_OK);
CHECK(xtc_graph_compile(graph, 48000.0, 4U, &fresh) == XTC_OK);
CHECK(xtc_plan_process(fresh, NULL, samples, 4U) == XTC_OK && samples[1] == 0.0f); /* No output. */
xtc_plan_destroy(fresh);
edit.data.add.type = XTC_NODE_GAIN; edit.data.add.value = 0.25f;
CHECK(apply(&graph, &edit, &gain) == XTC_OK);
edit.data.add.type = XTC_NODE_METER;
CHECK(apply(&graph, &edit, &meter) == XTC_OK);
edit.data.add.type = XTC_NODE_OUTPUT;
CHECK(apply(&graph, &edit, &output) == XTC_OK);
edit.kind = XTC_EDIT_CONNECT;
edit.data.connection = (xtc_connection_info){osc, 1U, gain, 0U};
CHECK(apply(&graph, &edit, NULL) == XTC_OK);
edit.data.connection = (xtc_connection_info){gain, 1U, meter, 0U};
CHECK(apply(&graph, &edit, NULL) == XTC_OK);
edit.data.connection = (xtc_connection_info){meter, 1U, output, 0U};
CHECK(apply(&graph, &edit, NULL) == XTC_OK);
CHECK(xtc_graph_get_connection(graph, 2U, &connection) == XTC_OK && connection.source_node == meter && connection.destination_node == output);
CHECK(xtc_graph_compile(graph, 48000.0, 4U, &old) == XTC_OK);
edit.data.connection = (xtc_connection_info){meter, 1U, gain, 0U};
CHECK(xtc_graph_apply_edit(graph, &edit, &candidate, &added) == XTC_ERR_GRAPH_CYCLE && candidate == NULL && added == 77U);
CHECK(xtc_graph_get_counts(graph, &nodes, &edges) == XTC_OK && nodes == 4U && edges == 3U);
edit.data.connection = (xtc_connection_info){gain, 0U, meter, 0U};
CHECK(apply(&graph, &edit, NULL) == XTC_ERR_INVALID_ARGUMENT);
edit.kind = XTC_EDIT_REMOVE_NODE; edit.data.node_id = gain;
CHECK(apply(&graph, &edit, NULL) == XTC_OK);
CHECK(xtc_graph_get_counts(graph, &nodes, &edges) == XTC_OK && nodes == 3U && edges == 1U);
CHECK(xtc_graph_get_node(graph, 1U, &info) == XTC_OK && info.id == meter && info.type == XTC_NODE_METER);
CHECK(xtc_graph_get_connection(graph, 0U, &connection) == XTC_OK && connection.source_node == meter);
edit.kind = XTC_EDIT_CONNECT; edit.data.connection = (xtc_connection_info){osc, 1U, meter, 0U};
CHECK(apply(&graph, &edit, NULL) == XTC_OK);
CHECK(xtc_graph_compile(graph, 48000.0, 4U, &fresh) == XTC_OK);
CHECK(xtc_plan_process(old, NULL, samples, 4U) == XTC_OK && fabsf(samples[1] - 0.25f) < 0.00001f);
CHECK(xtc_plan_process(fresh, NULL, samples, 4U) == XTC_OK && fabsf(samples[1] - 1.0f) < 0.00001f);
CHECK(xtc_plan_meter_peak(fresh, meter, &peak) == XTC_OK && fabsf(peak - 1.0f) < 0.00001f);
xtc_plan_destroy(old); xtc_plan_destroy(fresh);
edit.kind = XTC_EDIT_DISCONNECT;
CHECK(apply(&graph, &edit, NULL) == XTC_OK);
CHECK(apply(&graph, &edit, NULL) == XTC_ERR_NOT_FOUND);
edit.kind = XTC_EDIT_ADD_NODE; edit.data.add.type = XTC_NODE_GAIN; edit.data.add.value = NAN;
CHECK(apply(&graph, &edit, &added) == XTC_ERR_INVALID_ARGUMENT && added == 77U);
edit.data.add.value = 2.0f;
CHECK(apply(&graph, &edit, &added) == XTC_OK && added > output); /* Never reuse gain's ID. */
edit.kind = XTC_EDIT_REMOVE_NODE; edit.data.node_id = gain;
CHECK(apply(&graph, &edit, NULL) == XTC_ERR_NOT_FOUND);
edit.kind = (xtc_graph_edit_kind)999;
CHECK(apply(&graph, &edit, NULL) == XTC_ERR_INVALID_ARGUMENT);
edit.kind = XTC_EDIT_ADD_NODE; edit.data.add.type = (xtc_node_type)999;
CHECK(apply(&graph, &edit, NULL) == XTC_ERR_INVALID_ARGUMENT);
CHECK(xtc_graph_get_node(graph, SIZE_MAX, &info) == XTC_ERR_NOT_FOUND);
CHECK(xtc_graph_get_connection(graph, SIZE_MAX, &connection) == XTC_ERR_NOT_FOUND);
CHECK(xtc_graph_apply_edit(NULL, &edit, &candidate, NULL) == XTC_ERR_INVALID_ARGUMENT && candidate == NULL);
xtc_graph_destroy(graph);
puts("Transactional graph edits, stable IDs, snapshots and rendering passed.");
return 0;
}

View file

@ -89,7 +89,9 @@ static int validation(void)
CHECK(xtc_graph_create(SIZE_MAX, &graph) == XTC_ERR_INVALID_ARGUMENT && graph == NULL);
CHECK(xtc_graph_create(0U, &graph) == XTC_ERR_INVALID_ARGUMENT);
CHECK(xtc_graph_create(4U, &graph) == XTC_OK);
CHECK(xtc_graph_compile(graph, 48000.0, 4U, &plan) == XTC_ERR_INVALID_STATE && plan == NULL);
CHECK(xtc_graph_compile(graph, 48000.0, 4U, &plan) == XTC_OK);
xtc_plan_destroy(plan);
plan = NULL;
CHECK(xtc_graph_add_gain(graph, NAN, &unchanged) == XTC_ERR_INVALID_ARGUMENT && unchanged == 77U);
CHECK(xtc_graph_add_oscillator(graph, -1.0f, &unchanged) == XTC_ERR_INVALID_ARGUMENT);
CHECK(xtc_graph_add_gain(graph, 1.0f, &a) == XTC_OK);

View file

@ -16,6 +16,10 @@ static void *worker(void *context)
while (!__atomic_load_n(&state->stop, __ATOMIC_ACQUIRE)) {
if (xtc_realtime_process(state->engine, input, output, 32U) != XTC_OK)
__atomic_store_n(&state->failed, 1U, __ATOMIC_RELAXED);
xtc_realtime_status status;
(void)xtc_realtime_get_status(state->engine, &status);
float expected = status.active_revision == 2U ? 2.0f : status.active_revision % 2U ? 2.0f : 1.0f;
if (output[0] != expected) __atomic_store_n(&state->failed, 1U, __ATOMIC_RELAXED);
/* A whole block must come from one plan. No partially edited gains. */
for (i = 0U; i < 32U; ++i)
if ((output[i] != 0.0f && output[i] != 1.0f && output[i] != 2.0f) || output[i] != output[0])
@ -41,10 +45,12 @@ static int run(void)
CHECK(xtc_graph_add_gain(two, 2.0f, &node) == XTC_OK);
CHECK(xtc_realtime_submit(engine, one) == XTC_OK);
CHECK(xtc_realtime_submit(engine, two) == XTC_ERR_CAPACITY);
CHECK(xtc_realtime_get_status(engine, &status) == XTC_OK && status.submitted_revision == 1U && status.active_revision == 0U);
CHECK(xtc_realtime_process(engine, input, output, 4U) == XTC_OK && output[0] == 1.0f);
CHECK(xtc_realtime_submit(engine, two) == XTC_OK);
CHECK(xtc_realtime_process(engine, input, output, 4U) == XTC_OK && output[0] == 2.0f);
xtc_realtime_collect(engine);
CHECK(xtc_realtime_get_status(engine, &status) == XTC_OK && status.active_revision == 2U && status.submitted_revision == 2U);
CHECK(xtc_realtime_process(engine, input, output, 0U) == XTC_OK && output[0] == 2.0f);
CHECK(xtc_realtime_get_status(engine, &status) == XTC_OK && status.processed_blocks == 3U);
state.engine = engine; state.stop = 0U; state.failed = 0U;