Architecture
This page documents the static structure of gradient-motion-engine: which components exist, what each owns, and how they depend on each other. For the live signal flow and the design rationale, see index.md.
Component graph
┌────────────────────────────────────────────────────────────────────────────┐
│ gradient-motiond (daemon binary) │
│ │
│ GradientEngineApplication ─── ConfigurationManager │
│ │ │
│ │ owns │
│ ▼ │
│ gme::engine::GradientEngine │
│ │ │
│ ├─ owns ── gme::time::MtcTickSource ───────► mtcreceiver v2 │
│ │ (RtMidi) │
│ │ │
│ ├─ owns ── gme::signal::LockFreeQueue<FadeCommand, 64> │
│ │ │
│ ├─ owns ── gme::daemon::comms::OscServer ──► liblo (UDP) │
│ │ │ │
│ │ └─ uses ─ gme::signal::parseFadeOscCommand │
│ │ │
│ └─ owns ── gme::motion::MotionRegistry │
│ │ │
│ ├─ holds ─ std::unordered_map<id, IMotion> │
│ │ │
│ └─ uses ── gme::motion::MotionFactory │
│ │ │
│ ├─ uses ── gme::gradient::CurveFactory │
│ │ (LinearCurve, SigmoidCurve, │
│ │ BezierCurve, EaseIn/OutCurve, │
│ │ SCurve, all wrapped in │
│ │ ResampledCurve) │
│ │ │
│ └─ produces ── gme::motion::FadeMotion │
│ │ │
│ └─ uses ─ gme::osc::sendFloat│
│ (liblo UDP) │
│ │
└────────────────────────────────────────────────────────────────────────────┘
The daemon (gradient-motiond) is the only binary. Tests link gradient_motion (the static library) plus a per-test set of mocks and helpers. The library itself does not link liblo for consumers that don't use OscServer directly — OscServer is forward-declared in GradientEngine.h and GradientEngine.cpp is compiled into the daemon target.
Module dependency direction
gme::engine (depends on)
│
├──► gme::motion ──► gme::gradient
│ │ │
│ └──► gme::signal └──► (no further GME deps)
│ │
│ └──► (nlohmann_json, liblo for argv types)
│
├──► gme::time ──► mtcreceiver
│
└──► gme::osc ──► liblo
Rules:
- No cycles.
gme::gradientdepends on nothing else in GME;gme::timedepends on nothing else in GME;gme::oscdepends on nothing else in GME. gme::motionis the integrator ofgradient+osc+signal. It does not depend ontimedirectly — the registry receivesmtc_msas alongfrom the engine layer.gme::enginewires everything. It is the only namespace that depends on the daemon-layerOscServer, and that dependency lives inGradientEngine.cpp(compiled into the daemon binary, not the static library).
Threading model
| Thread | Owner | Responsibility |
|---|---|---|
| Main thread | GradientEngineApplication |
Lifecycle: initialize → run (blocks on pause) → shutdown on signal. |
| RtMidi MIDI callback | mtcreceiver (transitively) |
Decodes quarter frames; fires MtcTickSource's registered callback. Lock-free, non-blocking. |
| liblo network thread | gme::daemon::comms::OscServer |
Receives UDP, dispatches to address handlers, parses, pushes to the SPSC queue. |
| (none — no worker pool) | — | Evaluation runs on the RtMidi callback thread directly. |
Cross-thread handoff is exactly the LockFreeQueue<FadeCommand, 64>:
- Producer — exactly one (the liblo thread inside OscServer).
- Consumer — exactly one site at a time (GradientEngine::onTick on the RtMidi thread). There is no 100 ms fallback drain in the current implementation; the queue is drained only on tick.
The queue is wait-free for the consumer (pop is a single load + indexed read + store) and bounded for the producer (drop-oldest on full, returns false to signal the drop).
Lifecycle
Startup
main()constructsGradientEngineApplicationon the stack.app.initialize(argc, argv)runsConfigurationManager::parseArgs, sets up the optionalCuemsLogger, installs signal handlers, constructsGradientEngine.app.run()callsGradientEngine::initialize({midiPort, oscPort, nodeName}):MtcTickSource::start(midiPort)— opens MIDI; returnsMtcStartError. On non-kOk, the engine logs and the daemon exits non-zero.OscServer::start()— binds127.0.0.1:<port>and starts the liblo thread. On bind failure, returnsfalse; the engine logs and exits.MotionRegistryis constructed with the tick source reference and the status callback.MtcTickSource::setTickCallback([this](long ms){ onTick(ms); })— registration is the last step so no tick fires against a half-built engine.app.run()blocks onpause().
Tick
For each MTC quarter frame the RtMidi thread invokes MtcTickSource::setTickCallback's registered closure → GradientEngine::onTick(mtc_ms):
- Drain
queue_: for eachFadeCommandpopped,registry_->apply(cmd). registry_->tick(mtc_ms):- For each
IMotioninmotions_, callevalAndSend(mtc_ms). - On
result.failed: incrementconsecutive_osc_failures; atkOscFailureThreshold = 5mark for removal and emitMotionError:"osc_send_failed". Otherwise reset to 0. - On
result.completed: markcompleted = true, emitMotionComplete, mark for removal. - After iterating, remove all marked motions from both
motions_andosc_index_.
Shutdown
SIGTERM / SIGINT → handler sets the loop flag → app.run() returns from pause() → app.shutdown():
GradientEngine::shutdown():- Deregister the tick callback (
setTickCallback({})). The destructor ofMtcTickSource(when the engine destructs later) blocks until any in-flight callback returns. MotionRegistry::cancelAll()— removes every motion withoutsendSnapToEnd(final OSC values are NOT sent on shutdown).OscServer::stop()— joins the liblo thread.ConfigurationManagerandCuemsLoggerdestruct.- Process exits with the code returned by
run().
Build targets
| Target | Type | Sources | Links |
|---|---|---|---|
gradient_motion |
static lib | src/{time,gradient,motion,signal,osc}/*.cpp |
nlohmann_json::nlohmann_json, liblo |
gradient-motiond |
executable | daemon/{main,GradientEngineApplication}.cpp, daemon/config/ConfigurationManager.cpp, daemon/comms/OscServer.cpp, src/engine/GradientEngine.cpp |
gradient_motion, mtcreceiver, rtmidi, liblo, pthread, optional cuemslogger |
mtcreceiver |
static lib (submodule) | mtcreceiver/*.cpp |
rtmidi, optional cuemslogger |
cuemslogger |
static lib (submodule) | cuemslogger/*.cpp |
system libs (syslog) |
test_* |
executables | tests/test_*.cpp |
gradient_motion + per-test extras |
CMake options:
BUILD_DAEMON(defaultON) — set toOFFto build only the library and tests that don't need RtMidi.ENABLE_CUEMS_LOGGER(defaultON) — set toOFFto use the stub logger.MTCRECV_TESTING(forcedONfor the daemon build) — exposesmtcreceiver's test-only helpers (invokeTickForTesting,SkipPortOpenTag).
Test layout
| Test | Scope | Notes |
|---|---|---|
test_curves |
gme::gradient |
Curve boundary postconditions, parameter defaults, LUT continuity. |
test_mtc_tick_source |
gme::time |
Uses invokeTickForTesting; no MIDI hardware required. |
test_lockfree_queue |
gme::signal::LockFreeQueue |
SPSC contract, drop-oldest, advisory size. |
test_fade_motion |
gme::motion::FadeMotion |
Evaluation, snap-to-end, inheritFrom. |
test_motion_registry |
gme::motion::MotionRegistry |
Duplicate-id rejection, supersede, OSC-failure threshold, cancel paths. |
test_motion_registry_bench |
perf | Per-tick cost at saturation (≤50 active motions). |
test_osc_parse |
gme::signal::parseFadeOscCommand |
14 cases; pure parse-level, no network. |
test_osc_server_integration |
gme::daemon::comms::OscServer |
3 real-loopback cases using liblo's client side. |
bench_osc_latency |
perf | sendFloat syscall latency on loopback. |
End-to-end deploy tests on the running daemon live in dev/deploy_tests/: s007_t034_smoke.sh, s007_t052_rate_limit.sh, s007_t063_multi_node.sh, s007_t065_avahi_resilience.sh.
Cross-repo coupling
| Repo | Coupling point | Direction |
|---|---|---|
cuems-engine (NodeEngine) |
GradientClient sends /gradient/start_fade etc. over UDP to 127.0.0.1:<gradient_osc_port>. |
engine → gradient-motiond |
cuems-utils |
settings.xsd defines <gradient_osc_port> on NodeType; ConfigurationManager reads it. |
utils → gradient-motiond (config schema) |
cuems-audioplayer |
Receives /volmaster OSC float on its configured port. |
gradient-motiond → audioplayer |
cuems-videocomposer |
Receives /videocomposer/layer/{id}/opacity OSC float. |
gradient-motiond → videocomposer |
mtcreceiver |
Git submodule, pinned at 59fc76e. v2.0.0 API contract. |
dependency |
cuemslogger |
Git submodule. Optional (ENABLE_CUEMS_LOGGER=ON). |
dependency |
cuems-common |
Ships cuems-gradient-motiond.service systemd unit. |
runtime dep (Debian package). |