1
0
Fork 0
forked from hanez/xtc
xtc/docs/decisions/0004-jack-and-plan-handoff.md

108 lines
6.1 KiB
Markdown

# Decision 0004: JACK backend and bounded plan handoff
**Status:** Accepted for Phase 2
## Backend
Use the JACK C client API. The inspected system provides PipeWire's maintained
JACK compatibility library (`pkg-config jack` reports 3.1609.0), headers, a live
server, and mono audio ports. This is JACK client integration, not native
PipeWire or ALSA PCM. JACK owns device access, timing, sample rate and block
size. The application registers one mono input/output pair and exposes port
inspection and explicit routing. It neither starts the server nor changes its
configuration or automatically connects hardware.
JACK support is optional at build time. Without JACK development metadata,
backend open returns unsupported; offline and handoff tests still work.
Consumers that call backend APIs link `pkg-config --libs jack` as well as `-lm`.
No backend headers appear in public headers. The kind/configuration/status API
is the boundary for adding another backend; there is no speculative plugin
framework or second driver implementation.
The optional dependency provides actual audio callbacks and port routing.
PipeWire's installed JACK library is MIT licensed (`pipewire-jack/COPYING`).
Traditional JACK client libraries use LGPL licensing; review the actual linked
implementation, transitive libraries and notices when distributing. Nothing
is bundled or installed here. ALSA is still a future MIDI candidate.
## C99 and atomic operations
Retain C99 and use GCC/Clang `__atomic` builtins. This is a documented compiler
extension, avoiding a language-standard change and keeping the existing core
C99-compatible on the selected compilers. Initialization requires pointer and
unsigned-int atomics to be always lock-free, or returns unsupported before
callbacks are installed. Naturally aligned fields reside in calloc-allocated
objects. There is no mutex or libatomic fallback in the callback path.
## Ownership and publication
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/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.
The active pointer and all active DSP state belong exclusively to the consumer.
Producer never reads them. Caller graph memory is copied before publication;
callers never receive plan pointers through the realtime/backend API.
A full pending slot returns capacity without replacing anything. A full retired
slot defers adoption while the current plan continues processing. The consumer
performs at most one adoption per block and never loops waiting for the producer.
Producer collection can overlap processing because the retired plan is no
longer reachable from the consumer. Collection clears the slot before freeing;
consumer may then retire another, distinct plan. Publication never immediately
frees the previously active plan.
Submitting collects any already retired plan, so a producer repeatedly submitting
also reclaims memory. If it becomes idle, one pending, one active, and one retired
plan bound retained resources (at most three plans, each with up to 64 MiB of
sample buffers). Periodic collect is recommended. Shutdown closes the JACK client
and synchronizes its callbacks before destroying any active/pending/retired plan.
Standalone realtime-engine callers must stop and join their consumer themselves.
## Status, errors and recovery
Unsigned-int status fields use lock-free atomics. Values are individual snapshots,
not one transactional state. Counters wrap naturally; no string formatting,
logging, allocation, GUI operation or blocking synchronization occurs in project
callbacks. JACK's shutdown notification only stores a flag and leaves cleanup to
the control thread. Other notifications store the current format and xrun count.
Buffer changes within the configured maximum require no allocation. Larger
blocks are fully silenced and reported as capacity errors; smaller blocks can
resume processing. Sample-rate changes latch a flag and silence processing
rather than replaying an oscillator compiled for a different rate. Close/reopen
and resubmit after a rate change or server loss. There is no automatic reconnect.
JACK-deactivate pauses the retained plan; restart continues its oscillator phase.
Plan replacement starts the newly compiled oscillator at phase zero, without
crossfade. Smooth parameter ramps and click-free graph transitions are future work.
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.