-
-
Notifications
You must be signed in to change notification settings - Fork 21.5k
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
Move mixing out of the AudioStreamPlayer* nodes #51296
Conversation
439cd45
to
b13e7fd
Compare
This is the project I've been using to test: audioservertest.zip It's probably good to test this on other more complex projects too. |
b13e7fd
to
ae5572e
Compare
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 work!
I left some style nitpicks and only skimmed through the detailed code changes, but it looks pretty solid!
core/templates/safe_list.h
Outdated
// instead the node will be deallocated at a later time when it is safe to do so. | ||
// - No blocking synchronization primitives will be used. | ||
|
||
// This is used in very specific areas of the engine where it's critical that these guarantees are held |
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.
// This is used in very specific areas of the engine where it's critical that these guarantees are held | |
// This is used in very specific areas of the engine where it's critical that these guarantees are held. |
core/templates/safe_list.h
Outdated
struct SafeListNode { | ||
std::atomic<SafeListNode *> next = nullptr; | ||
|
||
// If the node is logically deleted, this pointer will typically point to the previous list item in time that was also logically deleted. |
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 don't have a line length limit but for long comments it can be worth wrapping manually:
// If the node is logically deleted, this pointer will typically point to the previous list item in time that was also logically deleted. | |
// If the node is logically deleted, this pointer will typically point | |
// to the previous list item in time that was also logically deleted. |
core/templates/safe_list.h
Outdated
// Fall through upon failure, try again. | ||
} | ||
} | ||
// Then queue it for deletion by putting it in the node graveyard. Don't touch `next` because an iterator might still be pointing at this node. |
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.
// Then queue it for deletion by putting it in the node graveyard. Don't touch `next` because an iterator might still be pointing at this node. | |
// Then queue it for deletion by putting it in the node graveyard. | |
// Don't touch `next` because an iterator might still be pointing at this node. |
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.
- a few other comments that might be worth wrapping, I'd recommend keeping line length at max 100-120 chars for comments.
core/templates/safe_list.h
Outdated
} | ||
}; | ||
|
||
#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.
I'd suggest putting a comment to remind what this #else
is about:
#else | |
#else // NO_THREADS |
scene/2d/audio_stream_player_2d.cpp
Outdated
|
||
int new_output_count = 0; | ||
//stop playing if no longer active |
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.
//stop playing if no longer active | |
// Stop playing if no longer active. |
servers/audio/audio_stream.cpp
Outdated
} else { | ||
for (int i = 0; i < p_frames; i++) { | ||
p_buffer[i] = AudioFrame(0, 0); | ||
} | ||
// TODO does this make sense? |
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.
To assess.
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 thought about it for a while, and I think it does make sense :) Removed as a todo.
servers/audio_server.cpp
Outdated
#include "servers/audio/effects/audio_effect_compressor.h" | ||
#include <cstring> |
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.
#include "servers/audio/effects/audio_effect_compressor.h" | |
#include <cstring> | |
#include "servers/audio/effects/audio_effect_compressor.h" | |
#include <cstring> |
servers/audio_server.h
Outdated
AUDIO_DATA_INVALID_ID = -1, | ||
MAX_CHANNELS_PER_BUS = 4, | ||
MAX_BUSES_PER_PLAYBACK = 6, | ||
LOOKAHEAD_BUFFER_SIZE = 32 |
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.
LOOKAHEAD_BUFFER_SIZE = 32 | |
LOOKAHEAD_BUFFER_SIZE = 32, |
Trailing comma can ensure that future additions won't generate unnecessary line diff.
servers/audio_server.h
Outdated
std::atomic<float> setseek = -1; | ||
std::atomic<float> pitch_scale = 1; | ||
std::atomic<float> highshelf_gain = 0; | ||
std::atomic<float> attenuation_filter_cutoff_hz = 0; // This isn't used unless highshelf_gain is nonzero. |
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 know we tend to use our own SafeNumeric
instead of std::atomic
(which wraps std::atomic
), but I'm not familiar with its semantics and whether it would actually make sense here. CC @RandomShaper
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.
That's right. SafeNumeric<T>
is intended to be the Godot "STL" counterpart of std::atomic
. It should fit most cases, where acquire-release semantics is the right choice.
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.
Elaborating on that, I see atomic are being used throughout this PR will the default memory ordering semantics (sequential). Even if we didn't have SafeNumeric
, it'd be good to check if acquire-release is enough, because, AFAIK, that is more performant on certain architectures.
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 think acquire-release is probably fine everywhere except in SafeList but I find it very difficult to reason about these things so I'm not sure. I do think a system-wide barrier of some sort is appropriate during insertions or removals to SafeList because they tend to be fairly rare and the penalty if something goes wrong could be a double free or a use after free. These scalar values can absolutely have weaker guarantees.
The main thing we're attempting to avoid here is a sleeping on mutex contention, right? A memory barrier is not in the same league performance-wise as sleeping the audio 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.
Apparently a compare-swap operation is about as expensive as a cache miss on x86 server platforms. I'm curious about other architectures but even if it were 100x more expensive than a cache miss I don't think I'd consider moving to another memory model for SafeList given how rarely insertions and deletions happen. I'm also hesitant to optimize without profiling results.
Side-note, thank you so much for reviewing this @RandomShaper. I know it's a difficult PR to review and I appreciate it. :)
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.
No worries! It's also hard for me to reason about every possible case. I just have the mindset of use acq-rel by default, which is already stronger than certain cases would need, but it still allows for more perfomant load/stores on some architectures than sequential would cause. When it's about compare-exchange, to be honest, I still haven't studied much about the performane consequences of using one memory ordering or the other.
In the case of SafeList
, I can't but agree that the wisest thing is to leave sequential ordering.
This said, the only thing I'd ask you to do is switch to SafeNumeric
in the other cases.
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.
Makes sense! I'll do that later today. I think I also want to get my NO_THREADS implementation of the SafeList class to a better state than it is right now. I haven't tested it so it might be broken and I don't want to check in broken code. I can ping you when everything is done if you'd like?
I'm glad I'm not the only one who finds this sort of thing difficult. Writing the SafeList class nearly broke my brain. Lock-free programming is hard. 🤣
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.
Sure, feel free to ping me.
Yeah, lockless is hell. I wouldn't dare myself to write a lockless data structure unless there's no other option, like basing it on some third party one known to be good.
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.
@RandomShaper Ready for another review. I've had lots of trouble thinking about whether or not the remaining atomic usages are okay for acquire-release. The scalar values are definitely okay to be SafeNumerics, but other than those, all remaining usages of the default atomic behavior impact control flow in non-trivial ways. If you'd like, I can switch the playback state enum over to be acquire-release, but I'd prefer to keep the bus details pointer as-is. I should be honest though, it's fear of double-frees or other weird behavior that's making me say this, not any sort of logic or reason. :)
servers/audio_server.h
Outdated
bool get_is_playback_active(Ref<AudioStreamPlayback> p_playback); | ||
float get_playback_position(Ref<AudioStreamPlayback> p_playback); | ||
bool get_is_playback_paused(Ref<AudioStreamPlayback> p_playback); |
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.
For boolean getters, we usually start directly with is_
and not with get_is_
.
3d9a87f
to
0a48adf
Compare
Made the changes Akien pointed out and rebased. I also added a preprocessor warning in the NO_THREADS implementation of SafeList because it occurred to me that this could cause some frustrating bugs in the future if nobody is aware of it. I used a pattern that I've seen elsewhere in the codebase to avoid throwing an error on MSVC toolchains. I just want it documented in the code that NO_THREADS SafeLists are untested so someone debugging can identify issues quickly. Hopefully that's fine. I also wanted to ask about whether my silly workaround in AudioStreamPlayer2D is appropriate to keep in this PR. I left it in for ease of testing but I don't think it's the correct solution for #50958. |
// Updating this ref after the list node is created breaks consistency guarantees, don't do it! | ||
Ref<AudioStreamPlayback> stream_playback; | ||
// Playback state determines the fate of a particular AudioStreamListNode during the mix step. Must be atomically replaced. | ||
std::atomic<PlaybackState> state = AWAITING_DELETION; |
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 guess this one can become a SafeNumeric
, too, with the same provision about checking that acquire-release is fine.
servers/audio_server.h
Outdated
|
||
struct CallbackItem { | ||
std::atomic<CallbackItem *> next; |
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.
In this case, since we don't have a SafePointer<T>
, using std::atomic
directly is fine. If we find many cases of atomic pointers sometime in the future, we can always introduce it.
In any case, again, it'd be good to check if acquire-release (or even something weaker) can be used intead of sequential in the operations.
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.
Mentioned above, but I'm not entirely comfortable switching over to acquire release here. I'm not convinced it would be a problem, but I can't convince myself it's safe either.
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.
Wow nevermind! These atomics can be removed. They're dead code leftover from before I factored out SafeList into its own file.
bb1b011
to
3598d30
Compare
Hello, when I play the game I've a segmentation fault at this line: https://github.com/ellenhp/godot/blob/3598d300cb43797a4f18b34d921875d060ce7de7/servers/audio_server.cpp#L1237 which was added by this PR. Any idea how to fix it?
|
I spot the issue, the following code if (playback_node->bus_details.load()->bus_active[bus_idx]) {
map[playback_node->bus_details.load()->bus[bus_idx]] = p_volumes;
} so it crashes. To fix it we should check it in this way: if (playback_node->bus_details.load()) {
if (playback_node->bus_details.load()->bus_active[bus_idx]) {
map[playback_node->bus_details.load()->bus[bus_idx]] = p_volumes;
}
} Even if this code fix the issue momentarily on my project, it still unsafe. Atomic pointers, if not under some well documented and strict conditions, are unsafe since the check is guaranteed only the moment of the first Please let me know if you need that I open an issue, I'm also available to test an eventual fix. |
Good catch. This can be easily made to be safe, it was just an oversight on my part. |
Maybe not as easily as I had hoped. Would you mind making an issue because this might need further discussion and I'd like to be able to point to something other than an enormous PR when I submit a fix. |
@AndreaCatania would you mind testing #52187? |
Fixes #25087
Fixes #21312
Implements godotengine/godot-proposals#2299
My focus here is parity with current
master
, not to add features, but it's still an enormous change. I can probably split off the individual commits into their own PRs if people would prefer that.More detail is in the proposal above, but this basically moves all mixing into the audio server. Audio streams register their playback objects with the audio server and then update the volume during their panning or spatialization steps. This eliminates an entire class of bugs in which the player node's state is updated and it can't continue a volume ramp or fade out.
This also opens the door to lots of really exciting things! Now that we can crossfade across audio buses, we can have Area2D/Area3D based bus overrides that smoothly transition to the new bus. Imagine the mouth of a cave that slowly fades reverb in, or an area of a 2d platformer level that adds a distortion to audio. Before these things would cause pops and abrupt transitions, but with this change the pops are gone and it'll be fairly easy to add smooth transitions.
Another thing we can add back is polyphony, which I think users will appreciate. An audio stream player node can simply instance multiple playbacks for a stream and send them all to the audio server which will mix them independently. That's also not included in this change but should be simple to add.
A few notes:
The NO_THREADS implementation of my SafeList data structure is probably broken, but I don't really know how to test it given that web exports aren't working in 4.0 right now, unless I'm mistaken on that.