Skip to content

Commit 7fd6cbb

Browse files
committed
Fix dynamic lifetime issue for nodes connected to params
Introduce AudioProcess::has_side_effects A processor with no side effect will be dropped when the control handle is gone and there are not output ports connected. This makes the cleanup of AudioParams easier and more robust. Relatest to #397 and #468
1 parent 1721282 commit 7fd6cbb

File tree

6 files changed

+60
-49
lines changed

6 files changed

+60
-49
lines changed

src/node/delay.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,10 @@ impl AudioProcessor for DelayWriter {
374374
// let the node be decommisioned if it has no input left
375375
false
376376
}
377+
378+
fn has_side_effects(&self) -> bool {
379+
true // message passing
380+
}
377381
}
378382

379383
impl DelayWriter {

src/node/destination.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,8 @@ impl AudioProcessor for DestinationRenderer {
141141

142142
true
143143
}
144+
145+
fn has_side_effects(&self) -> bool {
146+
true // speaker output
147+
}
144148
}

src/render/graph.rs

Lines changed: 36 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -89,19 +89,26 @@ impl Node {
8989
return false;
9090
}
9191

92-
// Drop when the node does not have any inputs and outputs
93-
if !self.has_inputs_connected && self.outgoing_edges.is_empty() {
94-
return true;
92+
// When the nodes has no incoming connections:
93+
if !self.has_inputs_connected {
94+
// Drop when the processor reports it won't yield output.
95+
if !tail_time {
96+
return true;
97+
}
98+
99+
// Drop when the node does not have any inputs and outputs
100+
if self.outgoing_edges.is_empty() {
101+
return true;
102+
}
95103
}
96104

97-
// Drop when the node does not have any inputs connected,
98-
// and if the processor reports it won't yield output.
99-
if !self.has_inputs_connected && !tail_time {
105+
// Node has no control handle and does have inputs connected.
106+
// Drop when the processor when it has no outpus connected and does not have side effects
107+
if !self.processor.has_side_effects() && self.outgoing_edges.is_empty() {
100108
return true;
101109
}
102110

103111
// Otherwise, do not drop the node.
104-
// (Even if it has no outputs connected, it may have side effects)
105112
false
106113
}
107114

@@ -503,31 +510,14 @@ impl Graph {
503510

504511
// Nodes are only dropped when they do not have incoming connections.
505512
// But they may have AudioParams feeding into them, these can de dropped too.
506-
self.nodes.retain(|id, node| {
507-
let node = node.get_mut(); // unwrap the RefCell
508-
513+
self.nodes.values_mut().for_each(|node| {
509514
// Check if this node was connected to the dropped node. In that case, it is
510-
// either an AudioParam (which can be dropped), or the AudioListener that feeds
511-
// into a PannerNode (which can be disconnected).
512-
let was_connected = {
513-
let outgoing_edges = &mut node.outgoing_edges;
514-
let prev_len = outgoing_edges.len();
515-
outgoing_edges.retain(|e| e.other_id != *index);
516-
outgoing_edges.len() != prev_len
517-
};
518-
519-
// Retain when
520-
// - special node (destination = id 0, listener = id 1), or
521-
// - not connected to this dropped node, or
522-
// - if the control thread still has a handle to it.
523-
let retain = id.0 < 2 || !was_connected || !node.control_handle_dropped;
524-
525-
if !retain {
526-
self.reclaim_id_channel
527-
.push(node.reclaim_id.take().unwrap());
528-
}
529-
retain
530-
})
515+
// either an AudioParam or the AudioListener that feeds into a PannerNode.
516+
// These should be disconnected
517+
node.get_mut()
518+
.outgoing_edges
519+
.retain(|e| e.other_id != *index);
520+
});
531521
}
532522
});
533523

@@ -819,7 +809,10 @@ mod tests {
819809
node_id: std::cell::Cell::new(AudioNodeId(0)),
820810
event_sender: None,
821811
};
822-
graph.render(&scope);
812+
813+
// render twice
814+
graph.render(&scope); // node is dropped
815+
graph.render(&scope); // param is dropped
823816

824817
// First the regular node should be dropped, then the audioparam
825818
assert_eq!(node_id_consumer.pop().unwrap().0, 2);
@@ -870,6 +863,11 @@ mod tests {
870863
let signal = Box::new(TestNode { tail_time: true });
871864
add_node(&mut graph, 4, signal);
872865
add_edge(&mut graph, 4, 3);
866+
// Mark the node as 'detached from the control thread', so it is allowed to drop
867+
graph
868+
.nodes
869+
.get_unchecked_mut(AudioNodeId(4))
870+
.control_handle_dropped = true;
873871

874872
// Render a single quantum
875873
let scope = AudioWorkletGlobalScope {
@@ -879,7 +877,10 @@ mod tests {
879877
node_id: std::cell::Cell::new(AudioNodeId(0)),
880878
event_sender: None,
881879
};
882-
graph.render(&scope);
880+
881+
// render twice
882+
graph.render(&scope); // node is dropped
883+
graph.render(&scope); // param is dropped
883884

884885
// First the regular node should be dropped, then the audioparam
885886
assert_eq!(node_id_consumer.pop().unwrap().0, 2);
@@ -889,7 +890,8 @@ mod tests {
889890
assert!(node_id_consumer.pop().is_none());
890891

891892
// Render again
892-
graph.render(&scope);
893+
graph.render(&scope); // param signal source is dropped
894+
assert_eq!(node_id_consumer.pop().unwrap().0, 4);
893895
}
894896

895897
#[test]

src/render/node_collection.rs

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -67,20 +67,6 @@ impl NodeCollection {
6767
self.nodes[index.0 as usize].as_mut()
6868
}
6969

70-
#[inline(always)]
71-
pub fn retain<F>(&mut self, mut f: F)
72-
where
73-
F: FnMut(AudioNodeId, &mut RefCell<Node>) -> bool,
74-
{
75-
self.nodes.iter_mut().enumerate().for_each(|(i, opt)| {
76-
if let Some(v) = opt.as_mut() {
77-
if !f(AudioNodeId(i as u64), v) {
78-
*opt = None;
79-
}
80-
}
81-
})
82-
}
83-
8470
#[track_caller]
8571
#[inline(always)]
8672
pub fn get_unchecked(&self, index: AudioNodeId) -> &RefCell<Node> {

src/render/processor.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,21 @@ pub trait AudioProcessor: Send {
141141
}
142142

143143
/// Return the name of the actual AudioProcessor type
144-
#[doc(hidden)] // not meant to be user facing
145144
fn name(&self) -> &'static str {
146145
std::any::type_name::<Self>()
147146
}
147+
148+
/// Indicates if this processor has 'side effects' other than producing output
149+
///
150+
/// Processors without side effects can not be dropped when there are no outputs connected, and
151+
/// when the control side handle no longer exists
152+
///
153+
/// Side effects could include
154+
/// - IO (e.g. speaker output of the destination node)
155+
/// - Message passing (e.g. worklet nodes)
156+
fn has_side_effects(&self) -> bool {
157+
false
158+
}
148159
}
149160

150161
impl std::fmt::Debug for dyn AudioProcessor {

src/worklet.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,10 @@ impl<P: AudioWorkletProcessor> AudioProcessor for AudioWorkletRenderer<P> {
431431
fn onmessage(&mut self, msg: &mut dyn Any) {
432432
self.processor.load().onmessage(msg)
433433
}
434+
435+
fn has_side_effects(&self) -> bool {
436+
true // could be IO, message passing, ..
437+
}
434438
}
435439

436440
#[cfg(test)]

0 commit comments

Comments
 (0)