끝끝내 왜 발생했는지는 알아내지 못했지만, 어떤 방식을 시도해봤는지 적어보았다.
발생 환경 : UE5.2, Windows10
포폴용 게임을 개발하던 어느날 아주 이상한 오류가 났다...
오류가 난 상황을 설명하기 전에 어떤 게임을 만드려고 시도하는 중인지 말해보자면,
리슨서버로 멀티플레이 게임을 구현하려고 하는 중이었다.
ServerTravel을 하기 전에 TCP 통신을 사용해 호스트의 LAN ip주소를 입력하면 해당 호스트의 서버에서 게임을 할 수 있도록 구현하는 중이었다.
void ULobbyUI::TryHost(FString IP, FString NickName)
{
// wait until all players join
auto GameInstance = Cast<UMSBGameInstance>(GetGameInstance());
AsyncTask(ENamedThreads::AnyBackgroundHiPriTask, [this, GameInstance]()->void
{
if(nullptr == ServerManager) ServerManager = NewObject<UTCPServer>();
ServerManager->OnPlayerCountUpdate.AddUObject(this, &ULobbyUI::UpdatePlayerCount);
check(GameInstance);
ServerManager->OnMaxPlayerJoined.AddDynamic(GameInstance, &UMSBGameInstance::EnterGameOnServer); // 서버트래블 수행위치
ServerManager->StartHost(); // maxPlayer가 채워질 때까지 클라이언트의 연결을 대기
});
}
void ULobbyUI::TryJoin(FString ServerIP, FString NickName)
{
MSB_LOG(Warning, TEXT("try join"));
Text_HostIP->SetText(FText::FromString(ServerIP));
// wait until all players join
auto GameInstance = Cast<UMSBGameInstance>(GetGameInstance());
AsyncTask(ENamedThreads::AnyBackgroundHiPriTask, [this, ServerIP, GameInstance]()->void
{
if(nullptr == ClientManager) ClientManager = NewObject<UTCPClient>();
ClientManager->OnPlayerCountUpdate.AddUObject(this, &ULobbyUI::UpdatePlayerCount);
ClientManager->OnMaxPlayerJoined.AddDynamic(GameInstance, &UMSBGameInstance::EnterGameOnClient); // 클라이언트트래블 수행위치
ClientManager->Join(ServerIP); // ServerIP로 참가 요청 보내기
});
}
위 코드에서는 문제가 나지 않았다. 다만 뷰포트에서 게임 종료를 누르더라도 소켓이 닫아지지 않아, 다음 플레이 시 호스트와 클라이언트 모두 무한대기하고 있는 상황이 벌어져서 각각의 람다에 아래 두 줄을 추가하였다.
ServerManager->OnMaxPlayerJoined.AddDynamic(ServerManager, &UTCPServer::CloseSocket);
ClientManager->OnMaxPlayerJoined.AddDynamic(ClientManager, &UTCPClient::CloseSocket);
(물론 각각의 인스턴스 소멸자에 소켓을 닫는 코드를 넣기는 했지만, 가비지컬렉터가 수거하기 전에 강제 종료되서 생기는 문제인지 제대로 닫히지 않는 모양이었다)
크래시 리포트 스택을 살펴봐도 아래와 같은 문구만 나올 뿐 어느 코드에서 오류가 발생했는지 나와있지 않았다.
UE5Editor_Core
UE5Editor_RenderCore
UE5Editor_RenderCore
UE5Editor_RenderCore
UE5Editor_Engine
등등(시도할 때마다 비슷한 형식의 다른 스택이 보임)
디버깅모드를 통해 오류가 날법한 곳에 브레이크포인트를 걸어놓고 돌려봐도
위와 같이 모든 브레이크포인트에서는 문제가 발생하지 않았고, 브레이크 포인트들을 다 통과한 후 GameThread가 아닌 처음보는 스레드에서(디버깅 할때마다 다른 스레드에서 오류가 났다) 알 수 없는 오류가 났다... 오류가 난 코드의 위치를 알 수도 없으니 참 막막했다.
그 다음으로는 로그파일을 뒤져보았다.
Log file open, 11/02/23 16:05:05
LogWindows: Failed to load 'aqProf.dll' (GetLastError=126)
LogWindows: File 'aqProf.dll' does not exist
LogProfilingDebugging: Loading WinPixEventRuntime.dll for PIX profiling (from ../../../Engine/Binaries/ThirdParty/Windows/WinPixEventRuntime/x64).
LogWindows: Failed to load 'VtuneApi.dll' (GetLastError=126)
LogWindows: File 'VtuneApi.dll' does not exist
LogWindows: Failed to load 'VtuneApi32e.dll' (GetLastError=126)
LogWindows: File 'VtuneApi32e.dll' does not exist
LogWindows: Started CrashReportClient (pid=12524)
LogWindows: Custom abort handler registered for crash reporting.
...
LogWindows: Failed to load 'WinPixGpuCapturer.dll' (GetLastError=126)
LogWindows: File 'WinPixGpuCapturer.dll' does not exist
PixWinPlugin: PIX capture plugin failed to initialize! Check that the process is launched from PIX.
...
아무리 봐도 알 수 없어서, 'aqProf.dll', 'VtuneApi.dll', 'VtuneApi32e.dll', 'WinPixGpuCapturer.dll'을 키워드로 에픽게임즈 커뮤니티를 찾아보았다.
놀랍게도 나와 비슷한 오류가 났다는 사람은 많은데 해결했다는 사람은 극소수였다. 질문 글에 달린 해결방법들은 아래와 같다.
내 경우도 그렇고 특정 레벨을 Open할 때 오류가 나는 사람이 많았어서 그런지 렌더링 쪽에 관련된 해결방법들이 많이 있었다.
나의 경우 Nvidia RTX 3080 ti를 사용하고 있는데 업데이트를 해도 소용이 없었다.
Project Settings > Platforms - Windows > Targeted RHIs > Default RHI
에 할당된 값을 DX 12에서 DX 11로 낮춰보라는 방법이었다.
해당 글의 질문자는 해결이 되었지만, 나는 같은 오류가 계속 나왔다..
에디터에서 변경하는 것이 아닌, DefaultEngine.ini 파일에서 직접 수정해도 된다.
DefaultGraphicsRHI=DefaultGraphicsRHI_DX12
-> DefaultGraphicsRHI=DefaultGraphicsRHI_DX11
다운로드 후 설치해보았지만, 해결 X
Edit > Plugins
에서 해당 플러그인을 임포트시켜봤지만 해결 X
.NET core sdk 3.1을 깔라는 내용의 댓글이 있었다. 1년 전 댓글이라 터무니없이 낮은 버전이긴 했지만 지푸라기라도 잡는 심정으로 설치했으나 해결 X (3.1버전 말고 현재 지원 중인 버전; 6.0 ~ 8.0을 깔아봐도 해결되지 않았다.)
두 방법 모두 X
언리얼 엔진 공식 레포지토리에 Pull Request가 하나 올라와 있다.(private repo이기 때문에 깃헙계정과 언리얼 계정을 먼저 연결하셔야합니당)
대충 이것과 비슷한 케이스의 오류가 나는 경우가 많은데, 이를 그냥 로깅할 때 무시하면 되지 않느냐는 내용이다. 답변은 (2023. 11. 02 기준) 현재 제약사항이 많아 당장 확인 및 반영이 어렵다는 입장.
pr 기록을 참고하여 바꿔보았지만 여전히 비슷한 오류가 났다(약간 다르긴 했다). 바꿨는데도 비슷한 오류가 나는 걸로 봐서는 한두줄 바꾼다고 해결될 문제가 아닌 듯 했다.
네이버 카페에도 역시나 비슷한 오류 때문에 고생하신 분들이 많았다. 그러나 해결했다는 분은 한명도 없었다....
내 경우에는 오류의 원인이 뭐가 되었든 간에 소켓을 닫는 코드를 작성함에 따라 발생한 문제였기 때문에
일단 소켓 닫는 코드 이전으로 커밋을 되돌려 놓았다.
소켓 연결을 담당하는 두 클래스를 싱글톤으로 만들면 소켓을 닫는 메서드를 굳이 델리게이트에 바인딩시키지 않고도
이미 바인딩되어 있는 다른 메서드에서 잠글 수 있지 않을까 생각 중이다.
소켓 클래스를 싱글톤으로 만들어도 Fatal Error가 나면서 게임이 강제종료되고 같은 로그가 띄워졌다.
소켓통신을 버리고 기본 OnlineSubsystem을 이용해도 같은 현상이 발생했다.(Fatal Error 나면서 강제종료되는 현상, 그리고 'aqProf.dll' (GetLastError=126))
// Character.cpp 코드에서 난 에러
[2023.11.14-11.27.39:748][356]LogWindows: Error: Unhandled Exception: EXCEPTION_ACCESS_VIOLATION reading address 0x0000000000000000
나의 경우 해당 오류가 났던 줄에서 Delegate를 Execute하고 있었는데, 이 부분에서 위의 오류가 났었다.
델리게이트에 바인딩하는 부분은 PostInitializeComponents였는데, 다음과 같이 게임모드 클래스의 인스턴스를 얻어와 게임모드 클래스의 메서드를 바인딩해주었다.
auto GameMode = Cast<ACustomGameModeBase>(UGameplayStatics::GetGameMode(GetWorld()));
CustomDelegate.BindUObject(GameMode, &ACustomGameModeBase::DoSth);
ServerTravel을 하게되면서 local에 있는 게임모드 클래스가 잠시 파괴되고 서버의 게임모드클래스의 인스턴스화가 진행되면서 해당 오류가 난다고 생각했었다.
그래서 Execute하기 전 바로 바인딩을 진행하였더니 오류가 해결되었다!
그럼에도 불구하고 로그파일을 확인해보니 '그 오류'는 여전히 나는 것으로 확인했다.
Log file open, 11/14/23 21:07:24
LogWindows: Failed to load 'aqProf.dll' (GetLastError=126)
LogWindows: File 'aqProf.dll' does not exist
LogProfilingDebugging: Loading WinPixEventRuntime.dll for PIX profiling (from ../../../Engine/Binaries/ThirdParty/Windows/WinPixEventRuntime/x64).
LogWindows: Failed to load 'VtuneApi.dll' (GetLastError=126)
LogWindows: File 'VtuneApi.dll' does not exist
LogWindows: Failed to load 'VtuneApi32e.dll' (GetLastError=126)
LogWindows: File 'VtuneApi32e.dll' does not exist
결론
aqProf.dll이 존재하지 않는다는 로그는 오류가 나지 않는 상황에도 로그에 띄워지기 때문에, 일단은 무시해도 될 것 같다.
그렇다면 왜 이전에는 EXCEPTION_ACCESS_VIOLATION오류가 뜨지 않았는가? <- shipping모드로 패키징하는 경우, 디버그모드로 패키징하는 것보다 적게 로깅된다.
아무튼 나를 1달도 넘게 고생시킨 에러가 해결되니까 기분은 좋다..
이 글을 보고있는 분들께도 나의 기록이 도움이 되었으면 좋겠다.