Graphics Programming
OpenGL에서 멀티쓰레딩하기 (1/3) 본문
회사 다니면서 내팽겨쳐둔 OpenGL 렌더링 엔진을 다시 꺼내서 손보고 있다. 그래도 엔진 프로그래머랍시고 3년을 굴렀더니 잘못된 게 너무 많이 보여서 아키텍처에 또다시 대규모 공사를 하고 있다. 이 엔진을 바닥부터 다시 만드는 수준으로 갈아엎은 적이 이미 3번은 넘은 것 같은데 볼 때마다 엉망이다.
차라리 DirectX 12나 Vulkan으로 새로 만드는 게 낫겠다고 생각하다가도, 그러면 중간에 그만둘 거 같아서 이미 만든 걸 손보기로 했다. 그리고 이런 API는 쓰기 번거로워서 회사에서 돈 받으면서 코딩해야 한다. (크로스플랫폼 개발도 마찬가지다. 엑스박스 타이틀 개발하는 게 멋진 경험일 줄 알았는데 똑같은 걸 서로 다른 플랫폼들에서 돌아가게 하는 건 노가다일 뿐이다. 회사에서 돈 받고 해야지 집에서 취미로 할 게 아니다.)
맨날 PIX와 RenderDoc을 붙잡고 디버깅, 프로파일링만 하다 보니 API는 이제 아무래도 상관 없게 된 것도 있다. 어차피 GPU 들어가면 다 똑같아서 어떤 리소스가 바인딩되어 있고 셰이더 실행하면서 버퍼가 어떻게 변하는 지만 보면 된다.
이번에는 언리얼 엔진을 2년 다루고 다시 설계하는 거니 잘 되지 않을까? -.-;;
미래를 생각하면 멀티쓰레딩이 가장 시급하다. 지금 뜯어고치는 것도 이렇게 힘든데 나중에 했을 생각을 하면 끔찍하다.
기존에는 싱글쓰레드에다가 GL 함수들을 그대로 호출했다.
GLenum hdr_draw_buffers[] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 };
glCreateFramebuffers(1, &fbo_hdr);
glNamedFramebufferTexture(fbo_hdr, GL_COLOR_ATTACHMENT0, sceneContext.sceneColor, 0);
glNamedFramebufferTexture(fbo_hdr, GL_COLOR_ATTACHMENT1, sceneContext.sceneBloom, 0);
glNamedFramebufferDrawBuffers(fbo_hdr, 2, hdr_draw_buffers);
이걸 멀티쓰레딩하려면 두 가지 일을 해야 한다.
- 렌더 커맨드 리스트를 만들어 모든 GL 함수에 대한 프록시 함수를 정의한다. 프록시 함수는 gl 함수의 인자들을 그대로 복사하는 구조체를 만들고 커맨드 버퍼에 넣는다.
- 렌더 쓰레드에서 리스트에 있는 커맨드들을 실행한다.
DirectX 11/12, Vulkan은 API가 애초에 이렇게 설계되어 있지만, 역사와 전통의 OpenGL은... 그리고 나는 그걸 그대로 가져다 썼고...
그래서 커맨드 리스트를 직접 정의해야 하는데, GL 함수는 매우매우 많다. 그냥 때려칠까 고민하다가 glcorearb.h를 파싱해서 모든 함수를 자동 생성하면 되겠다는 생각이 들었다. #ifdef GL_GLEXT_PROTOTYPES ~ #endif로 묶인 부분에 GL 함수들이 파싱하기 쉽게 선언되어 있다. 스크립트는 허접하게 배운 Rust로 짰다.
// render_command_list.generated.h
void bindTextures(
GLuint first,
GLsizei count,
const GLuint *textures)
{
RenderCommand_bindTextures* __restrict packet = (RenderCommand_bindTextures*)getNextPacket();
packet->pfn_execute = PFN_EXECUTE(RenderCommand_bindTextures::execute);
packet->first = first;
packet->count = count;
packet->textures = textures;
}
// render_commands.generated.h
struct RenderCommand_bindTextures : public RenderCommandBase {
GLuint first;
GLsizei count;
const GLuint *textures;
static void APIENTRY execute(const RenderCommand_bindTextures* __restrict params) {
glBindTextures(
params->first,
params->count,
params->textures // 문제 있음. 글 밑부분에서 설명
);
}
};
대략 이런 식
이렇게 만들다 보니 깨달은 것들이 있는데,
1. 모든 GL 호출을 커맨드 리스트를 통해 호출하는 코드로 변경해야 한다. 렌더 함수들이 커맨드 리스트를 인자로 받고 함수도 커맨드 리스트를 통해 호출하도록 바꾸고 있는데... 요 며칠 간 1000줄 가량을 뜯어 고쳤지만 끝날 기미가 안 보인다 -_-
2. 리소스 생성/삭제는 지연 호출해서는 안 된다.
cmdList.createTextures(GL_TEXTURE_2D, 1, &texture);
cmdList.textureStorage2D(texture, 1, format, width, height);
cmdList.bindTexture(GL_TEXTURE_2D, texture);
cmdList.objectLabel(GL_TEXTURE, texture, -1, objectLabel);
여기서 glCreateTextures()가 반환하는 텍스처 이름이 texture에 저장되니까 이 이름을 후속 함수들에 매개변수로 넘기는 건데, 첫 줄에서 텍스처를 지연 생성해버리면 이후 함수들에서는 texture의 값이 올바르지 않다.
임시방편으로 리소스(텍스처, 버퍼, 프레임버퍼, 셰이더 등) 생성/삭제 함수들은 커맨드 버퍼에 쌓는 게 아니라 바로 GL 함수를 호출하도록 고쳤는데, 이런 경우 올바른 접근법은 그냥 glCreateTextures()를 바로 호출하는 것 같다. 커맨드 리스트를 통해 리소스를 지연 생성하고 싶을 수도 있기 때문에... 이건 고민을 좀 더 해야 한다.
3. 포인터로 전달되는 배열 인자
GLenum hdr_draw_buffers[] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 };
cmdList.createFramebuffers(1, &fbo_hdr);
cmdList.namedFramebufferTexture(fbo_hdr, GL_COLOR_ATTACHMENT0, sceneContext.sceneColor, 0);
cmdList.namedFramebufferTexture(fbo_hdr, GL_COLOR_ATTACHMENT1, sceneContext.sceneBloom, 0);
cmdList.namedFramebufferDrawBuffers(fbo_hdr, 2, hdr_draw_buffers);
이 글 첫 부분의 코드를 커맨드 리스트를 쓰도록 고친 것인데 예상치 못한 문제가 있었다. 이게 문제라는 것도 한참을 헤매다 깨달았다.
이 코드는 어떤 함수 안에서 호출되는 코드고, 따라서 hdr_draw_buffers는 로컬 변수다. namedFramebufferDrawBuffers()로 생성한 커맨드가 실제로 GL 호출로 이어질 때 fbo_hdr, 2는 값 복사여서 문제 없는데 hdr_draw_buffers는 쓰레기 값을 가리킨다. 그 쓰레기 값도 기가 막히게 계속 0이 읽혔다.
웃기게도 이 버그는 디버깅을 하려고 하면 사라지는 종류의 버그였는데, 문제가 되는 코드를 찾으려 중간 중간 커맨드 리스트를 flush하는 코드를 심고 실행해보면 이런 로컬 변수가 멀쩡한 채 실행되어 문제가 사라졌다.
처음에는 로컬 변수를 멤버 변수로 옮겼다가, 이런 인자의 수명 관리를 직접 해줘야 하면 귀찮기도 하고 언젠가 실수할 게 뻔하기 때문에 커맨드 버퍼와 별도로 파라미터 버퍼를 만들고, 커맨드 생성할 때 파라미터 버퍼에 복사한 후 포인터도 파라미터 버퍼의 내용물을 가리키게 처리했다. 다만 배열 파라미터가 있는 GL 함수들의 이름에 딱히 일관성이 없기 때문에 일일이 고친 다음 그런 함수들의 목록을 스크립트에 하드코딩해야 할 것 같다.
여기에 적은 것 외에도 커맨드 리스트를 도입한 것만으로 생긴 문제가 많지만, 제목을 (1/2) 라고 적었으니 다음 글을 쓸 때면 다 해결하기를 빈다...