-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enable sub quantum delay #198
Conversation
Ok I fixed the issue and added some more tests. Let me know if you are ok with the strategy I used in the graph! |
src/render/graph.rs
Outdated
// store node id to clear the node outgoing edges | ||
cycle_breakers.push(node_id); | ||
// remove nodes from mark temp after pos | ||
marked_temp.truncate(pos); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not completely sure of this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it's quite hard to reason about it. My gut feeling is that this implementation is to optimistic. It would be safer to clear out marked_temp
and also remove these nodes from marked
. Then start visiting again from the first item that was in marked_temp
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe a nice thing to have would be a unit test with really weird edge case: some cycle inside a cycle I guess, to check this behaves as expected (whatever the solution)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great to see we are nearing a solution for this issue.
Very happy to see the extensive tests for it. It will help us iterate on the solution while checking all works well.
I dropped two notes, but I do not have a solution ready at hand. Would you like me to take the work from here and spend some time on it, or will you have another go?
src/render/processor.rs
Outdated
@@ -26,6 +26,10 @@ pub struct RenderScope { | |||
/// | |||
/// Check the `examples/worklet.rs` file for example usage of this trait. | |||
pub trait AudioProcessor: Send { | |||
fn can_break_cycle(&self) -> bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand why you have put this here, but adding this to the AudioProcessor
is a big thing because it forces all our users to have a look at it when implementing custom processors. Let's try to put this somewhere else
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We cannot really use downcasting for this problem, e.g. https://stackoverflow.com/a/33687996 without adding an as_any
method to all processors, which is undesirable too.
Another solution would be to explicitly store is_cycle_breaker
in the Node
, and setting that value with a call
ConcreteBaseAudioContext::markCycleBreaker(&self, node_id: &AudioNodeId)
that passes the info to the render thread
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup I agree that's not very clean, especially as it only concerns one (weird) node, that was just the only I found :)
src/render/graph.rs
Outdated
// store node id to clear the node outgoing edges | ||
cycle_breakers.push(node_id); | ||
// remove nodes from mark temp after pos | ||
marked_temp.truncate(pos); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it's quite hard to reason about it. My gut feeling is that this implementation is to optimistic. It would be safer to clear out marked_temp
and also remove these nodes from marked
. Then start visiting again from the first item that was in marked_temp
Actually if you have ideas to iterate, please go on yes! I can try to contribute the test I spoke about but then I'm short with ideas about how to continue |
Okay I will have a try at all three things ( |
ok cool! |
instead of at runtime: before: self.nodes.get(...).unwrap().borrow_mut() after: self.nodes.get_mut(...).unwrap().get_mut() See https://doc.rust-lang.org/std/cell/struct.RefCell.html#method.get_mut
7ef0597 Nice approach, way cleaner! |
Graph ordering might depend on the insertion order of the nodes and edges, therefore, shuffle the inputs and run the tests a few times
Benchmark result: bench_ctor bench_sine bench_sine_gain bench_sine_gain_delay |
The graph test suite looks very nice! |
The test fails, so the cycle breaking algo is not correct!
It's not an ideal solution, but it works. TODO make nicer
Okay this is conceptually finished but I'm still looking for a more elegant graph ordering after the cycles are broken |
Benchmark result: bench_ctor bench_sine bench_sine_gain bench_sine_gain_delay |
edit: I have completely messed around here, restarting... I think if we have a pile of tests like: (A)
where, if I'm right, To make it even more perverse and provoke the recursive stuff, we could also add connections between (B)
or the other way around (C)
If I reason well (which I'm not sure :) In all these 3 cases, Something like that should result in both (D)
My impression is that with such test cases running consistently across several iterations, we can know if the optimistic solution is enough (i.e. truncate only is ok, which I actually I can't think why it should not but as you said it's quite hard to reason about) or if being pessimistic is required (i.e. clear delay internal connection and restart ordering the graph from the beginning, considering that creating feedback delays lines is maybe not something people do so often that is it a big issue neither :) |
and hence is in a cycle. This means the Graph does not need to do any bookkeeping - cleaner code (and possibly faster because because it takes less memory this way)
I'm adding your examples, stay tuned! |
Benchmark result: bench_ctor bench_sine bench_sine_gain bench_sine_gain_delay |
src/node/delay.rs
Outdated
let in_cycle = self.in_cycle.load(Ordering::SeqCst); | ||
|
||
let latest_frame_written = self.latest_frame_written.load(Ordering::SeqCst); | ||
let in_cycle = latest_frame_written != scope.current_frame; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not that sure this is always right when in cycle actually:
- if not in cycle the order of rendering is guaranteed, therefore we can trust this check
- but when in cycle, the order of rendering between
Reader
andWriter
is not guaranteed anymore (at least it wasn't before, did it changed somehow?), so the check should fail sometimes?
I actually didn't manage to make a test crash and I see your sort_cycle_breaker
test, but it feels a bit weird to me, I don't understand what could have change here (and I'm pretty sure I saw this behaviour in the past, that's why there was this loop in node::delay::tests::test_node_stays_alive_long_enough
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmmm right, it is quite nuanced actually and my code needs to be improved
- When not in a cycle, there is a edge from writer to reader so writer always renders first
- When in a cycle, that edge is dropped. The nature of the cycle means output of the reader feeds into the writer, hence, the reader always renders first
- However, when the user breaks the cycle manually by dropping some other connections, the order is no longer guaranteed and thus random
The last case is a bit of an edge case of course, but I could make the in_cycle property sticky (once you are in a cycle, treat as if you will always stay in that cycle) so we prevent erratic behaviour in rendering of sub-quantum delays.
Another issue I realize is when the Writer is dropped, it will stop updating the latest_frame_written
. This causes the Reader to set in_cycle
true, disallowing sub-quantum delays. If the delay is set to 100 samples this behaviour is not right becase the final 28 frames om the last Writer call should still render directly in the first 28 frames of the next Reader output.
Pfft, not simple stuff, I will iterate again
oh ok, I didn't see the sub comment in the commit message, so pessimistic we must be indeed!
Nice one! (perf is really a trap :) |
From discussion at orottier#198 Another issue I realize is when the Writer is dropped, it will stop updating the latest_frame_written. This causes the Reader to set in_cycle true, disallowing sub-quantum delays. If the delay is set to 100 samples this behaviour is not right becase the final 28 frames om the last Writer call should still render directly in the first 28 frames of the next Reader output.
Once you are in a cycle, treat as if you will always stay in that cycle, so we prevent erratic behaviour in rendering of sub-quantum delays.
Benchmark result: bench_ctor bench_sine bench_sine_gain bench_sine_gain_delay |
src/node/constant_source.rs
Outdated
@@ -186,7 +186,8 @@ impl AudioProcessor for ConstantSourceRenderer { | |||
current_time += dt; | |||
} | |||
|
|||
true | |||
// tail_time false when output has ended this quantum | |||
stop_time > next_block_time |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remembered that we had to output one channel of silence before returning false, but can't find it in the spec actually... I think the check line 165 makes no sense anymore then?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
handled in e088c95
Benchmark result: bench_ctor bench_sine bench_sine_gain bench_sine_gain_delay |
Benchmark result: bench_ctor bench_sine bench_sine_gain bench_sine_gain_delay |
Thanks for the update, I think we have done enought in this PR.
Because 2 is also part of the cycle with 4, it is muted as well |
Hey, here is a first draft on #79
Don't merge this yet, I'm really not sure it's finished but It would be nice to have your feedback on what I have done so far. As this is related to the graph, I'm a bit nervous because I'm not sure I fully understand the
visit/order
stuff :)Except from the one I mentioned the all other tests seems to be happy