mirror of
https://gitlab.freedesktop.org/pipewire/pipewire.git
synced 2025-10-29 05:40:27 -04:00
470 lines
17 KiB
Text
470 lines
17 KiB
Text
/** \page page_scheduling Graph Scheduling
|
|
|
|
This document tries to explain how the PipeWire graph is scheduled.
|
|
|
|
Graphs are constructed from linked nodes together with their ports. This
|
|
results in a dependency graph between nodes. Special care is taken for
|
|
loopback links so that the graph remains a directed graph.
|
|
|
|
# Processing threads
|
|
|
|
The server (and clients) has two processing threads:
|
|
|
|
- A main thread that will do all IPC with clients and server and configure the
|
|
nodes in the graph for processing.
|
|
- One (or more) data processing threads that only do the data processing.
|
|
|
|
|
|
The data processing threads are given realtime priority and are designed to
|
|
run with as little overhead as possible. All of the node resources such as
|
|
buffers, I/O areas and metadata will be set up in shared memory before the
|
|
node is scheduled to run.
|
|
|
|
This document describes the processing that happens in the data processing
|
|
thread after the main thread has configured it.
|
|
|
|
# Nodes
|
|
|
|
Nodes are objects with 0 or more input and output ports.
|
|
|
|
Each node also has:
|
|
|
|
- an eventfd to signal the node that it can start processing
|
|
- an activation record that lives in shared memory with memfd.
|
|
|
|
```
|
|
eventfd
|
|
+-^---------+
|
|
| |
|
|
in out
|
|
| |
|
|
+-v---------+
|
|
activation {
|
|
status:OK, // bitmask of NEED_DATA, HAVE_DATA or OK
|
|
pending:0, // number of unsatisfied dependencies needed to be able to run
|
|
required:0 // number of dependencies with other nodes
|
|
}
|
|
```
|
|
|
|
The activation record has the following information:
|
|
|
|
- processing state and pending dependencies. As long as there are pending dependencies
|
|
the node cannot be processed. This is the only relevant information for actually
|
|
scheduling the graph and is shown in the above illustration.
|
|
- Current status of the node and profiling info (TRIGGERED, AWAKE, FINISHED, timestamps
|
|
when the node changed state).
|
|
- Timing information, mostly for drivers when the processing started, the time, duration
|
|
and rate (quantum) etc..
|
|
- Information about repositions (seek) and timebase owners.
|
|
|
|
|
|
# Links
|
|
|
|
When two nodes are linked together, the output node becomes a dependency for the input
|
|
node. This means the input node can only start processing when the output node is finished.
|
|
|
|
This dependency is reflected in the required counter in the activation record. In below
|
|
illustration, B's required field is incremented with 1. The pending field is set to the
|
|
required field when the graph is started. Node A will keep a list of all targets (B) that it
|
|
is a dependency of.
|
|
|
|
This dependency update is only performed when the link is ready (negotiated) and the nodes
|
|
are ready to schedule (runnable).
|
|
|
|
|
|
```
|
|
eventfd eventfd
|
|
+-^---------+ +-^---------+
|
|
| | link | |
|
|
in A out ---------------------> in B out
|
|
| | | |
|
|
+-v---------+ +-v---------+
|
|
activation { target activation {
|
|
status:OK, --------------------> status:OK,
|
|
pending:0, pending:1,
|
|
required:0 required:1
|
|
} }
|
|
```
|
|
|
|
Multiple links between A and B will only result in 1 target link between A and B.
|
|
|
|
|
|
# Drivers
|
|
|
|
The graph can only run if there is a driver node that is in some way linked to an
|
|
active node.
|
|
|
|
The driver is special because it will have to initiate the processing in the graph. It
|
|
will use a timer or some sort of interrupt from hardware to start the cycle.
|
|
|
|
Any node can also be a candidate for a driver (when the node.driver property is true).
|
|
PipeWire will select the node with the highest priority.driver property as the driver.
|
|
|
|
Nodes will be assigned to the driver node they will be scheduled with. Each node holds
|
|
a reference to the driver and increments the required field of the driver.
|
|
|
|
When a node is ready to be scheduled, the driver adds the node to its list of targets
|
|
and increments the required field.
|
|
|
|
|
|
```
|
|
eventfd eventfd
|
|
+-^---------+ +-^---------+
|
|
| | link | |
|
|
in A out ---------------------> in B out
|
|
| | | |
|
|
+-v---------+ +-v---------+
|
|
activation { target activation {
|
|
status:OK, --------------------> status:OK,
|
|
pending:0, pending:0,
|
|
required:1 required:2
|
|
} }
|
|
| ^ ^
|
|
| | / /
|
|
| | / /
|
|
| | / /
|
|
| | / /
|
|
| | / /
|
|
v | /-------------/ /
|
|
activation { /
|
|
status:OK, V---------------/
|
|
pending:0,
|
|
required:2
|
|
}
|
|
+-^---------+
|
|
| |
|
|
| driver |
|
|
| |
|
|
+-v---------+
|
|
eventfd
|
|
```
|
|
|
|
As seen in the illustration above, the driver holds a link to each node it needs to
|
|
schedule and each node holds a link to the driver. Some nodes hold a link to other
|
|
nodes.
|
|
|
|
It is possible that the driver is the same as a node in the graph (for example node A)
|
|
but conceptually, the links above are still valid.
|
|
|
|
The driver will then start processing the graph by emitting the ready signal. PipeWire
|
|
will then:
|
|
|
|
- Check the previous cycle. Did it complete? Mark xrun on unfinished nodes.
|
|
- Perform reposition requests if any, timebase changes, etc..
|
|
- The pending counter of each follower node is set to the required field.
|
|
- Update the cycle counter in the driver activation io.
|
|
- It then loops over all targets of the driver and atomically decrements the required
|
|
field of the activation record. When the required field is 0, the eventfd is signaled
|
|
and the node can be scheduled.
|
|
|
|
In our example above, nodes A and B will have their pending state decremented. Node A
|
|
will be 0 and will be triggered first (node B has 2 pending dependencies to start with and
|
|
will not be triggered yet). The driver itself also has 2 dependencies left and will not
|
|
be triggered (completed) yet.
|
|
|
|
## Scheduling node A
|
|
|
|
When the eventfd is signaled on a node, we say the node is triggered and it will be able
|
|
to process data. It consumes the input on the input ports and produces more data on the
|
|
output ports.
|
|
|
|
After processing, node A goes through the list of targets and decrements each pending
|
|
field (node A has a reference to B and the driver).
|
|
|
|
In our above example, the driver is decremented (from 2 to 1) but is not yet triggered.
|
|
Node B is decremented (from 1 to 0) and is triggered by writing to the eventfd.
|
|
|
|
## Scheduling node B
|
|
|
|
Node B is scheduled and processes the input from node A. It then goes through the list of
|
|
targets and decrements the pending fields. It decrements the pending field of the
|
|
driver (from 1 to 0) and triggers the driver.
|
|
|
|
## Scheduling the driver
|
|
|
|
The graph always completes after the driver is triggered and scheduled. All required
|
|
fields from all the nodes in the target list of the driver are now 0.
|
|
|
|
The driver calculates some stats about CPU time etc.
|
|
|
|
# Async scheduling
|
|
|
|
When a node has the node.async property set to true, it will be considered an async
|
|
node and will be scheduled differently.
|
|
|
|
Async nodes don't increment the pending counter of their peers and the upstream peers
|
|
also don't increment the async node pending counters. Only the driver increments the
|
|
pending counter to the async node.
|
|
|
|
This means that the async nodes do not depend on any other node and also are not a
|
|
dependency for other nodes. This also means that the async nodes can be scheduled as
|
|
soon as the driver has started the graph.
|
|
|
|
The completion of the async node does not influence the completion of the graph in
|
|
any way and async nodes are therefore interesting when real-time performance can not
|
|
be guaranteed, for example when the processing threads are not running with a real-time
|
|
priority.
|
|
|
|
A link between a port of an async node and another port (async or not) is called an
|
|
async link and will have the link.async=true property.
|
|
|
|
Because async nodes then run concurrently with other nodes, a method must be in place
|
|
to avoid concurrent access to buffer data. This is done by sending a spa_io_async_buffers
|
|
I/O to the (mixer) ports of an async link. The spa_io_async_buffers has 2 spa_io_buffer
|
|
slots.
|
|
|
|
The driver will increment a cycle counter for each cycle that it starts. Output ports
|
|
will write to the spa_io_async_buffers (cycle+1)&1 slot and input ports will read from
|
|
(cycle&1) slots. This way the async node will always consume the output of the previous
|
|
cycle and will provide data for the next cycle. They will therefore always add 1 cycle
|
|
of latency in the graph.
|
|
|
|
A special exception is made for the output ports of the driver node. When the driver is
|
|
started, the output port buffers are copied to the previous cycle spa_io_buffer slot.
|
|
This way, the async nodes will immediately pick up the new data from the driver source.
|
|
|
|
Because there are 2 buffers in flight on the spa_io_async_buffers I/O area, the link needs
|
|
to negotiate at least 2 buffers for this to work.
|
|
|
|
|
|
## Example
|
|
|
|
A, B, C are async nodes and have async links between their ports. The async
|
|
link has the spa_io_async_buffers with 2 slots (named 0 and 1) below. All the
|
|
slots are empty.
|
|
|
|
```
|
|
+--------+ +-------+ +-------+
|
|
| A | | B | | C |
|
|
| 0 -( )-> 0 0 -( )-> 0 |
|
|
| 1 ( ) 1 1 ( ) 1 |
|
|
+--------+ +-------+ +-------+
|
|
```
|
|
|
|
cycle 0: A produces a buffer AB0 on the output port in the (cycle+1)&1 slot (1).
|
|
B consumes slot cycle&1 (0) with the empty buffer and produces BC0 in slot 1
|
|
C consumes slot cycle&1 (0) with the empty buffer
|
|
|
|
```
|
|
+--------+ +-------+ +-------+
|
|
| A | | B | | C |
|
|
| (AB0) 0 -( )-> 0 ( ) 0 -( )-> 0 ( ) |
|
|
| 1 (AB0) 1 1 (BC0) 1 |
|
|
+--------+ +-------+ +-------+
|
|
```
|
|
|
|
cycle 1: A produces a buffer AB1 on the output port in the (cycle+1)&1 slot (0).
|
|
B consumes slot cycle&1 (1) with buffer AB0 and produces BC1 in slot 0
|
|
C consumes slot cycle&1 (1) with buffer BC0
|
|
|
|
```
|
|
+--------+ +-------+ +-------+
|
|
| A | | B | | C |
|
|
| (AB1) 0 -(AB1)-> 0 (AB0) 0 -(BC1)-> 0 (BC0) |
|
|
| 1 (AB0) 1 1 (BC0) 1 |
|
|
+--------+ +-------+ +-------+
|
|
```
|
|
|
|
cycle 2: A produces a buffer AB2 on the output port in the (cycle+1)&1 slot (1).
|
|
B consumes slot cycle&1 (0) with buffer AB1 and produces BC2 in slot 1
|
|
C consumes slot cycle&1 (0) with buffer BC1
|
|
|
|
```
|
|
+--------+ +-------+ +-------+
|
|
| A | | B | | C |
|
|
| (AB2) 0 -(AB1)-> 0 (AB1) 0 -(BC1)-> 0 (BC1) |
|
|
| 1 (AB2) 1 1 (BC2) 1 |
|
|
+--------+ +-------+ +-------+
|
|
```
|
|
|
|
Each async link adds 1 cycle of latency to the chain. Notice how AB0 from cycle 0,
|
|
produces BC1 in cycle 1, which arrives in node C at cycle 2.
|
|
|
|
## Latency reporting
|
|
|
|
Because the latency is really introduced by the links, the additional cycle of
|
|
latency is added when the SPA_PARAM_Latency is copied between the output and
|
|
input ports of a link.
|
|
|
|
It is possible for a sync node A to be linked to another sync node D and an
|
|
async node B:
|
|
|
|
```
|
|
+--------+ +-------+
|
|
| A | | B |
|
|
| (AB1) 0 -(AB1)-> 0 (AB0) 0 ...
|
|
| 1 \(AB0) 1 1
|
|
+--------+ \ +-------+
|
|
\
|
|
\ +-------+
|
|
\ | D |
|
|
-(AB1)-> 0 (AB1) |
|
|
| |
|
|
+-------+
|
|
```
|
|
|
|
The output latency on A's output port is what A reports. When it is copied to the
|
|
input port of B, 1 cycle is added and when it is copied to D, nothing is added.
|
|
|
|
|
|
# Remote nodes
|
|
|
|
For remote nodes, the eventfd and the activation are transferred from the server
|
|
to the client.
|
|
|
|
This means that writing to the remote client eventfd will wake the client directly
|
|
without going to the server first.
|
|
|
|
All remote clients also get the activation and eventfd of the peer and driver they
|
|
are linked to and can directly trigger peers and drivers without going to the
|
|
server first.
|
|
|
|
## Remote driver nodes
|
|
|
|
Remote drivers start the graph cycle directly without going to the server first.
|
|
|
|
After they complete (and only when the profiler is active), they will trigger an
|
|
extra eventfd to signal the server that the graph completed. This is used by the
|
|
server to generate the profiler info.
|
|
|
|
|
|
# Lazy scheduling
|
|
|
|
Normally, a driver will wake up the graph and all the followers need to process
|
|
the data in sync. There are cases where:
|
|
|
|
1. the follower might not be ready to process the data
|
|
2. the driver rate is not ideal, the follower rate is better
|
|
3. the driver might not know when new data is available in the follower and
|
|
might wake up the graph too often.
|
|
|
|
In these cases, the driver and follower roles need to be reversed and a mechanism
|
|
needs to be provided so that the follower can know when it is worth processing the
|
|
graph.
|
|
|
|
For notifying when the graph is ready to be processed, (non driver) nodes can send
|
|
a RequestProcess event which will arrive as a RequestProcess command in the driver.
|
|
The driver can then decide to run the graph or not.
|
|
|
|
When the graph is started or partially controlled by RequestProcess events and
|
|
commands we say we have lazy scheduling. The driver is not always scheduling according
|
|
to its own rhythm but also depending on the follower.
|
|
|
|
We cannot just enable lazy scheduling when no follower will emit RequestProcess events
|
|
or when no driver will listen for RequestProcess commands. Two new node properties are
|
|
defined:
|
|
|
|
- node.supports-lazy = 0 | 1 | ...
|
|
|
|
0 means lazy scheduling as a driver is not supported
|
|
>1 means lazy scheduling as a driver is supported with increasing preference
|
|
|
|
- node.supports-request
|
|
|
|
0 means request events as a follower are not supported
|
|
>1 means request events as a follower are supported with increasing preference
|
|
|
|
We can only enable lazy scheduling when both the driver and (at least one) follower
|
|
have the node.supports-lazy and node.supports-request properties respectively.
|
|
|
|
Nodes can end up as a driver (is_driver()) and lazy scheduling can be enabled (is_lazy()),
|
|
which results in the following cases:
|
|
|
|
driver producer
|
|
-> node.driver = true
|
|
-> is_driving() && !is_lazy()
|
|
-> calls trigger_process() to start the graph
|
|
|
|
lazy producer
|
|
-> node.driver = true
|
|
-> node.supports-lazy = 1
|
|
-> is_driving() && is_lazy()
|
|
-> listens for RequestProcess and calls trigger_process() to start the graph
|
|
|
|
requesting producer
|
|
-> node.supports-request = 1
|
|
-> !is_driving() && is_lazy()
|
|
-> emits RequestProcess to suggest starting the graph
|
|
|
|
follower producer
|
|
-> !is_driving() && !is_lazy()
|
|
|
|
|
|
driver consumer
|
|
-> node.driver = true
|
|
-> is_driving() && !is_lazy()
|
|
-> calls trigger_process() to start the graph
|
|
|
|
lazy consumer
|
|
-> node.driver = true
|
|
-> node.supports-lazy = 1
|
|
-> is_driving() && is_lazy()
|
|
-> listens for RequestProcess and calls trigger_process() to start the graph
|
|
|
|
requesting consumer
|
|
-> node.supports-request = 1
|
|
-> !is_driving() && is_lazy()
|
|
-> emits RequestProcess to suggest starting the graph
|
|
|
|
follower consumer
|
|
-> !is_driving() && !is_lazy()
|
|
|
|
|
|
Some use cases:
|
|
|
|
1. Screensharing - driver producer, follower consumer
|
|
- The producer starts the graph when a new frame is available.
|
|
- The consumer consumes the new frames.
|
|
-> throttles to the rate of the producer and idles when no frames
|
|
are available.
|
|
|
|
producer
|
|
- node.driver = true
|
|
|
|
consumer
|
|
- node.driver = false
|
|
|
|
-> producer selected as driver, consumer is a simple follower.
|
|
lazy scheduling inactive (no lazy driver or no request follower)
|
|
|
|
|
|
2. headless server - requesting producer, (semi) lazy driver consumer
|
|
|
|
- The producer emits RequestProcess when new frames are available.
|
|
- The consumer requests new frames from the producer according to its
|
|
refresh rate when there are RequestProcess commands.
|
|
-> this throttles the framerate to the consumer but idles when there is
|
|
no activity on the producer.
|
|
|
|
producer
|
|
- node.driver = true
|
|
- node.supports-request = 1
|
|
|
|
consumer
|
|
- node.driver = true
|
|
- node.supports-lazy = 2
|
|
|
|
-> consumer is selected as driver (lazy > request)
|
|
lazy scheduling active (1 lazy driver and at least 1 request follower)
|
|
|
|
|
|
3. frame encoder - lazy driver producer, requesting follower consumer
|
|
|
|
- The consumer pulls a frame when it is ready to encode the next one.
|
|
- The producer produces the next frame on demand.
|
|
-> throttles the speed to the consumer without idle.
|
|
|
|
producer
|
|
- node.driver = true
|
|
- node.supports-lazy = 1
|
|
|
|
consumer
|
|
- node.driver = true
|
|
- node.supports-request = 1
|
|
|
|
-> producer is selected as driver (lazy <= request)
|
|
lazy scheduling active (1 lazy driver and at least 1 request follower)
|
|
|
|
|
|
|
|
*/
|