diff --git a/doc/dox/internals/driver.dox b/doc/dox/internals/driver.dox new file mode 100644 index 000000000..68cf1dd5e --- /dev/null +++ b/doc/dox/internals/driver.dox @@ -0,0 +1,111 @@ +/** \page page_driver Driver architecture and workflow + +This document explains how drivers are structured and how they operate. +This is useful to know both for debugging and for writing new drivers. + +(For details about how the graph does scheduling, which is tied to the driver, +see \ref page_scheduling ). + +# Clocks + +A driver is a node that starts graph cycles. Typically, this is accomplished +by using a timer that periodically invokes a callback, or by an interrupt. + +Drivers use the monotonic system clock as the reference for timestamping. Note +that "monotonic system clock" does not refer to the \c MONOTONIC_RAW clock in +Linux, but rather, to the regular monotonic clock. + +Drivers may actually be run by a custom internal clock instead of the monotonic +system clock. One example would be a sound card DAC's clock. Another would be +a network adapter with a built in PHC. Or, the driver may be using a system +clock other than the monotonic system clock. The driver then needs to perform +some sort of timestamp translation and drift compensation from that internal +clock to the monotonic clock, since it still needs to generate monotonic clock +timestamps for the beginning cycle. (More on that below.) + +# Updates and graph cycle start + +Every time a driver starts a graph cycle, it must update the contents of the +\ref spa_io_clock instance that is assigned to them through the +\ref spa_node_methods::set_io callback. The fields of the struct must be +updated as follows: + +- \ref spa_io_clock::nsec : Must be set to the time (according to the monotonic + system clock) when the cycle that the driver is about to trigger started. To + minimize jitter, it is usually a good idea to increment this by a fixed amount + except for when the driver starts and when discontinuities occur in its clock. +- \ref spa_io_clock::rate : Set to a value that can translate samples to nanoseconds. +- \ref spa_io_clock::position : Current cycle position, in samples. This is the + ideal position of the graph cycle (this is explained in greater detail further below). + It is incremented by the dduration (in samples) at the beginning of each cycle. If + a discontinuity is experienced by the driver that results in a discontinuity in the + position of the old and the current cycle, consider setting the + \ref SPA_IO_CLOCK_FLAG_DISCONT flag to inform other nodes about this. +- \ref spa_io_clock::duration : Duration of this new cycle, in samples. +- \ref spa_io_clock::rate_diff : A decimal value that is set to whatever correction + factor the driver applied to for a drift between an internal driver clock and the + monotonic system clock. A value above 1.0 means that the internal driver clock + is faster than the monotonic system clock, and vice versa. Always set this to + 1.0 if the driver is directly using the monotonic clock. +- \ref spa_io_clock::next_nsec : Must be set to the time (according to the monotonic + system clock) when the cycle that comes after the current one is to be started. In + some cases, this may actually be in the past relative to nsec, for example, when + some internal driver clock experienced a discontinuity. Consider setting the + \ref SPA_IO_CLOCK_FLAG_DISCONT flag in such a case. Just like with nsec, to + minimize jitter, it is usually a good idea to increment this by a fixed amount + except for when the driver starts and when discontinuities occur in its clock. + +The driver node signals the start of the graph cycle by calling \ref spa_node_call_ready +with the \ref SPA_STATUS_HAVE_DATA and \ref SPA_STATUS_NEED_DATA flags passed +to that function call. That call must happen inside the thread that runs the +data loop assigned to the driver node. + +As mentioned above, the \ref spa_io_clock::position field is the _ideal_ position +of the graph cycle, in samples. This contrasts with \ref spa_io_clock::nsec, which +is the moment in monotonic clock time when the cycle _actually_ happens. This is +an important distinction when driver is run by a clock that is different to the monotonic +cloc. In that case, the \ref spa_io_clock::nsec timestamps are adjusted to match the pace +of that different clock (explained in the section below). In such a case, +\ref spa_io_clock::position still is incremented by the duration in samples. + +# Using clocks other than the monotonic clock + +As mentioned earlier, the driver may be run by an internal clock that is different +to the monotonic clock. If that other clock can be directly used for scheduling +graph cycle initiations, then it is sufficient to compute the offset between that +clock and the monotonic clock (that is, offset = other_clock_time - monotonic_clock_time) +at each cycle and use that offset to translate that other clock's time to the monotonic +clock time. This is accomplished by adding that offset to the \ref spa_io_clock::nsec +and \ref spa_io_clock::next_nsec fields. For example, when the driver uses the realtime +system clock instead of the monotonic system clock, then that realtime clock can still +be used with \c timerfd to schedule callback invocations within the data loop. Then, computing +the (realtime_clock_time - monotonic_clock_time) offset is sufficient, as mentioned, +to fulfill the requirements of the \ref spa_io_clock::nsec and \ref spa_io_clock::next_nsec +fields that their timestamps must be given in monotonic clock time. + +If however that other clock cannot be used for scheduling graph cycle initiations directly +(for example, because the API of that clock has no functionality to trigger callbacks), +then, in addition to the aforementioned offset, the driver has to use the monotonic clock +for triggering callbacks (usually via \c timerfd) and adjust the time when callbacks are +invoked such that they match the pace of that other clock. + +As an example (clock speed difference exaggerated for sake of clarity), suppose the other +clock is twice as fast as the monotonic clock. Then the monotonic clock timestamps have +to be calculated in a manner that halves the durations between said timestamps, and the +\ref spa_io_clock::rate_diff field is set to 2.0. + +The dummy node driver uses a DLL for this purpose. It is fed the difference between the +expected position (in samples) and the actual position (derived from the current time +of the driver's internal clock), passes the delta between these two quantities into the +DLL, and the DLL computes a correction factor (2.0 in the above example) which is used +for scaling durations between \c timerfd timeouts. This forms a control loop, since the +correction factor causes the durations between the timeouts to be adjusted such that the +difference between the expected position and the actual position reaches zero. Keep in +mind the notes above about \ref spa_io_clock::position being the ideal position of the +graph cycle, meaning that even in this case, the duration it is incremented by is +_not_ scaled by the correction factor; the duration in samples remains unchanged. + +(Other popular control loop mechanisms that are suitable alternatives to the DLL are +PID controllers and Kalman filters.) + +*/ diff --git a/doc/dox/internals/index.dox b/doc/dox/internals/index.dox index 64f548ccc..89d2e9da3 100644 --- a/doc/dox/internals/index.dox +++ b/doc/dox/internals/index.dox @@ -11,6 +11,7 @@ - \subpage page_library - \subpage page_dma_buf - \subpage page_scheduling +- \subpage page_driver - \subpage page_latency - \subpage page_tag - \subpage page_native_protocol diff --git a/doc/meson.build b/doc/meson.build index dc90e122b..7bd86bfac 100644 --- a/doc/meson.build +++ b/doc/meson.build @@ -68,6 +68,7 @@ extra_docs = [ 'dox/internals/objects.dox', 'dox/internals/audio.dox', 'dox/internals/scheduling.dox', + 'dox/internals/driver.dox', 'dox/internals/protocol.dox', 'dox/internals/pulseaudio.dox', 'dox/internals/dma-buf.dox', diff --git a/spa/include/spa/node/io.h b/spa/include/spa/node/io.h index 01983a26d..2920c1b3b 100644 --- a/spa/include/spa/node/io.h +++ b/spa/include/spa/node/io.h @@ -114,15 +114,16 @@ struct spa_io_range { * Driver nodes are supposed to update the contents of \ref SPA_IO_Clock before * signaling the start of a graph cycle. These updated clock values become * visible to other nodes in \ref SPA_IO_Position. Non-driver nodes do - * not need to update the contents of their \ref SPA_IO_Clock. + * not need to update the contents of their \ref SPA_IO_Clock. Also + * see \ref page_driver for further details. * * The host generally gives each node a separate \ref spa_io_clock in \ref * SPA_IO_Clock, so that updates made by the driver are not visible in the * contents of \ref SPA_IO_Clock of other nodes. Instead, \ref SPA_IO_Position * is used to look up the current graph time. * - * A node is a driver when \ref spa_io_clock.id in \ref SPA_IO_Clock and - * \ref spa_io_position.clock.id in \ref SPA_IO_Position are the same. + * A node is a driver when \ref spa_io_clock::id and the ID in + * \ref spa_io_position.clock in \ref SPA_IO_Position are the same. * * The flags are set by the graph driver at the start of each cycle. */ @@ -168,13 +169,16 @@ struct spa_io_clock { * can be used to check if nodes share the same clock. */ uint64_t nsec; /**< Time in nanoseconds against monotonic clock * (CLOCK_MONOTONIC). This fields reflects a real time instant - * in the past. The value may have jitter. */ + * in the past, when the current cycle started. The value may + * have jitter. */ struct spa_fraction rate; /**< Rate for position/duration/delay/xrun */ uint64_t position; /**< Current position, in samples @ \ref rate */ uint64_t duration; /**< Duration of current cycle, in samples @ \ref rate */ int64_t delay; /**< Delay between position and hardware, in samples @ \ref rate */ double rate_diff; /**< Rate difference between clock and monotonic time, as a ratio of - * clock speeds. */ + * clock speeds. A value higher than 1.0 means that the driver's + * internal clock is faster than the monotonic clock (by that + * factor), and vice versa. */ uint64_t next_nsec; /**< Estimated next wakeup time in nanoseconds. * This time is a logical start time of the next cycle, and * is not necessarily in the future. @@ -308,8 +312,8 @@ enum spa_io_position_state { * * It is set on all nodes in \ref SPA_IO_Position, and the contents of \ref * spa_io_position.clock contain the clock updates made by the driving node in - * the graph in its \ref SPA_IO_Clock. Also, \ref spa_io_position.clock.id - * will contain the clock id of the driving node in the graph. + * the graph in its \ref SPA_IO_Clock. Also, the ID in \ref spa_io_position.clock + * will be the clock id of the driving node in the graph. * * The position clock indicates the logical start time of the current graph * cycle.