버퍼 해상도 크기에 따라 OcclusionCullPipe의 Oversubscription이 증가하는 현상을 완화할 방법

PC 사양별로 Trace를 캡쳐하였습니다. 문제 발생 위치는 overview 이미지를 참조해 주시기 바랍니다.

pc 1

RTX 4080 super

intel i7 11700f

64gb ram

pc 2

rtx 4060

intel i7 12700

64gb ram

화면 해상도에 따라 OcclusionCullPipe의 Oversubscription 항목이 크게 변화하는 것을 확인할 수 있습니다.

최초로 현상을 발견했을 때에는 폴리지 인스턴스 수가 많은 것을 원인으로 판단하고 draw distance를 조절하는 방식을 적용하였으나, 특정 임계점 이후로는 distance를 줄여도 비용이 더 줄어들지 않는 것을 확인할 수 있었습니다.

이후 에디터 테스트 시 컬링 대상이 더 많음에도 불구하고 oversubscription이 발생하지 않는 상황을 확인하여, 원인을 파악하던 중, buffer의 해상도에 따라 oversubscription 비용이 크게 발생하는 현상을 확인하였습니다.

해당 이슈가 발생하는 원인과 4k 해상도에서 플레이가 가능하도록 비용을 절감하기 위해 어떤 접근을 취해야 할지에 대해 문의드립니다.

안녕하세요.

질문하신 사항에 대해 확인 후 답변드리겠습니다.

감사합니다.

안녕하세요.

Oversubscription은 모든 워커가 바쁜 상태에서 어떤 스레드가 Wait로 멈추면, 스케줄러가 그래프 진행을 막지 않기 위해 임시 워커를 잠깐 더 깨우는 동작입니다. 그래서 막대가 크게 보이는 것 자체만으로는 문제라고 단정하기 어렵습니다(“대기하는 동안 보조 스레드가 돌았다”는 신호에 가깝습니다).

첨부하신 인사이트에서 보이는 SyncPoint_Wait 패턴은 보통 CPU가 GPU보다 앞서가 결과(오클루전/HZB)를 너무 일찍 요구할 때 발생합니다. 즉, 프레임에 따라 GPU 쪽이 느려지는 순간이 있어 CPU가 첫 프리미티브에서 대기를 타는 형태입니다. 비슷한 사례로 아래 링크를 참고하시면 도움이 될 것 같습니다.

[Content removed]

다만, 첨부해주신 캡처와 샘플에서는 GPU 총 비용이 낮게 보이는데도 SyncPoint_Wait가 뜨는 구간이 있어, 5.5 스케줄러의 상호의존/재진입 이슈 가능성도 염두에 두고 있습니다. 현재 프로젝트를 5.6에서 동일 장면으로 A/B 테스트해 SyncPoint_Wait와 Oversubscription 체류 시간이 줄어드는지 확인해 보시길 권합니다. 이와 관련해 5.6에 관련 개선 사항이 있는 CL 41536900, CL 35125841을 참고하시면 좋을 것 같습니다.

마지막으로, 네이티브 4K에서 원활한 플레이를 위한 개발은 매우 어렵습니다. UE5의 현재 그래픽 고려 사항(UE5.3) | Tutorial

엔진은 나나이트, 루멘, VSM, TSR, 월드 파티션 등 여러 서브시스템이 맞물려 작동하므로 4k 플레이를 위한 최적화는 각 기능의 구조를 이해한 뒤 단계적으로 진행하셔야 제대로 된 결과를 얻을 수 있습니다. 이와 관련하여 도움이 되는 링크를 함께 첨부해드립니다.

https://dev.epicgames.com/community/learning/talks\-and\-demos/Vpv2/unreal\-engine\-optimizing\-ue5\-rethinking\-performance\-paradigms\-for\-high\-quality\-visuals\-part\-1\-nanite\-and\-lumen\-unreal\-fest\-2023

