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

@ -28,8 +28,8 @@ tempo automation.
## Phase 1 offline API
`xtc_graph_create` accepts 1..256 nodes; IDs are insertion indexes, stable for
that graph's lifetime. Supported nodes are gain (any finite multiplier), sine
`xtc_graph_create` accepts 1..256 nodes; IDs are monotonic stable identifiers, never reused
within a graph's lifetime. Enumeration indexes are dense and distinct from IDs. Supported nodes are gain (any finite multiplier), sine
oscillator (finite nonnegative Hz), pass-through peak meter and audio output.
Ports carry mono float audio: input 0, output 1. Oscillators have no input and
output nodes have no output. Port lookup reports direction, signal type and
@ -39,7 +39,7 @@ edit becomes visible. Fan-out copies a source to consumers; fan-in sums inputs
without normalization. Failed additions leave IDs and graph contents unchanged.
`xtc_graph_compile(graph, sample_rate, max_frames, &plan)` runs on the control
thread. Empty graphs are rejected. Sample rate must be finite and positive,
thread. Empty graphs compile to silence. Sample rate must be finite and positive,
oscillators must be at or below Nyquist, and max_frames is 1..65536. Compilation
allocates and copies topology/parameters, builds a topological schedule and
input adjacency, and allocates one max_frames float buffer per node. The
@ -56,7 +56,9 @@ with processing, including reset, meter reads or destruction.
Unconnected non-oscillator nodes receive external input. Connected inputs
receive only the sum of their upstream buffers. Explicit output nodes are
summed to the caller's output. If none exist, terminal nodes are summed for
compatibility with the original gain-chain API. Disconnected components cannot
compatibility with the original gain-chain API, unless
`xtc_graph_require_output_nodes(graph, 1)` is set. The GUI uses that policy;
no output nodes means silence. Disconnected components cannot
contribute when explicit outputs exist, but they are still processed. Floating
point overflow and nonfinite input become silence per sample. No clipping or
normalization is applied to finite samples.
@ -103,7 +105,10 @@ Use `xtc_backend.h` on one serialized control thread:
between enumeration and connection; route failures are returned as I/O errors.
4. During playback, submit edited graphs and periodically collect retired
resources. Capacity means one pending graph is still awaiting adoption;
keep the current graph and retry later. get_status is a nonblocking snapshot
keep the current graph and retry later. Stopped submissions instead compile
a replacement engine off-thread and swap only after success, while callbacks
are quiescent. Its block/revision counters restart and oscillator phase resets;
no edit is blocked behind a plan that cannot be adopted until playback. get_status is a nonblocking snapshot
of rate, frames, running/server-loss/rate-change flags, xruns, processed
blocks and last process result. No active DSP memory is exposed.
5. Stop pauses the retained DSP state after deactivation. Restart resumes it.
@ -133,4 +138,46 @@ test README. Current limitations: mono input/output, no parameter ramping or
crossfaded graph changes, no realtime meter bridge, no automatic reconnect,
no physical-device or long-duration endurance qualification. Phase 3 connects
the FLTK shell to backend controls and status; see GUI.md. Its editors remain
placeholders and the GUI submits no playback graph yet.
placeholders except the Phase 4 modular editor described below.
## Phase 4 graph snapshots and edits
The control-thread API exposes copies through get_counts, get_node(index) and
get_connection(index), including node IDs/types/values and port endpoints. No
mutable graph/engine storage is exposed. Node IDs increase monotonically;
removing a node compacts private storage and adjusts private edge indexes but
leaves surviving IDs unchanged. Exhausted ID space returns capacity. Compiled
meter lookup resolves stable IDs too.
`xtc_graph_apply_edit(source, edit, &candidate, &added_id)` accepts ADD_NODE,
REMOVE_NODE, CONNECT or DISCONNECT. It copies the source, validates the edit,
and returns a caller-owned candidate on success. On failure candidate is NULL,
source and added_id are unchanged. added_id is optional and written only for
ADD_NODE. ADD_NODE validates type and the used parameter (finite gain, or finite
nonnegative oscillator Hz). Meter/output do not use the value field. Compile
additionally checks the oscillator against the actual sample rate. Removing
a node removes its incident connections; disconnecting a nonexistent edge
returns not-found. Missing nodes/ports, reversed directions, duplicate edges,
cycles and unknown command/type values are rejected without visible edits.
The GUI keeps a 64-node graph requiring explicit output nodes. It creates a
candidate, submits that exact candidate to the backend, then swaps its owned
graph and refreshes the canvas only if submission succeeds. Failed compile or
pending-slot pressure destroys the candidate and retains both the displayed
graph and previously submitted engine plan. When disconnected at startup,
edits can be stored locally and are submitted on reconnect. Lost/changed active
backends reject edits until reconnect. Reconnect submits the retained graph.
Submitted/active revision counters expose adoption without exposing plan
pointers. A submission increments the submitted revision; only the audio
consumer acknowledges the revision when adopting the plan at a block boundary.
The editor shows prepared while stopped, queued while an older plan is active,
and active when those revisions agree. Atomic fields remain independent
snapshots and wrap modulo UINT_MAX + 1. Rebuilding a stopped engine or reopening
a backend starts a new counter epoch. See decision 0004 for slot ordering.
Movement, selection and scroll position are GUI-only state; they never compile
or publish audio plans. Empty graphs and graphs without explicit outputs are
silent under the GUI policy. Terminal fallback remains the default for earlier
offline/standalone callers. Input on unconnected nodes retains the existing
external-input contract; the GUI does not connect external JACK ports.