PipeWire 1.5.0
|
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 Graph Scheduling ).
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 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.)
Every time a driver starts a graph cycle, it must update the contents of the spa_io_clock instance that is assigned to them through the spa_node_methods::set_io callback. The fields of the struct must be updated as follows:
The driver node signals the start of the graph cycle by calling spa_node_call_ready with the SPA_STATUS_HAVE_DATA and 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 spa_io_clock::position field is the ideal position of the graph cycle, in samples. This contrasts with 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 clock. In that case, the spa_io_clock::nsec timestamps are adjusted to match the pace of that different clock (explained in the section below). In such a case, spa_io_clock::position still is incremented by the duration in samples. This is important, since nodes and modules may use this field as an offset within their own internal ring buffers or similar structures, using the position field as an offset within said data structures. This requires the position field to advance in a continuous way. By incrementing by the duration, this requirement is met.
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 = monotonic_clock_time - other_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 spa_io_clock::nsec and 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 timerfd
to schedule callback invocations within the data loop. Then, computing the (monotonic_clock_time - realtime_clock_time) offset is sufficient, as mentioned, to be able to translate clock's time to monotonic time for spa_io_clock::nsec and spa_io_clock::next_nsec (which require monotonic clock timestamps).
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 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 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 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 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.)