doc: spa: Add more docs about SPA_IO_Clock and driver operations

This commit is contained in:
Carlos Rafael Giani 2025-07-14 12:33:22 +02:00
parent 67711e899c
commit eb3d14053d
4 changed files with 124 additions and 7 deletions

View file

@ -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.)
*/

View file

@ -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

View file

@ -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',

View file

@ -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.