Skip to content

Commit 8753217

Browse files
authored
Merge pull request #469 from orottier/bugfix/panics-in-nodecollection
Dynamic lifetime issues and the node collection
2 parents 165f770 + 85d873b commit 8753217

File tree

6 files changed

+178
-96
lines changed

6 files changed

+178
-96
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: 147 additions & 59 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 outputs 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

@@ -193,8 +200,8 @@ impl Graph {
193200
}
194201

195202
pub fn add_edge(&mut self, source: (AudioNodeId, usize), dest: (AudioNodeId, usize)) {
196-
self.nodes[source.0]
197-
.get_mut()
203+
self.nodes
204+
.get_unchecked_mut(source.0)
198205
.outgoing_edges
199206
.push(OutgoingEdge {
200207
self_index: source.1,
@@ -206,8 +213,8 @@ impl Graph {
206213
}
207214

208215
pub fn remove_edge(&mut self, source: AudioNodeId, dest: AudioNodeId) {
209-
self.nodes[source]
210-
.get_mut()
216+
self.nodes
217+
.get_unchecked_mut(source)
211218
.outgoing_edges
212219
.retain(|edge| edge.other_id != dest);
213220

@@ -216,7 +223,7 @@ impl Graph {
216223

217224
pub fn remove_edges_from(&mut self, source: AudioNodeId) {
218225
// Remove outgoing edges
219-
self.nodes[source].get_mut().outgoing_edges.clear();
226+
self.nodes.get_unchecked_mut(source).outgoing_edges.clear();
220227

221228
// Remove incoming edges - we need to traverse all nodes
222229
self.nodes.values_mut().for_each(|node| {
@@ -241,21 +248,27 @@ impl Graph {
241248
}
242249

243250
pub fn mark_cycle_breaker(&mut self, index: AudioNodeId) {
244-
self.nodes[index].get_mut().cycle_breaker = true;
251+
self.nodes.get_unchecked_mut(index).cycle_breaker = true;
245252
}
246253

247254
pub fn set_channel_count(&mut self, index: AudioNodeId, v: usize) {
248-
self.nodes[index].get_mut().channel_config.count = v;
255+
self.nodes.get_unchecked_mut(index).channel_config.count = v;
249256
}
250257
pub fn set_channel_count_mode(&mut self, index: AudioNodeId, v: ChannelCountMode) {
251-
self.nodes[index].get_mut().channel_config.count_mode = v;
258+
self.nodes
259+
.get_unchecked_mut(index)
260+
.channel_config
261+
.count_mode = v;
252262
}
253263
pub fn set_channel_interpretation(&mut self, index: AudioNodeId, v: ChannelInterpretation) {
254-
self.nodes[index].get_mut().channel_config.interpretation = v;
264+
self.nodes
265+
.get_unchecked_mut(index)
266+
.channel_config
267+
.interpretation = v;
255268
}
256269

257270
pub fn route_message(&mut self, index: AudioNodeId, msg: &mut dyn Any) {
258-
self.nodes[index].get_mut().processor.onmessage(msg);
271+
self.nodes.get_unchecked_mut(index).processor.onmessage(msg);
259272
}
260273

261274
/// Helper function for `order_nodes` - traverse node and outgoing edges
@@ -278,7 +291,7 @@ impl Graph {
278291
let cycle_breaker_node = marked_temp
279292
.iter()
280293
.skip(pos)
281-
.find(|&&node_id| self.nodes[node_id].borrow().cycle_breaker);
294+
.find(|&&node_id| self.nodes.get_unchecked(node_id).borrow().cycle_breaker);
282295

283296
match cycle_breaker_node {
284297
Some(&node_id) => {
@@ -307,7 +320,13 @@ impl Graph {
307320
marked_temp.push(node_id);
308321

309322
// Visit outgoing nodes, and call `visit` on them recursively
310-
for edge in self.nodes[node_id].borrow().outgoing_edges.iter() {
323+
for edge in self
324+
.nodes
325+
.get_unchecked(node_id)
326+
.borrow()
327+
.outgoing_edges
328+
.iter()
329+
{
311330
let cycle_breaker_applied = self.visit(
312331
edge.other_id,
313332
marked,
@@ -386,9 +405,11 @@ impl Graph {
386405

387406
if cycle_breaker_applied {
388407
// clear the outgoing edges of the nodes that have been recognized as cycle breaker
389-
let nodes = &mut self.nodes;
390408
cycle_breakers.iter().for_each(|node_id| {
391-
nodes[*node_id].get_mut().outgoing_edges.clear();
409+
self.nodes
410+
.get_unchecked_mut(*node_id)
411+
.outgoing_edges
412+
.clear();
392413
});
393414

394415
continue;
@@ -423,16 +444,13 @@ impl Graph {
423444
// keep track of end-of-lifecyle nodes
424445
let mut nodes_dropped = false;
425446

426-
// for borrow-checker reasons, move mutable borrow of nodes out of self
427-
let nodes = &mut self.nodes;
428-
429447
// process every node, in topological sorted order
430448
self.ordered.iter().for_each(|index| {
431449
// acquire a mutable borrow of the current processing node
432-
let mut node = nodes[*index].borrow_mut();
450+
let mut node = self.nodes.get_unchecked(*index).borrow_mut();
433451

434452
// let the current node process (catch any panics that may occur)
435-
let params = AudioParamValues::from(nodes);
453+
let params = AudioParamValues::from(&self.nodes);
436454
scope.node_id.set(*index);
437455
let (success, tail_time) = {
438456
// We are abusing AssertUnwindSafe here, we cannot guarantee it upholds.
@@ -456,7 +474,7 @@ impl Graph {
456474
// audio params are connected to the 'hidden' usize::MAX output, ignore them here
457475
.filter(|edge| edge.other_index != usize::MAX)
458476
.for_each(|edge| {
459-
let mut output_node = nodes[edge.other_id].borrow_mut();
477+
let mut output_node = self.nodes.get_unchecked(edge.other_id).borrow_mut();
460478
output_node.has_inputs_connected = true;
461479
let signal = &node.outputs[edge.self_index];
462480
let channel_config = &output_node.channel_config.clone();
@@ -482,7 +500,7 @@ impl Graph {
482500
// Check if we can decommission this node (end of life)
483501
if can_free {
484502
// Node is dropped, remove it from the node list
485-
let mut node = nodes.remove(*index).into_inner();
503+
let mut node = self.nodes.remove(*index).into_inner();
486504
self.reclaim_id_channel
487505
.push(node.reclaim_id.take().unwrap());
488506
drop(node);
@@ -492,39 +510,22 @@ impl Graph {
492510

493511
// Nodes are only dropped when they do not have incoming connections.
494512
// But they may have AudioParams feeding into them, these can de dropped too.
495-
nodes.retain(|id, node| {
496-
let node = node.get_mut(); // unwrap the RefCell
497-
513+
self.nodes.values_mut().for_each(|node| {
498514
// Check if this node was connected to the dropped node. In that case, it is
499-
// either an AudioParam (which can be dropped), or the AudioListener that feeds
500-
// into a PannerNode (which can be disconnected).
501-
let was_connected = {
502-
let outgoing_edges = &mut node.outgoing_edges;
503-
let prev_len = outgoing_edges.len();
504-
outgoing_edges.retain(|e| e.other_id != *index);
505-
outgoing_edges.len() != prev_len
506-
};
507-
508-
// Retain when
509-
// - special node (destination = id 0, listener = id 1), or
510-
// - not connected to this dropped node, or
511-
// - if the control thread still has a handle to it.
512-
let retain = id.0 < 2 || !was_connected || !node.control_handle_dropped;
513-
514-
if !retain {
515-
self.reclaim_id_channel
516-
.push(node.reclaim_id.take().unwrap());
517-
}
518-
retain
519-
})
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+
});
520521
}
521522
});
522523

523524
// If there were any nodes decommissioned, remove from graph order
524525
if nodes_dropped {
525526
let mut i = 0;
526527
while i < self.ordered.len() {
527-
if nodes.get(self.ordered[i]).is_none() {
528+
if !self.nodes.contains(self.ordered[i]) {
528529
self.ordered.remove(i);
529530
} else {
530531
i += 1;
@@ -533,7 +534,7 @@ impl Graph {
533534
}
534535

535536
// Return the output buffer of destination node
536-
&self.nodes[AudioNodeId(0)].get_mut().outputs[0]
537+
&self.nodes.get_unchecked_mut(AudioNodeId(0)).outputs[0]
537538
}
538539
}
539540

@@ -735,7 +736,10 @@ mod tests {
735736
// dropped and the AudioNodeId(3) should be reclaimed
736737
add_node(&mut graph, 2, node.clone());
737738
// Mark the node as 'detached from the control thread', so it is allowed to drop
738-
graph.nodes[AudioNodeId(2)].get_mut().control_handle_dropped = true;
739+
graph
740+
.nodes
741+
.get_unchecked_mut(AudioNodeId(2))
742+
.control_handle_dropped = true;
739743

740744
// Connect the regular node to the AudioDestinationNode
741745
add_edge(&mut graph, 2, 0);
@@ -777,7 +781,10 @@ mod tests {
777781
// dropped and the AudioNodeId(3) should be reclaimed
778782
add_node(&mut graph, 2, node.clone());
779783
// Mark the node as 'detached from the control thread', so it is allowed to drop
780-
graph.nodes[AudioNodeId(2)].get_mut().control_handle_dropped = true;
784+
graph
785+
.nodes
786+
.get_unchecked_mut(AudioNodeId(2))
787+
.control_handle_dropped = true;
781788

782789
// Connect the regular node to the AudioDestinationNode
783790
add_edge(&mut graph, 2, 0);
@@ -786,7 +793,10 @@ mod tests {
786793
let param = Box::new(TestNode { tail_time: true }); // audio params have tail time true
787794
add_node(&mut graph, 3, param);
788795
// Mark the node as 'detached from the control thread', so it is allowed to drop
789-
graph.nodes[AudioNodeId(3)].get_mut().control_handle_dropped = true;
796+
graph
797+
.nodes
798+
.get_unchecked_mut(AudioNodeId(3))
799+
.control_handle_dropped = true;
790800

791801
// Connect the audioparam to the regular node
792802
add_audioparam(&mut graph, 3, 2);
@@ -799,7 +809,10 @@ mod tests {
799809
node_id: std::cell::Cell::new(AudioNodeId(0)),
800810
event_sender: None,
801811
};
802-
graph.render(&scope);
812+
813+
// render twice
814+
graph.render(&scope); // node is dropped
815+
graph.render(&scope); // param is dropped
803816

804817
// First the regular node should be dropped, then the audioparam
805818
assert_eq!(node_id_consumer.pop().unwrap().0, 2);
@@ -809,6 +822,78 @@ mod tests {
809822
assert!(node_id_consumer.pop().is_none());
810823
}
811824

825+
#[test]
826+
fn test_audio_param_with_signal_lifecycle() {
827+
let (node_id_producer, mut node_id_consumer) = llq::Queue::new().split();
828+
let mut graph = Graph::new(node_id_producer);
829+
830+
let node = Box::new(TestNode { tail_time: false });
831+
832+
// Destination Node is always node id 0, and should never drop
833+
add_node(&mut graph, 0, node.clone());
834+
835+
// AudioListener Node is always node id 1, and should never drop
836+
add_node(&mut graph, 1, node.clone());
837+
838+
// Add a regular node at id 3, it has tail time false so after rendering it should be
839+
// dropped and the AudioNodeId(3) should be reclaimed
840+
add_node(&mut graph, 2, node.clone());
841+
// Mark the node as 'detached from the control thread', so it is allowed to drop
842+
graph
843+
.nodes
844+
.get_unchecked_mut(AudioNodeId(2))
845+
.control_handle_dropped = true;
846+
847+
// Connect the regular node to the AudioDestinationNode
848+
add_edge(&mut graph, 2, 0);
849+
850+
// Add an AudioParam at id 4, it should be dropped alongside the regular node
851+
let param = Box::new(TestNode { tail_time: true }); // audio params have tail time true
852+
add_node(&mut graph, 3, param);
853+
// Mark the node as 'detached from the control thread', so it is allowed to drop
854+
graph
855+
.nodes
856+
.get_unchecked_mut(AudioNodeId(3))
857+
.control_handle_dropped = true;
858+
859+
// Connect the audioparam to the regular node
860+
add_audioparam(&mut graph, 3, 2);
861+
862+
// Add a source node to feed into the AudioParam
863+
let signal = Box::new(TestNode { tail_time: true });
864+
add_node(&mut graph, 4, signal);
865+
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;
871+
872+
// Render a single quantum
873+
let scope = AudioWorkletGlobalScope {
874+
current_frame: 0,
875+
current_time: 0.,
876+
sample_rate: 48000.,
877+
node_id: std::cell::Cell::new(AudioNodeId(0)),
878+
event_sender: None,
879+
};
880+
881+
// render twice
882+
graph.render(&scope); // node is dropped
883+
graph.render(&scope); // param is dropped
884+
885+
// First the regular node should be dropped, then the audioparam
886+
assert_eq!(node_id_consumer.pop().unwrap().0, 2);
887+
assert_eq!(node_id_consumer.pop().unwrap().0, 3);
888+
889+
// No other dropped nodes
890+
assert!(node_id_consumer.pop().is_none());
891+
892+
// Render again
893+
graph.render(&scope); // param signal source is dropped
894+
assert_eq!(node_id_consumer.pop().unwrap().0, 4);
895+
}
896+
812897
#[test]
813898
fn test_release_orphaned_source_nodes() {
814899
let (node_id_producer, mut node_id_consumer) = llq::Queue::new().split();
@@ -828,7 +913,10 @@ mod tests {
828913
add_node(&mut graph, 2, node);
829914

830915
// Mark the node as 'detached from the control thread', so it is allowed to drop
831-
graph.nodes[AudioNodeId(2)].get_mut().control_handle_dropped = true;
916+
graph
917+
.nodes
918+
.get_unchecked_mut(AudioNodeId(2))
919+
.control_handle_dropped = true;
832920

833921
// Render a single quantum
834922
let scope = AudioWorkletGlobalScope {

0 commit comments

Comments
 (0)