bugfix(milesaudiomanager): Prevent dangling pointer to AudioEventRTS in PlayingAudio when handing it over to a new AudioRequest after a call to MilesAudioManager::startNextLoop()#2774
Conversation
|
| Filename | Overview |
|---|---|
| Core/GameEngine/Include/Common/AudioEventRTS.h | DynamicAudioEventRTS now directly inherits AudioEventRTS (removing the m_event wrapper) and RefCountClass; Delete_This() correctly routes deallocation through the memory pool. |
| Core/GameEngine/Include/Common/AudioRequest.h | Replaces the raw AudioEventRTS*/AudioHandle union with separate RefCountPtr m_pendingEvent and AudioHandle m_handleToInteractOn fields; adds an explicit constructor that zero-initialises both. |
| Core/GameEngineDevice/Source/MilesAudioDevice/MilesAudioManager.cpp | Core fix: PlayingAudio::m_audioEventRTS is now RefCountPtr; startNextLoop defers request creation to the main thread via m_rerequestOnNextUpdate; killAudioEventImmediately correctly uses getPlayingHandle() and stopPlayingAudio CAS to PS_Stopped blocks spurious rerequests. |
| Core/GameEngineDevice/Include/MilesAudioDevice/MilesAudioManager.h | Replaces m_cleanupAudioEventRTS with volatile Bool m_rerequestOnNextUpdate; adds rerequestPlayingAudio / rerequestPlayingAudioWhenSignalled declarations; PlayingAudio::m_audioEventRTS becomes RefCountPtr. |
| Core/GameEngine/Source/Common/Audio/GameAudio.cpp | addAudioEvent allocates DynamicAudioEventRTS via newInstance and wraps in RefCountPtr::Create_NoAddRef; removes manual releaseAudioEventRTS calls; properly handles both music and sound paths with RAII cleanup. |
| GeneralsMD/Code/GameEngine/Include/Common/ThingTemplate.h | AudioArray members converted from raw DynamicAudioEventRTS* to RefCountPtr; constructor/destructor simplified to defaults; copy-ctor/assign correctly deep-copy the underlying objects. |
| GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp | m_ambientSound converted to RefCountPtr; all manual deleteInstance/nullptr pairs replaced with .Clear(); direct m_event. member accesses removed since DynamicAudioEventRTS IS-A AudioEventRTS now. |
| Core/Libraries/Source/WWVegas/WWLib/ref_ptr.h | Minor fix: corrects include path from wwdebug.h to WWDebug/wwdebug.h; no semantic changes to the smart pointer implementation. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Main as Main Thread (processPlayingList)
participant Timer as MSS Timer Thread (notifyOfAudioCompletion)
participant PA as PlayingAudio
participant Pool as AudioRequest Pool
Note over Timer,PA: Audio loop ends - startNextLoop called on timer thread
Timer->>PA: hasMoreLoops()?
alt "has delay > 1 logic frame"
Timer->>PA: "m_rerequestOnNextUpdate = true"
Timer->>PA: InterlockedCAS(status, PS_Stopping, PS_Playing)
Note over Timer: return (no AudioRequest created yet)
Main->>PA: sees PS_Stopping
Main->>PA: rerequestPlayingAudioWhenSignalled()
PA-->>Pool: generateFilename() + allocate AudioRequest with shared ref
Main->>PA: stopPlayingAudio() to PS_Stopped + releaseMilesHandles()
else no delay
Timer->>PA: generateFilename()
Timer->>PA: playSample / playSample3D (same RefCountPtr)
end
Note over Main: killAudioEventImmediately path
Main->>Pool: scan m_audioRequests via getPlayingHandle() match
alt audio still in PS_Stopping list
Main->>PA: stopPlayingAudio() CAS PS_Stopping to PS_Stopped
Note over Main: processPlayingList sees PS_Stopped, skips rerequestPlayingAudioWhenSignalled
end
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Main as Main Thread (processPlayingList)
participant Timer as MSS Timer Thread (notifyOfAudioCompletion)
participant PA as PlayingAudio
participant Pool as AudioRequest Pool
Note over Timer,PA: Audio loop ends - startNextLoop called on timer thread
Timer->>PA: hasMoreLoops()?
alt "has delay > 1 logic frame"
Timer->>PA: "m_rerequestOnNextUpdate = true"
Timer->>PA: InterlockedCAS(status, PS_Stopping, PS_Playing)
Note over Timer: return (no AudioRequest created yet)
Main->>PA: sees PS_Stopping
Main->>PA: rerequestPlayingAudioWhenSignalled()
PA-->>Pool: generateFilename() + allocate AudioRequest with shared ref
Main->>PA: stopPlayingAudio() to PS_Stopped + releaseMilesHandles()
else no delay
Timer->>PA: generateFilename()
Timer->>PA: playSample / playSample3D (same RefCountPtr)
end
Note over Main: killAudioEventImmediately path
Main->>Pool: scan m_audioRequests via getPlayingHandle() match
alt audio still in PS_Stopping list
Main->>PA: stopPlayingAudio() CAS PS_Stopping to PS_Stopped
Note over Main: processPlayingList sees PS_Stopped, skips rerequestPlayingAudioWhenSignalled
end
Reviews (13): Last reviewed commit: "bugfix(milesaudiomanager): Prevent dangl..." | Re-trigger Greptile
c9bfbb0 to
049a95b
Compare
|
I revisited the implementation and added new fixup commits to simplify it, because I noticed that we can avoid adding the deferred audio requests container by using a new flag in The RefCountMTClass is now no longer used, but we can keep it anyway for future use cases. |
730b739 to
cb3b9b4
Compare
|
Polished. Ready for review. |
|
I've been debugging this PR, and it sometimes softlocks when I exit near the factory and town in Alpine Assault. I didn't see anything strange in the debugger, but having a heightened camera increases the chances that it hangs. I think I found a race condition that explains it. |
|
|
||
| if (playing->m_rerequestOnNextUpdate) { | ||
| rerequestPlayingAudio(playing); | ||
| playing->m_rerequestOnNextUpdate = false; |
There was a problem hiding this comment.
Since processPlayingList() iterates without a lock, could a Miles background callback be modifying this object's state at the exact same time we read/overwrite it?
There was a problem hiding this comment.
I don't think so, because this bool is set true only in that miles callback and the miles callback will not be called again.
Break the debugger to see the callstack and where it hangs. |
|
I don't see where Regardless, there are some thread safety issues with that class so I'd like to see that extracted to a separate PR. |
|
I did use RefCountMTClass before. Then simplified code and it was no longer needed. I left it because it might be needed in future. I can remove it again. |
Fair enough.
Please remove it from this PR at least. A reference counting class is quite tricky stuff for multi-threaded purposes. It should be added in a separate PR if we ever need it. |
cb3b9b4 to
ad3b52e
Compare
First commit removed. |
ad3b52e to
9c94e13
Compare
9c94e13 to
6fd2dbb
Compare
6fd2dbb to
0ed037e
Compare
…in PlayingAudio when handing it over to a new AudioRequest after a call to MilesAudioManager::startNextLoop() (#2774)
0ed037e to
7b96d91
Compare
|
The bot had a lot of complaints here about my code. Pffft. |
| #include "Lib/BaseType.h" | ||
| #include "Common/STLTypedefs.h" | ||
| #include "Common/SubsystemInterface.h" | ||
| #include "mutex.h" |
There was a problem hiding this comment.
What does "GameAudio.h" use from "mutex.h"?
|
|
||
| #include "always.h" | ||
| #include "wwdebug.h" | ||
| #include "WWDebug/wwdebug.h" |
There was a problem hiding this comment.
There are many cases of this throughout the code base. Perhaps this should be done in a separate PR?
There was a problem hiding this comment.
This was needed here because this header was added to WWCommon.h
You are right this needs to be fixed at larger scale.
It is already split into a separate commit.
| { | ||
| if (that.m_audio[i]) | ||
| m_audio[i] = newInstance(DynamicAudioEventRTS)(*that.m_audio[i]); | ||
| m_audio[i] = RefCountPtr<DynamicAudioEventRTS>::Create_NoAddRef(newInstance(DynamicAudioEventRTS)(*that.m_audio[i])); |
There was a problem hiding this comment.
Why doesn't this do m_audio[i] = that.m_audio[i];?
There was a problem hiding this comment.
Because that would be not identical to what it did before.
Merge with Rebase
This change has 2 commits to work towards fixing race conditions in
MilesAudioManagerconcerning a sharedAudioEventRTSinstance in classesAudioRequestandPlayingAudio.The first commit implements a newRefCountMTClasswhich is fundamentally identical toRefCountClass, except it has a thread safe counter and all the debug functionality is omitted.The first commit adds the
RefCountMTClassRefCountClasstoDynamicAudioEventRTSto allow for shared ownership. All existing users ofDynamicAudioEventRTSaccomodate it and will now useRefCountPtrfor automatic reference counting.The second commit replaces
AudioEventRTS*withRefCountPtr<DynamicAudioEventRTS>inAudioRequestandPlayingAudioto allow sharing the audio event data between them. This is needed, because ownership will be shared in functionMilesAudioManager::startNextLoop(orMilesAudioManager::stopPlayingAudio), where previouslyAudioRequestwas given the sole authority to delete theAudioEventRTSwhilePlayingAudiostill kept a pointer to it. Now both classes need to release their reference count before the audio event data is deleted.This likely was also a problem in retail, because
AudioEventRTSis heap allocated, not pool allocated.TODO