네이티브 4k 플레이를 위한 최적화가 쉽지 않다는 점은 저희도 이해하고 있습니다. 다만, 리포트에 첨부하였듯이, 샘플로 제공해드린 완전히 빈 프로젝트의 기본 오픈월드 씬에서도 동일한 현상이 발생하고 있습니다. 여기에서 발생하는 비용은 현재 제작중인 프로젝트에서도 거의 동일한 수준(4080s 기준 5 ~ 8ms)으로 나타난다는 점이 핵심 이슈입니다. 체류시간에 대해서는 udn의 다른 질문들을 통해 이미 확인하였고, 저희도 동일한 이슈로 접근하고는 있으나, wait의 원인을 짐작하기 어렵습니다.

[Content removed]

말씀하신 5.6 버전의 빈 오픈월드 레벨에서 테스트도 진행하였으나, ​약간의 차이가 존재할 뿐 buffer resolution에 따라 유사한 oversubscription 체류 시간을 나타내고 있습니다.

https://drive.google.com/file/d/1ThzXeM348We0IpHcrm3xbl\-7BWSnsxn6/view?usp\=sharing

구글 드라이브로 트레이스 및 테스트한 빈 프로젝트 추가하였으니 참조하시기 바랍니다. 빈 프로젝트는 nanite off, vsm off 외에는 엔진 기본 그대로입니다.

핵심 현상 정리하여 다시 공유드립니다.

  • 개발 프로젝트와 테스트용 빈 프로젝트​에서 동일한 수준으로 wait 발생
  • 해당 wait은 Buffer Resolution에 영향을 받으며, 특히 native 4k에서 크게 나타나나, 50% resolution에서도 아예 사라지지는 않음
  • 4060 등 메인스트림 하드웨어에서 해당 wait은 강력한 수준으로 나타나며 (4k native epic에서 약 30ms 이상)
  • 4060에서 gpu 비용을 줄이더라도 줄어든 gpu 비용 이상으로 나타납니다 (4k native high에서 24ms 수준)
  • 4080s와 같은 하이엔드 하드웨어에서도 발생 (4k native 시 5~8ms, 50% 해상도에서 1ms 수준)​

안녕하세요.

첨부해 주신 기본 프로젝트에서 보이는 OcclusionCullPipe의 Oversubscription 대기 시간은 프러스텀 컬링 자체의 비용이 아니라, 오클루전 결과(특히 하드웨어 쿼리)를 동기식으로 읽는 구간에서 발생한 대기에 가깝습니다. 기본 프로젝트처럼 렌더링 대상이 적어도 엔진은 그림자, 로컬 라이트, 플래너 리플렉션 등에 대해 하드웨어 오클루전 쿼리를 발행하고, 다음 프레임에 CPU가 결과를 읽기 때문에 결과가 준비되지 않았으면 렌더 스레드가 블록되어 대기 시간이 생깁니다. 이 대기 동안 그래프 정체를 막으려고 스케줄러가 임시 워커를 더 돌리면 인사이트에는 Oversubscription으로 표시됩니다.

[Image Removed]

해상도가 올라갈수록 깊이/베이스패스와 HZB 빌드 등 선행 GPU 구간이 길어지므로, 렌더 스레드가 상대적으로 더 이른 시점에 결과를 요구하는 빈도가 커지고 그만큼 대기 시간도 길어집니다. 따라서 폴리지나 드로우 디스턴스를 줄여 쿼리 개수를 감축해도, 결과를 읽는 타이밍이 여전히 GPU 완료보다 빠르면 어느 임계점 이후에는 대기 시간이 더 줄지 않는 현상이 나타날 수 있습니다.

완화책으로는 하드웨어 쿼리 대신 HZB 기반 오클루전을 강제하는 방법이 있습니다. r.HZBOcclusion 1로 설정하면 HZB 경로를 사용하여 동기 대기 지점이 크게 줄고, 실제로 Oversubscription 대기 시간이 감소하는 경향을 보입니다. 다만 HZB는 GPU/CPU 비용은 낮지만 결과가 더 보수적이므로, 콘텐츠에 따라 약간의 오버드로우 증가나 가시성 변화가 없는지 반드시 확인해 보시는 것이 좋습니다. 이와 함께 r.NumBufferedOcclusionQueries로 버퍼링 프레임을 늘려 대기를 더 느슨하게 하거나, r.DownsampledOcclusionQueries로 다운샘플된 깊이 버퍼를 사용해 쿼리 드로우 부담을 낮추는 방법도 상황에 따라 도움이 될 수 있습니다.

[Image Removed]

대체 수단으로 r.AllowOcclusionQueries 0을 사용하면 쿼리 제출·펜스·결과 읽기 경로가 통째로 비활성화되어 대기 시간은 크게 줄지만, 오클루전 컬링 자체가 꺼지면서 그림자·리플렉션 등에서 스킵되던 드로우가 다시 실행되어 GPU 비용과 최종 결과가 달라질 수 있습니다. 따라서 먼저 r.HZBOcclusion 1을 적용해 효과를 확인한 뒤, 필요할 때만 추가 옵션을 병행하는 순서를 권장드립니다.

컬링과 관련하여 아래의 공식 문서를 참고하시면 도움이 될 것 같습니다.

https://dev.epicgames.com/documentation/ko\-kr/unreal\-engine/visibility\-and\-occlusion\-culling\-in\-unreal\-engine

감사합니다.

예시적으로 말하자면, 프레임 26 렌더 스레드에서, 프레임 25 GPU의 HZB Build 결과를 참조해야 하기 때문에 대기하는 것이군요. 확실히 내용 이해했습니다.

HZB 기반 Occlusion으로 변경하는 것은 이미 테스트를 진행해 봤었는데요, 말씀하신 내용과 동일하게 컬링 자체에 큰 시간이 드는 것이 아니기 때문에 효과를 전혀 얻지 못했습니다. 승환님께서 캡쳐하신 스샷에서도 동일한 결과를 볼 수 있는데요, 아마도 GPU의 HZB 결과 동기화를 기다리는 시간 자체는 동일했기 때문이라고 생각합니다.

r.NumBufferedOcclusionQueries는 비용 분할은 조금 되었지만 카메라의 회전 / 이동에 따른 깜빡임 이슈가 눈에 잘 띄는 만큼 적용이 어렵다고 판단했고,

r.DownsampledOcclusionQueries의 경우에도 현재에도 실제 오클루전 쿼리(대기를 제외한)에 큰 시간을 쓰고 있지 않기 때문에 절감 효과가 크지 않았습니다. 테스트 씬에서나 제작중인 프로젝트에서나 이는 비슷합니다.

r.AllowOcclusionQueries 0 의 사용은 어렵습니다. 진작 검토는 해봤지만 특정 각도에 따라 납득하기 어려운 프레임 하락이 나타납니다.

[Image Removed]이런 점들을 감안하면, HZB 실행 타이밍 자체가 줄어드는 과정이 필요하다고 생각하고 있습니다. 이미지는 5.6 테스트 씬의 사례인데요, RHI Thread에서 GPU에 일감을 Queue한 이후 실제 GPU의 일을 시작하는 구간 사이의 이격이 상당한 편입니다. 이런 불필요한 wait은 Shader와 Compute 스테이지 처리 간에서도 마찬가지로 보이는데요, FXSystemPreRender에서 실제로 일하는 시간은 매우 짧지만, Shader쪽 일감이 진행되기를 상당히 기다리는 형태입니다. 이 또한 HZB 일감이 시작하는 지점을 딜레이하고 있습니다.

[Image Removed]제작중인 프로젝트 관련해서 가릴 부분을 최대한 가린 후 공유드리는 분석 결과입니다.

여기에서도 1201 프레임의 RHI Thread 이후 GPU에서 일하는 순간 사이에 갭이 상당한 편입니다. RHI Thread의 실행 시점은 RDG Graph를 빌드한 이후, 즉 Execution 타이밍이 되어야 하니 Render Thread와의 갭은 이해할 수 있는 범위입니다. 그러나 RHI Thread의 실행과 GPU 사이의 갭이 크고, 그 갭이 Occlusion Culling에게 Wait 압력으로 이어진다는 점은 효율적으로 CPU / GPU를 활용하기 어렵게 만드는 요인입니다.

해당 프로파일링 결과는 dev 빌드, Screen Percentage 66%에서 촬영되었습니다 (i7-11700, 4080 super)

네이티브 4k일 때 문제가 가장 도드라지긴 하지만, screen Percentage 66% 정도에서도 이 wait은 항상 나타난다는 점이 일을 어렵게 만드는 요인들 중 하나입니다. 50%에서는 간헐적으로, 5~10프레임에 한번 정도 눈에 띄구요.

혹시 관련해서 좀 더 확인해볼 수 있는 내용이 있을까요? culling 자체의 이슈가 아닌, RDG Execution - RHI Thread - GPU 의 일처럼 보여서 제가 능동적으로 찾아볼 만한 자료가 많지 않게 느껴집니다.

과거 빌드들 테스트해보고 공유해주신 내용들 다시 복기하면서 점검하고 있는 내용들을 공유드립니다.

GPU 시간이 Game, Render, RHI Thread보다 높은, 그러니까 GPU 바운드인 상황에서는 HZB의 지연으로 인해 Render Thread에 유휴시간이 계속 발생해도 합리적인 현상일 수 있습니다. 그러나 현재로서는 Render Thread의 부담으로 CPU 바운드인 상황인 만큼, GPU의 실행이 늦어지는 다른 원인을 찾아야 할 것으로 보고 있습니다.

그래서 몇 가지 추가적인 테스트를 하고 있는데요,

shipping 빌드에서는 dev 빌드 대비 훨씬 원활하게 실행되는 현상을 발견하였습니다. 다만 이 경우에는 프로파일링이 불가능한 상황이고, 프레임 정보도 nvidia / afterburner 등에서 n/a로 출력되는 등 확인이 쉽지 않아, 다른 GPU 프로파일러를 통해 프레임타임 분석 및 버퍼 해상도 점검 등의 추가 테스트를 진행 예정입니다.

안녕하세요.

현재 진행 중이신 프로젝트에서는 나나이트가 비활성화되어 있으므로, 나나이트 클러스터 단위의 컬링이 아닌 기존 방식의 디스턴스 컬링, 프러스텀 컬링, 오클루전 컬링이 적용되고 있을 것으로 보입니다.

첨부해주신 인사이트 이미지를 보면 렌더 스레드의 비용이 높게 나타나는데, 이는 컬링해야 하는 오브젝트의 수가 많기 때문일 가능성이 큽니다. 또한 렌더 스레드의 Ray Tracing 구간도 큰 편이므로, 컬링 대상이 아닌 액터들에 대한 필수 비용이라면 문제없지만, 만약 레이트레이싱 품질 설정이 과도하게 높아서 발생한 비용이라면 품질을 낮춰 테스트해 보시는 것도 권장드립니다. 따라서 디스턴스 컬링, HLOD, HISM 등 다양한 방법을 활용해 오브젝트 수를 줄이면 렌더 스레드의 부하를 완화하는 데 도움이 될 수 있습니다. 또한 월드 파티션을 사용 중이시라면, 월드 파티션 셀의 단위나 크기를 조정하는 것도 성능 개선에 효과적일 수 있으니 함께 확인해 보시길 권장드립니다.

감사합니다.

테스트 프로젝트를 통해, Shipping Build에서 약 1ms 정도 프레임타임이 줄어드는 것까지는 확인하였습니다. OcclusionCullPipe의 대기비용이 3~5ms 수준으로 나타나는 점을 고려 시 분석 우선순위에서는 미뤄둔 상태입니다.

렌더 스레드의 높은 비용은 오브젝트 수가 원인은 아닙니다. 이미 드로우 디스턴스를 절반으로 줄였지만, 고정적인 HZB 동기화 시간으로 인해 대기비용은 그대로이구요. OcclusionCullPipe의 감소가 에픽에서도 불가능하다고 판단하신다면 레이트레이싱 비용을 손대겠지만, 우선은 불필요하게 낭비하는 OcclusionCullPipe 대기비용을 제거하는 쪽이 핵심이라고 보고 있습니다.

RayTracing 구간은 OcclusionCullPipe와는 달리 대응방안을 이미 파악하고 추려두고 있습니다. 대상 인스턴스 수 감소가 핵심이므로 거리 조절 / 그래스 제외 및 컨택트 섀도우 보완 등으로 대응할 준비를 마쳐둔 상태입니다.

지금과 같은 경우에는 r.NumBufferedOcclusionQuries 2 의 경우에도 효과가 전혀 없습니다. 현재 프레임 HZB 결과를 포함하고 있기 때문에 단순히 느슨하게 만드는 것 외의 효과는 없는 것으로 추정하고 있습니다. 최초에 대기가 매우 길었을 때, r.NumBufferedOcclusionQuries 2 를 적용했을 때에도 비용이 줄어들었지 없어지지는 않은 것도 동일한 이유인 것으로 추정하고 있구요.

[Image Removed]다른 샘플 프로젝트 테스트 결과를 보면, 대기 비용이 그대로인 상태에, RHI Thread에만 약 3ms 수준의 비용이 추가되는 것을 확인할 수 있습니다.

  • 해당 프로젝트는 5.6 엔진 디폴트, 나나이트 활성화, 폴리지 툴로 애셋 배치 상태입니다.

프로젝트 테스트에서도 이는 동일하게 확인할 수 있습니다.

[Image Removed]동일하게 RHIT에 대기비용을 추가하는 것 외에는 더이상 wait 비용이 낮아지지 않습니다.

현재는 개발중인 프로젝트이기 때문에 장기적으로 보고 있으며, 단순히 덜 그리거나 대상을 줄이는 방향으로 16ms를 대응하지는 않고 있습니다. 대신 불필요한 대기인 OcclusionCullPipe를 줄여나가고, 필요하다면 엔진 측면의 해법을 탐색하는 것이 이번 질의를 통해 달성하고 싶은 것임을 이해해 주시면 감사드리겠습니다.

OcclusionCullPipe에 대해 추가적으로 더 확인해주실 내용은 없으실까요?

안녕하세요.

렌더 스레드가 GPU의 HZB 작업을 기다리는 이유는, 다음 프레임에서 그 결과를 안전하게 쓰기 위한 의존성이 있기 때문입니다. HZB 기반 오클루전이든 하드웨어 오클루전 쿼리든 GPU가 먼저 작업을 끝낸 뒤에야 이후 단계(오클루전 판정, Nanite·VSM 등)나 다음 프레임의 가시성 계산이 진행될 수 있습니다. 이 완료 여부를 확인하는 곳이 FSceneRenderer::WaitOcclusionTests로, 여기에 걸린 펜스가 끝나지 않았으면 렌더 스레드가 FRHICommandListExecutor::WaitOnRHIThreadFence로 GPU를 기다리게 됩니다. 따라서 GPU가 늦게 끝나면 HZB 결과를 참조해야 하는 렌더 스레드가 대기하면서 SyncPoint_Wait가 발생합니다.

OcclusionCullPipe 대기 지점을 정확히 보려면 아래 함수들을 차례로 살펴보시는 것을 권장드립니다.

  • FVisibilityViewPacket::FVisibilityViewPacket: 파이프 콜백과 프러스텀 컬 태스크가 언제 명령을 넣는지 확인
  • FSceneRenderer::RenderHzb / FenceOcclusionTests / WaitOcclusionTests: HZB와 하드웨어 쿼리가 어떤 펜스·대기 흐름을 만드는지 확인
  • BuildHZB: HZB 패스가 RDG에 등록·실행되는 방식 이해
  • FDeferredShadingSceneRenderer::InitViews: LaunchVisibilityTasks 이전 선행 작업이 RHI 제출·대기에 미치는 영향 확인

이 경로를 따라가면 ‘파이프 명령 → HZB 실행 → 펜스 대기’까지의 호출 흐름 파악에 도움이 될 것 같습니다.

또한 샘플 프로젝트와 실제 제작 중인 씬은 렌더링 조건이 크게 다르므로, 실제 콘텐츠의 특정 씬을 기준으로 최적화 테스트를 진행해 보시는 것도 함께 권장드립니다.

감사합니다.