C++/Game

[게임 프레임워크 개발 일지] #17 Window 이것저것 수정하기

Kareus 2024. 2. 18. 22:49

지금까지 작업한 걸로 뭔가 만들어보려다 바로 에러가 나서

다시 수정할 필요성을 느꼈습니다.

 

1. Event mutex 처리

디버그 중에 가끔씩 프로그램을 종료할 때 mutex destroyed while busy 에러가 발생해서 알아보다가 문제를 발견했습니다.

이 에러 자체는 mutex가 lock인 상태에서 프로그램이 종료되는 등의 문제로 강제로 mutex가 삭제될 때 발생합니다.

실험을 해보니 디버그 중에만 에러 메시지가 나오고, 릴리즈 모드일 때 에러가 발생하지는 않습니다.

 

Window를 닫는 도중에 렌더나 이벤트 호출이 발생하면 안 된다는 생각에 두 mutex를 잠그고 끄게 했는데,

메인 쓰레드에서 이벤트를 호출하는 도중에 시스템 쓰레드에서 윈도우를 종료하면 데드락이 걸려서 발생하는 문제였습니다.

이외에 프로그램을 강제로 종료하면 lock_guard의 소멸자가 호출되지 않아서 이런 에러가 발생할 수 있긴 한데,

이건 강제 종료라는 상황부터가 특수한 경우이고, 별도의 해결 방법도 찾지 못해서 그냥 냅두기로 했습니다.

 

그래서 윈도우 종료 메서드에서 이벤트 쓰레드 락을 제거하고, 가만 생각해보니 재귀 호출로 mutex를 lock하게 되면 데드락에 걸리지 않나? 하는 의문이 들었습니다. 그때부터 삽질을 하게 되었군요.

 

이벤트가 어떤 함수든 될 수 있었기 때문에, call 함수에서 이벤트를 호출해 다시 call 함수를 호출할 수도 있었습니다.

다시 말해서, call 함수에서 Event mutex를 lock하고 이벤트를 호출한다면

Event mutex를 다시 lock하게 되는 문제가 잠정적으로 발생할 수 있다는 이야기죠.

 

이는 recursive_mutex를 사용하면 해결되는 문제입니다. 그 mutex를 소유하고 있는 thread가 아니면 다른 thread에서는 해당 mutex에 접근할 수 없겠죠.

근데 전 그냥 recursive call 자체를 차단하고 싶었습니다. 그냥 내버려두면 무한 재귀가 발생할 것 같아서요.

 

그런데 해결 방법이 떠오르질 않아서 이게 정석인지 야매인지, 괜찮은 방법이긴 한 건지 조차도 잘 모르겠지만\

무려 ChatGPT의 도움을 받아서 해결책을 찾았습니다.

thread local인 변수 하나를 만들어서 해당 쓰레드에서 lock 했는지 판별하라더군요.

 

thread_local bool EventMutexLocked = false;
std::mutex eventMutex;

void call()
{
    if (!EventMutexLocked)
    {
    	// mutex is not locked by current thread
    	std::unique_lock<std::mutex> guard(eventMutex);
        //if mutex is locked by other thread, wait for it.
        
        //mutex locked
        EventMutexLocked = true; //mutex is locked by current thread
        //do something
        EventMutexLocked = false;
    } //mutex unlocked as unique_lock deconstructed
}

 

EventMutexLocked가 thread_local이기 때문에, 실행되는 thread에 다라 EventMutexLocked 값이 꼬일 일은 없습니다.

eventMutex를 lock하고 //do something 파트에서 이벤트 호출이 일어나더라도,

같은 쓰레드 안에서는 if 조건문에서 걸리기 때문에 recursive call이 발생하지 않게 됩니다.

다른 쓰레드인 경우에는 unique_lock으로 인해 대기하게 되겠죠.

wait도 안 하게 하려면 std::unique_lock<std::mutex> guard(eventMutex, std::try_to_lock);으로 바꾸면 됩니다만

지금 경우를 예로 들면 이벤트가 스킵되겠죠.

처음에 이렇게 했다가 텍스트가 자꾸 입력이 되다말다 하길래 그제서야 눈치 챔;;

 

2. Window Resizing/Moving Event

SFML에서는 Window Resize가 끝난 후에 Resized 이벤트를 호출합니다.

Microsoft Windows 기준으로, 윈도우 메시지 자체에는 Resizing이나 Moving 관련 프로토콜 메시지가 존재하지만

이 이벤트를 처리하는 도중에는 그 이벤트만 호출하느라 thread가 block되기 때문에 SFML에는 관련 이벤트를 구현하지 않았다는 얘기가 있더군요.

실제로 윈도우의 크기를 조절하거나 위치를 이동하면 그 이벤트만 계속 호출되고, 다른 이벤트는 처리되지 않습니다.

그래도 Resizing Event 중에 렌더링은 깔끔하게 되었으면 좋겠다 싶어서, 이벤트를 구현해보기로 했습니다.

 

class ExtraEvent
{
public:
    struct WindowMoveEvent
    {
        sf::Vector2i position;
    };

    struct WindowResizeEvent
    {
        sf::Vector2u size;
    };

    sf::String type;
    WindowMoveEvent windowMove;
    WindowResizeEvent windowResize;
};

//...

case WM_MOVING:
{
	e.type = EventType::WindowMovingEvent;
	RECT rect;
	GetWindowRect(Handle, &rect);

	e.windowMove.position = sf::Vector2i(rect.left, rect.top);
	break;
}

case WM_SIZING:
{
	e.type = EventType::WindowResizingEvent;
	RECT rect;
	GetClientRect(Handle, &rect);
	sf::Vector2u size = sf::Vector2u(rect.right - rect.left, rect.bottom - rect.top);
	window->get().setSize(size);
	e.windowResize.size = size;
	break;
}

 

일종의 custom event를 추가한 것이기 때문에, 이전에 IME 이벤트 처리용으로 끼워넣은 프로토콜을 가져와서 추가해줬습니다. 그리고 ExtraEvent를 처리하는 쓰레드를 분리해줬습니다.

이렇게까지 쓰레드를 분리하는 게 맞나? 싶긴 했지만 기존 SFML의 pollEvent에 끼워넣을 수가 없어서 이렇게 했습니다.

 

 

그리고 가장 많이 바꾼 게 Clipping인데 이건 내용도 길 것 같고, 아직 테스트할 게 남아 있어서 조금 더 살펴보고 글로 써야겠습니다.