ShellEngine은 멀티 스레딩 구조를 가지고 있습니다.
크게 게임 스레드와 렌더 스레드의 구조로 나눠져 있으며, 각 스레드 내에서 분할 가능한 작업을 Task단위로 나눠 스레드 풀에 넣어서 병렬적으로 처리합니다.
모든 스레드의 작업이 끝난 후 동기화가 시작 되며, 동기화가 끝난 후 쓰레기 수집의 타이밍이 맞다면 쓰레기 수집을 거친 후 스레드들의 작업을 재개합니다.
class Texture :
public core::SObject,
public core::INonCopyable,
public core::ISyncable
{
SCLASS(Texture)
/*...*/
SH_RENDER_API void SyncDirty() override;
SH_RENDER_API void Sync() override;
};SH_RENDER_API void Texture::SyncDirty()
{
if (!bDirty.test_and_set(std::memory_order::memory_order_acquire))
core::ThreadSyncManager::PushSyncable(*this);
}
SH_RENDER_API void Texture::Sync()
{
if (bSetDataDirty)
{
CreateTextureBuffer();
bSetDataDirty = false;
}
bDirty.clear(std::memory_order::memory_order_relaxed);
}해당 코드는 Texture코드의 일부입니다. 텍스쳐는 여러 스레드에서 접근 할 가능성을 가지며 동기화가 필요합니다.
이러한 객체는 core::ISyncable인터페이스를 상속 후 SyncDirty()와 Sync()를 오버라이딩 하여 구현해야 합니다.
SyncDirty는 두 스레드가 공유 해야 할 자원이 변경 됐음을 알리는 함수입니다.
Important
SyncDirty 함수에서 core::ThreadSyncManager::PushSyncable를 통해 동기화 될 객체를 넣어줘야 하며, 중복으로 들어가지 않게 주의해야합니다.
Sync함수가 동기화 타이밍에 수행 되는 함수이며 이 함수 내부에서 동기화 코드를 작성하면 동기화 타이밍에 호출됩니다.
위의 예시인 텍스쳐 클래스의 동기화 방식은 Sync 타이밍 때 값을 재설정하는 지연된 업데이트 방식이라고 부르며, 스레드 별로 변수를 두고 swap 하는 방식인 더블 버퍼링 방식도 쓸 수 있습니다.
core::SyncArray<uint32_t> drawcall;
SH_RENDER_API void Renderer::Sync()
{
/*...*/
std::swap(drawcall[core::ThreadType::Game], drawcall[core::ThreadType::Render]);
}swap방식은 메모리를 추가로 먹지만 매우 빠르게 작동하니 성능에 중요한 곳에서 사용하면 좋습니다.
core모듈의 ThreadPool클래스를 이용하여 작업을 할당하고 future로 값을 받아올 수 있습니다.
// 파이프라인 커맨드 기록
std::vector<std::future<Command>> futureCommands;
futureCommands.reserve(renderPipelines.size());
for (auto& renderPipeline : renderPipelines)
{
futureCommands.push_back(core::ThreadPool::GetInstance()->AddTask(
[&, pipeline = renderPipeline.get()]() -> Command
{
std::thread::id tid{ std::this_thread::get_id() };
auto pipelineImpl = static_cast<VulkanRenderPipelineImpl*>(pipeline->GetImpl());
// 스레드 별로 할당된 커맨드 버퍼풀에서 커맨드 버퍼를 할당 받아온다.
auto pipelineCmd = context->GetCommandBufferPool().AllocateCommandBuffer(tid, VkQueueFlagBits::VK_QUEUE_GRAPHICS_BIT);
pipelineImpl->SetCommandBuffer(*pipelineCmd);
// 커맨드 버퍼에 파이프라인 명령을 기록한다.
pipelineCmd->Build([&] { pipeline->RecordCommand(cams, imgIdx); }, true);
return { pipeline, pipelineCmd };
}
));
}
// 스레드풀에 할당된 작업이 다 끝나면 넘어간다.
for (auto& futureCommand : futureCommands)
recordedCommands.push_back(futureCommand.get());