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
|
|
@ -28,7 +28,7 @@ stable node IDs, topological order, precomputed input adjacency, per-node mono
|
|||
buffers and oscillator/meter state. Each plan has one processing owner. Graph
|
||||
edits and destruction cannot affect an existing plan. All nodes are scheduled,
|
||||
including disconnected nodes; only explicit output nodes are rendered (or
|
||||
terminal nodes when no output node exists). See AUDIO_ENGINE.md for contracts.
|
||||
terminal nodes for legacy callers that do not require explicit outputs). See AUDIO_ENGINE.md for contracts.
|
||||
`xtc_realtime` now provides a one-pending/one-retired handoff around an
|
||||
exclusively audio-owned active plan. `xtc_backend` is the C backend boundary;
|
||||
the optional JACK implementation drives that same realtime engine.
|
||||
|
|
@ -51,4 +51,20 @@ on the GUI thread. A GUI timer reads snapshots and collects retired resources;
|
|||
no audio callback calls FLTK. Successful window close first closes the JACK
|
||||
client (synchronizing callbacks), then removes the GUI timer and hides the
|
||||
window. Failed close leaves both handle and window available for retry. View
|
||||
switching is GUI state only; editors have no domain mutations yet. See GUI.md.
|
||||
switching and node positions are GUI state only. Semantic edits use transactional
|
||||
C commands and publish independent compiled snapshots. See GUI.md.
|
||||
|
||||
## Editable graph and canvas
|
||||
|
||||
Phase 4 adds read-only C snapshots, monotonic node IDs, and a small set of C
|
||||
edit commands. The GUI owns an opaque graph handle, never private arrays.
|
||||
An edit produces a candidate, and the GUI commits it only after backend submit
|
||||
succeeds (or locally while offline). A busy pending slot or invalid edit leaves
|
||||
the prior model/plan unchanged. Active/submitted revisions distinguish the
|
||||
queued drawing from the engine's acknowledged plan. Empty/output-less editor
|
||||
graphs render silence; legacy offline terminal fallback remains available.
|
||||
|
||||
GraphWidget owns only selection, node positions, pointer gestures and drawing.
|
||||
Its fixed canvas is hosted by FLTK scrolling. C owns nodes, ports, edges,
|
||||
validation, ID assignment, compilation and rendering. No audio thread accesses
|
||||
GUI-owned state. Reconnect republishes the same retained editable graph.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ make gui
|
|||
core/offline tests, concurrent plan-handoff stress, JACK fault injection
|
||||
(when JACK headers are available), and the startup check. `debug` builds and tests separately in
|
||||
`build/debug`. `gui` builds the FLTK shell with backend controls and status.
|
||||
The shell starts a silent engine and never connects physical ports. Generated files remain under `build` and header changes trigger rebuilds.
|
||||
The shell starts with a silent graph and never connects physical ports. Generated files remain under `build` and header changes trigger rebuilds.
|
||||
Compiler, flags, `BUILD_DIR`, `PKG_CONFIG`, `FLTK_CONFIG`, and installation
|
||||
`PREFIX` can be overridden through Make variables. C uses C99; GUI uses C++17.
|
||||
|
||||
|
|
@ -92,8 +92,25 @@ exit-time font allocations; see tests/integration/README.md for the distinction
|
|||
and command. No compiler warnings remain with GCC or Clang. Core/backend tests
|
||||
pass with unsuppressed ASan/UBSan. No packages or system configuration changed.
|
||||
|
||||
## Phase 4 verification
|
||||
|
||||
```sh
|
||||
make clean all test gui debug test-gui test-gui-jack
|
||||
make BUILD_DIR=build/nojack PKG_CONFIG=false all test gui test-gui
|
||||
```
|
||||
|
||||
The normal suite adds test_graph_edits for stable IDs, dense snapshots,
|
||||
transactional rejection/deletion/disconnect and known rendered samples before
|
||||
and after edits. Core/backend/realtime tests pass under unsuppressed Clang
|
||||
ASan/UBSan; ThreadSanitizer passes the concurrent handoff test with revision-to-
|
||||
gain checks. GUI ASan/UBSan tests pass with the existing fontconfig-only leak
|
||||
suppression. Live GUI tests build a connected graph, adopt accepted edits during
|
||||
playback, reject cycles, reconnect and close active clients. Mouse/visual smoke
|
||||
checks cover node creation, wiring, cycle rejection, dragging and deletion.
|
||||
No physical routes, new dependencies or system changes are introduced.
|
||||
|
||||
## Next milestone
|
||||
|
||||
Phase 4 implements the modular graph editor and submits validated graph edits
|
||||
through the C API. Physical playback/endurance, project persistence and
|
||||
sequencing remain unverified/unimplemented as applicable.
|
||||
Phase 5 adds versioned project persistence and the sequencer foundation, stable
|
||||
track/clip IDs and timeline editing. Physical-device playback/endurance remains
|
||||
unverified; graph parameters are fixed GUI presets and undo/zoom are not present.
|
||||
|
|
|
|||
45
docs/GUI.md
45
docs/GUI.md
|
|
@ -20,10 +20,10 @@ not energyXT's exact artwork or proprietary resources.
|
|||
## Phase 3 shell
|
||||
|
||||
The shell opens JACK on startup through `xtc_backend.h`. It never accesses
|
||||
backend structures, processes samples, creates domain objects, or calls JACK
|
||||
itself. All backend control calls run on the GUI thread; FLTK is never called
|
||||
backend structures, processes samples, or calls JACK itself. Graph domain
|
||||
data is owned by opaque C objects, manipulated only through the C API. All backend control calls run on the GUI thread; FLTK is never called
|
||||
from an audio callback. No server is started and no audio ports are connected
|
||||
by the GUI. With no graph submitted, the running backend produces silence.
|
||||
by the GUI. The initially empty graph produces silence.
|
||||
|
||||
- **Play / F5:** start the engine. **Stop / F8:** deactivate it. **Space:** toggle.
|
||||
These are engine controls, not timeline playback or transport positioning.
|
||||
|
|
@ -32,7 +32,7 @@ by the GUI. With no graph submitted, the running backend produces silence.
|
|||
the handle and reports the failure instead of opening another client.
|
||||
- **View menu / Ctrl+1, Ctrl+2, Ctrl+3:** switch between sequencer, mixer and
|
||||
modular graph. Tabs provide the same view switching. Editors remain clearly
|
||||
labeled placeholders; no project editing is implied.
|
||||
labeled placeholders for sequencer/mixer; Modular Graph is implemented below.
|
||||
- **File / Quit / Ctrl+Q** or the window close button: close the backend and
|
||||
synchronize audio callbacks before hiding/destroying widgets. A failed close
|
||||
leaves the window open for a retry. Unsupported New/Open/Save actions are
|
||||
|
|
@ -62,3 +62,40 @@ removal. `make test-gui-jack` additionally requires a running JACK-compatible
|
|||
server, tests repeated start/stop, reconnect and active-client close, and creates
|
||||
no routes. Physical playback is outside this shell. See BUILD.md and the
|
||||
integration test README for sanitizer limitations and keyboard smoke results.
|
||||
|
||||
## Phase 4 modular editor
|
||||
|
||||
Open **Modular Graph** or press **Ctrl+3**. Add oscillator (440 Hz), gain (0.10),
|
||||
pass-through meter or output nodes with the toolbar. Their parameters are fixed
|
||||
presets in this milestone and are drawn on each node; parameter editing and
|
||||
live meter display are not implemented. Connect oscillator → gain → meter →
|
||||
output for a simple test chain. No sound reaches hardware until ports are
|
||||
explicitly connected outside the GUI. Output nodes are required to render;
|
||||
isolated sources and an empty graph produce silence.
|
||||
|
||||
Drag a node body/title to move it. Drag a circular output port to an input port
|
||||
to connect. Click a node or wire to select it (yellow outline/wire), then press
|
||||
Delete/Backspace or **Delete selection**. Deleting a node deletes its incident
|
||||
wires. Escape cancels a drag preview. Canvas scrollbars pan the view; positions
|
||||
are retained across edits/reconnect but not across application restart. There
|
||||
is no zoom, multiselection, undo, project persistence or external routing UI yet.
|
||||
The editor supports 64 nodes on a fixed 2000x4000 canvas.
|
||||
|
||||
Every semantic action becomes a C graph command. The window owns the graph
|
||||
handle and publishes the C-created candidate through the backend. Cycle,
|
||||
invalid-direction, duplicate connection, missing-port, capacity and backend
|
||||
failures are shown below the canvas; failed edits leave the graph unchanged.
|
||||
Capacity feedback distinguishes the possible node limit/pending edit condition
|
||||
and allows retry. Editing while stopped replaces the prepared plan without
|
||||
starting playback. Editing with a disconnected startup backend stays local
|
||||
until reconnect. A lost or changed live backend requires reconnect before
|
||||
further semantic edits. Moving/selecting nodes does not touch the engine.
|
||||
|
||||
The graph state line distinguishes offline, prepared, queued and active states.
|
||||
Queued explicitly means the drawing represents the next submitted graph while
|
||||
the engine finishes the previous plan. The 100 ms GUI timer observes audio
|
||||
adoption revisions and then shows active. Graphs are not edited in place on the
|
||||
audio thread. There is no automatic retry/hidden edit queue; users retry a
|
||||
rejected capacity edit. Plan replacement can reset oscillator phase and is not
|
||||
crossfaded. Render validation and active replacement tests are described in
|
||||
tests/integration/README.md.
|
||||
|
|
|
|||
|
|
@ -35,13 +35,21 @@ and shutdown through the C API.
|
|||
Implemented: dark FLTK shell, Play/Stop/Reconnect, view/menu shortcuts, backend
|
||||
state/format/xrun/process status, persistent command errors, timer-based refresh
|
||||
and retirement collection, and callback-safe close/retry. Mock-backend GUI tests
|
||||
and live GUI/JACK start/stop/reconnect/close tests pass under Xvfb. Editors and
|
||||
project-file actions remain placeholders; engine runs in silence. See GUI.md.
|
||||
and live GUI/JACK start/stop/reconnect/close tests pass under Xvfb. Sequencer,
|
||||
mixer and project-file actions remain placeholders; initial graph is silent. See GUI.md.
|
||||
|
||||
## Phase 4 — Modular editor
|
||||
Draw nodes/ports/edges; create, move and connect nodes; reject invalid edges;
|
||||
safely submit edits to the C core.
|
||||
|
||||
Implemented: scrollable node/port/wire canvas; add/select/move/delete nodes;
|
||||
drag connections, select/delete wires, and show invalid-edit feedback. C owns
|
||||
stable IDs, read-only snapshots and transactional edits. Exact accepted graph
|
||||
snapshots are submitted to the backend; prepared/queued/active state is exposed
|
||||
through revision acknowledgements. Core/render, GUI rollback, mouse smoke and
|
||||
live adoption tests pass. See GUI.md and AUDIO_ENGINE.md. Preset parameters,
|
||||
mono-only processing, fixed canvas and no persistence/undo/zoom remain limits.
|
||||
|
||||
## Phase 5 — Project and sequencer
|
||||
Versioned persistence, stable IDs, tracks, clips, timeline, selection, zoom,
|
||||
scrolling, and transport synchronization.
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ objects. There is no mutex or libatomic fallback in the callback path.
|
|||
One serialized control producer and one audio consumer share two pointer slots:
|
||||
|
||||
- `pending`: producer publishes a fully compiled plan with release; consumer
|
||||
exchanges it for NULL with acquire at the next eligible block boundary.
|
||||
exchanges it for NULL with acquire/release at the next eligible block boundary.
|
||||
- `retired`: consumer releases its previous active plan into this slot only
|
||||
after its final access. Producer exchanges with NULL using acquire/release,
|
||||
then destroys that acknowledged plan on the control thread.
|
||||
|
|
@ -82,3 +82,27 @@ crossfade. Smooth parameter ramps and click-free graph transitions are future wo
|
|||
|
||||
No stereo, recording, MIDI, native PipeWire API, GUI engine controls or hardware
|
||||
configuration dialogs are part of this phase.
|
||||
|
||||
## Phase 4 adoption acknowledgement and stopped editing
|
||||
|
||||
The pending pointer has an associated atomic revision. Producer stores the
|
||||
revision before release-publishing the pointer. Consumer acquire-loads a
|
||||
non-NULL pending pointer, reads its revision, and only then releases that slot
|
||||
with an acquire/release exchange to NULL. Thus producer cannot reuse the
|
||||
revision field until the previous consumer read is complete. Consumer publishes
|
||||
active_revision when selecting that plan; the GUI compares it with the
|
||||
producer's submitted_revision. These are wrapping counters and individual
|
||||
snapshots, not plan pointers or a multi-field transactional snapshot.
|
||||
|
||||
While JACK is stopped, a backend submission builds a separate realtime engine,
|
||||
submits/compiles the graph there, and replaces the retained engine only on
|
||||
success. Process callbacks cannot run on an inactive client or after successful
|
||||
deactivation, and notification callbacks do not access the engine pointer.
|
||||
The control thread can therefore reclaim all old state safely. Counters and
|
||||
oscillator phases restart for that new engine epoch. The standalone realtime
|
||||
API keeps its one-pending-slot contract regardless of external stop state.
|
||||
|
||||
The callback still adopts at most one plan and never allocates/frees. Revision
|
||||
ordering is tested alongside the plan's rendered gain under 2,000 concurrent
|
||||
submissions and ThreadSanitizer. Stopped compile failure and active pending
|
||||
capacity are checked with backend fault injection.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue