Unity 검색

공유

Is this article helpful for you?

Thank you for your feedback!

로딩 화면을 좋아하는 사람은 없습니다. 그런데 혹시 AUP(Async Upload Pipeline, 비동기식 업로드 파이프라인) 파라미터를 간단히 조정해서 로딩 시간을 획기적으로 개선할 수 있다는 사실, 알고 계셨나요? 이번 글에서는 AUP를 통해서 메시와 텍스처를 로딩하는 방법을 자세히 소개해 볼까 합니다. 이 방법을 이해하게 되면 로딩 시간을 훨씬 단축할 수 있을 것입니다. 이 방법을 적용한 일부 프로젝트에서는 성능이 2배 이상 개선된 효과를 보여주었습니다.

이 글을 통해 AUP가 작동하는 원리를 기술적인 관점에서 살펴보고, 어떤 종류의 API가 최고의 효율을 이끌어낼 수 있는지 알아보겠습니다.

사용해 보기

2018.3 베타 버전에서 가장 최적화된 최신 AUP를 이용하실 수 있습니다.

 

지금 2018.3 베타 다운로드

 

먼저, AUP를 언제 사용하는지, 그리고 로딩 프로세스가 어떻게 작동하는지 자세히 살펴보겠습니다.

AUP는 언제 사용할까요?

2018.3 이전 버전까지 AUP는 텍스처만 처리했습니다. 그러나 2018.3 베타를 시작으로, 이제 텍스처와 메시도 AUP로 로딩됩니다. 그렇지만 일부 예외도 있습니다. 읽기/쓰기 기능이 활성화된 텍스처, 또는 읽기/쓰기가 활성화되어 있거나 압축된 메시는 AUP를 사용하지 않습니다. (참고로 2018.2에서 도입된 텍스처 밉맵 스트리밍(Texture Mipmap Streaming)도 AUP를 사용합니다.)

로딩 프로세스의 작동 방식

빌드 프로세스를 진행하는 동안 텍스처나 메시 오브젝트는 직렬화된 파일에 기록되고 대량의 바이너리 데이터(텍스처 또는 버텍스 데이터)는 함께 제공되는 .resS 파일에 기록됩니다. 이러한 레이아웃은 플레이어 데이터와 에셋 번들 모두에 적용됩니다. 이렇게 오브젝트와 바이너리 데이터를 별도로 관리하면 직렬화된 파일(일반적으로 작은 오브젝트를 포함)을 더욱 신속하게 로딩할 수 있고 이후 .resS 파일의 대량 바이너리 데이터의 로딩을 간소화할 수 있습니다. 텍스처/메시 오브젝트는 역직렬화될 때, 커맨드를 AUP의 커맨드 대기열로 제출합니다. 해당 커맨드 실행이 완료되면, 텍스처/메시 데이터가 GPU에 업로드되어 오브젝트가 메인 스레드에 통합될 수 있습니다.

그림: 빌드를 위해 직렬화되는 시점의 메시 및 텍스처 데이터 레이아웃

업로드 프로세스를 진행하는 동안 .resS 파일의 대량 바이너리 데이터는 크기가 고정된 링 버퍼로 읽혀 들어갑니다. 메모리에 입력된 데이터는 렌더 스레드에서 시간 분할 방식으로 GPU에 업로드됩니다. 이때 링 버퍼의 크기와 시간 분할 기간이 사용자가 변경하여 시스템 동작에 영향을 줄 수 있는 두 가지 파라미터입니다.

AUP는 각 커맨드에 대해 다음의 프로세스를 진행합니다.

  1. 링 버퍼에서 필요한 만큼 메모리를 사용할 수 있을 때까지 대기합니다.
  2. 소스 .resS 파일의 데이터를 할당된 메모리로 읽어 들입니다.
  3. 포스트 프로세싱(텍스처 압축 풀기, 메시 충돌 생성, 플랫폼별 수정 등)을 수행합니다.
  4. 시간 분할 방식으로 렌더 스레드에 업로드합니다.
  5. 링 버퍼 메모리를 해제합니다.

커맨드 여러 개를 동시에 처리할 수도 있습니다. 단 이럴 경우 모든 커맨드는 각자 필요한 메모리를 동일한 공유 링 버퍼에서 할당해야 합니다. 링 버퍼가 채워지고 나면 새로운 커맨드는 대기 상태가 됩니다. 커맨드가 대기 상태에 있다고 해서 메인 스레드가 차단되는 문제가 발생하지는 않으며, 프레임 속도에도 영향을 주지 않습니다. 비동기 로딩 프로세스만 느려집니다.

이러한 영향은 다음과 같이 요약할 수 있습니다.

로드 파이프라인 비교
AUP 미사용AUP 사용사용자에 대한 영향
메모리 사용기본 힙에서 데이터를 읽으면서 할당(메모리의 상위 수위선)고정 크기 링 버퍼메모리의 상위 수위선 감소
업로드 프로세스데이터를 이용할 수 있을 때 업로드고정된 시간 분할로 분할식 업로딩히칭(hitch) 없는 업로딩
포스트 프로세싱로딩 스레드에서 수행(로딩 스레드를 차단)백그라운드 작업으로 수행로딩 시간 단축

로딩 파라미터 조정에 이용할 수 있는 공용 API

2018.3에서는 런타임 시점에서 다음 세 가지 파라미터를 조정하여 AUP를 최대로 활용할 수 있습니다.

  • asyncUploadTimeSlice – 각 프레임의 렌더 스레드에서 텍스처와 메시 데이터를 업로드하는 데 소요되는 시간입니다(단위: 밀리 초). 비동기 로드 작업이 진행 중일 때는 시스템이 이 크기로 두 가지 시간 분할을 수행합니다. 기본값은 2ms입니다. 이 값이 너무 작으면 텍스처/메시 GPU 업로드 시 병목 현상이 발생할 수 있습니다. 반대로 이 값이 너무 크면, 프레임 속도에 히칭이 발생할 수 있습니다.
  • asyncUploadBufferSize – 메가바이트 단위의 링 버퍼 크기입니다. 각 프레임에 업로드 시간 분할이 발생할 때 링 버퍼에 전체 시간 분할을 활용할 만큼 충분한 데이터가 있는지 확인해야 합니다. 링 버퍼가 너무 작으면 업로드 시간 분할이 짧아집니다. 기본값은 2018.2의 경우 4MB였으나, 2018.3에서 16MB로 증가했습니다.
  • asyncUploadPersistentBuffer – 2018.3 버전부터 적용된 이 플래그로 모든 대기 중인 읽기 작업이 완료될 때 업로드 링 버퍼가 할당 해제되는지 판단합니다. 이 버퍼를 할당하거나 할당 해제할 때 종종 메모리 단편화 문제가 발생할 수 있으므로 보통 기본값(true)을 유지하는 것이 좋습니다. 로딩 중이 아닐 때 메모리를 꼭 회수해야 한다면 이 값을 false로 설정하면 됩니다.

이러한 설정은 스크립팅 API 또는 QualitySettings 메뉴를 통해서 조정할 수 있습니다.

예시 워크플로

이제 기본값인 2ms의 시간 분할 및 4MB의 링 버퍼를 사용하여 AUP를 통해 여러 텍스처와 메시를 업로드할 때의 워크로드를 살펴보겠습니다. 이 예시에서는 로딩 중이므로 렌더 프레임당 2개의 시간 분할이 수행되며, 따라서 업로드 시간은 4ms가 되어야 합니다. 이 예시에서 프로파일러 데이터를 확인해 보면 약 1.5ms만 사용합니다. 또한 업로드 직후, 링 버퍼의 메모리가 사용 가능한 상태가 되면서 새로운 읽기 작업이 시작되는 것을 알 수 있습니다. 이는 더욱 큰 링 버퍼가 필요하다는 의미입니다.

그럼 링 버퍼를 늘려 보겠습니다. 지금 로딩 화면에 있으므로 업로드 시간 분할을 늘리는 것도 좋습니다. 여기에서 링 버퍼를 16MB로, 시간 분할을 4ms로 조정하면 다음과 같은 결과를 확인할 수 있습니다.

이제 렌더 스레드 시간의 대부분이 업로드에 사용되고 업로드 사이의 짧은 시간만 프레임 렌더링에 사용되는 것을 볼 수 있습니다.

아래는 업로드 시간 분할 및 링 버퍼 크기가 다양하게 설정된 워크로드 샘플의 로딩 시간을 나타낸 것입니다. 테스트는 OS X El Capitan을 실행하는 2.8GHz Intel Core i7 MacBook Pro에서 진행했습니다. 업로드 속도와 입출력 속도는 플랫폼과 기기마다 다를 수 있습니다. 워크로드는 유니티에서 내부 성능 테스트에 사용하는 Viking Village 예시 프로젝트에 포함되어 있습니다. 다른 오브젝트도 로딩이 되기 때문에 다양한 값 가운데 성능의 우위를 정확하게 가리는 것은 불가능합니다. 그러나 이 경우에는 4MB/2ms 설정을 16MB/4ms 설정으로 전환하면 텍스처와 메시 로딩이 두 배 이상 빠르다고 말할 수 있습니다.

이러한 파라미터를 이용한 테스트 결과는 다음과 같습니다.

AUP 설정 변경에 따른 로딩 시간 분석(단위: 밀리초)

따라서 이 예시 프로젝트의 경우 로딩 시간을 최적화하려면 아래와 같은 설정이 필요합니다.

QualitySettings.asyncUploadTimeSlice = 4
QualitySettings.asyncUploadBufferSize = 16
QualitySettings.asyncUploadPersistentBuffer = true

핵심 요점 및 권장 사항

텍스처와 메시의 로딩 속도 최적화를 위해 일반적으로 권장되는 방법은 다음과 같습니다.

  • 프레임 속도를 떨어뜨리지 않는 최대 크기의asyncUploadTimeSlice를 선택합니다.
  • 화면을 로딩하는 동안 일시적으로asyncUploadTimeSlice를 늘립니다.
  • 프로파일러를 사용하여 시간 분할 이용률을 살펴봅니다. 프로파일러에서 시간 분할은AsyncResourceUpload로 나타날 것입니다. 시간 분할이 완전하게 활용되고 있지 않은 경우 QualitySettings.asyncUploadBufferSize를 늘립니다.
  • 일반적으로asyncUploadBufferSize가 커질수록 로딩은 빨라집니다. 따라서 메모리에 여유가 있다면 16MB 또는 32MB로 늘립니다.
  • 로딩하지 않는 동안 불가피하게 런타임 메모리 사용을 줄여야 하는 상황이 아니라면, QualitySettings.asyncUploadPersistentBuffer 설정을 true로 유지합니다.

자주 묻는 질문

질문: 렌더 스레드에서 시간 분할 업로딩은 얼마나 자주 발생하나요?

  • 시간 분할 업로딩은 렌더 프레임당 한 번, 또는 비동기 로드 작업 중 두 번 발생합니다. VSync는 이 파이프라인에 영향을 줍니다. 렌더 스레드가 VSync를 기다리는 동안, 업로딩이 진행될 수 있습니다. 실행 프레임이 16ms이고 하나의 프레임이 예를 들어 17ms로 길어지면, 결론적으로 15ms 동안 VSync를 기다리게 됩니다. 일반적으로 프레임 속도가 높을수록 업로드 시간 분할이 발생하는 빈도가 증가합니다.

질문: AUP를 통해서 로드되는 것은 무엇인가요?

  • 읽기/쓰기가 활성화되지 않은 텍스처가 AUP를 통해서 업로드됩니다.
  • 2에서는 텍스처 밉맵이 AUP를 통해서 스트리밍됩니다.
  • 3에서는 읽기/쓰기가 활성화되지 않은 비압축 메시도 AUP를 통해서 업로드됩니다.

질문: 예를 들어 매우 큰 텍스처가 있다고 할 때, 링 버퍼의 크기가 현재 업로드되고 있는 데이터를 수용하지 못하는 경우에는 어떻게 되나요?

  • 업로드 커맨드가 링 버퍼보다 큰 경우 링 버퍼가 모두 소모될 때까지 대기합니다. 그런 다음 링 버퍼가 할당 크기에 맞추어 재할당됩니다. 업로드가 완료되면 링 버퍼는 재할당을 거쳐 원래 크기로 돌아갑니다.

질문: 동기식 로드 API, 즉 Resources.Load, AssetBundle.LoadAsset 등은 어떻게 작동하나요?

  • 동기식 로드 호출은 AUP를 사용하고, 기본적으로 비동기 업로드 작업이 완료될 때까지 메인 스레드를 차단합니다. 사용되는 로딩 API 유형은 작업에 영향을 미치지 않습니다.

의견 전달하기

유니티는 언제나 여러분의 피드백을 기다립니다. 댓글을 달아주시거나, Unity 2018.3 베타 포럼에 의견을 남겨 주세요!

2018년 10월 8일 엔진 & 플랫폼 | 7 분 소요

Is this article helpful for you?

Thank you for your feedback!

관련 게시물