D3D Tiled Resources를 이용한 텍스처 스트리밍 – wait제거하기

D3D Tiled Resources를 사용한 텍스처 스트리밍 기능을 D3D11 / D3D12 / DXR 3개의 렌더러에 모두 추가했다.

그 과정에 소소한 깨달음이 있었다.

GPGPU를 사용하면 대부분의 경우 결국은 CPU측 메모리로 결과를 가져와야 할 때가 많다.

최근 작업중이었던 텍스처 스트리밍도 마찬가지인데 어쨌든 GPU스레드가 버퍼를 스캔해서 어떤 텍스처를 로딩할지 GPU메모리에 써넣은 후 그걸 CPU측 메모리로 가져와서 CPU가 읽어야 한다. 텍스처를 업데이트 하는 주체는 CPU코드니까.

일단 D3D11에선 아무 생각없이 그냥 짜면 된다.

그런데 D3D12에서는 그냥 하면 존나 느리다.

그러니까 GPU Memory -> CPU Memory로 Copy하도록 command를 던져놓으면 언젠간 완료가 되긴 할텐데 그 타이밍은 아무도 모른다. copy가 완료되어야 CPU에서 읽을거 아닌가. 그러니까 결과적으로 Command Queue에 fence를 넣고 이 fence값이 완료되었는지 wait를 걸어야 한다.

여기서 성능이 반토막 혹은 반의 반토막 난다.

D3D12렌더러에선 이런 상황이 부지기수인데 중첩렌더링을 지원하도록 하면서 대부분 wait안하도록 처리했다. 어쩔 수 없이 wait하고 있는게 HW Occlusion Culling이 있다. 그리고 지금 작업중엔 텍스트 스트리밍쪽 코드가 wait를 하고 있는 것이다.

근데 가만 생각해보면 어차피 텍스처 스트리밍이란게 한 프레임 늦게 텍스처를 로드할 수 밖에 없다. page fault를 만나고 그 결과를 인지하고 나서 텍스처를 로드하는거니까.

그럼 이걸 GPU한테 분석하라 시켜놓고 그 결과를 기다릴 필요가 없지 않나? 그냥 잊어버리고 다음번 프레임에 지금 프레임의 결과를 활용하면 되잖아.

어차피 D3D12렌더러는 이미 여러 프레임(2-3) 중첩되어서 렌더링을 수행한다. 그러니까 각 프레임 컨텍스트마다 결과를 받아올 ID3D12Resource(시스템 메모리)를 할당해주고 한 프레임씩 늦게 처리하면 된다.

현재 프레임의 분석결과는 현재 컨텍스트의 메모리에 카피하되 읽지 않는다. 그리고 다음번 프레임에 이전 프레임의(지금 분석 맡겨놓은) 결과를 CPU에서 읽어서 사용하면 GPU에 한 프레임만큼의 꽤 충분한 시간을 보장해주게 된다.

  1. GPU메모리의 결과를 copy할 시스템 메모리측 ID3D12Resource를 중첩 렌더링될 프레임 수만큼 만든다.
  2. Tiled Resources의 residency를 갱신할 타이밍에 Compute Shader를 호출하고 그 결과를 다음 프레임 컨텍스트의 ID3D12Resource(시스템 메모리)에 copy 시킨다.
  3. 결과를 기다리지 않고 곧바로 이전 프레임 컨텍스트의 ID3D12Resource(시스템 메모리)로부터 결과를 읽어온다.
  4. 최초의 0프레임에선 당연히 결과값이 몽땅 0지만, 그 다음 프레임부턴 결과값이 충실히 채워져 있다.

이렇게 실행해도 딱히 프레임이 밀린다는 느낌은 느낄 수 없다. 프레임 레이트는 1.5배 이상 올라갔다!

이렇게까지 하고 나서 다시 생각해보면 D3D11에서도 마찬가지의 상황이 일어날거라고 추측할 수 있다. 물론 nvidia가 최선을 다한 덕에 D3D11에선 GPU측 메모리->시스템 메모리로 카피하고 곧바로 시스템 메모리에 Map()해서 CPU에서 읽어도 성능 저하가 심하지는 않다. 하지만 코드를 액면 그대로 실행한다면 내부적으로 wait를 아예 안할 수는 없을 것이다.

해서 D3D11에서도 D3D12에서 사용한 방법으로 N개의 시스템 버퍼를 만들어두고 이전 프레임에서 완료한 값을 읽는 방법을 도입해보기로 했다.

물론 D3D11에서는 내부적으로 중첩 렌더링을 수행하지만 바깥에서 볼때는 완전히 동기화되어서 1프레임씩 렌더링하는 것으로 보인다. 중첩 렌더링을 컨트롤 할 수 있는 별도의 API는 노출되어있지 않다.

상식적으로 생각해보면 CopyResource(Dest: System Memory A, Src: GPU Memory B) 이렇게 호출해 놓고 즉시 Map(System Memory A)를 호출한다면 내부적으로 wait할 것이다.

그러니까 이번 프레임에 Map()을 안하면 다음 프레임에 Map() 하면 되겠네.

  1. Tiled Texture Manager안에서 임의로 중첩 프레임은 2개라고 가정한다.
  2. 결과를 copy할 목적지 ID3D11Buffer는 원래 1개였으나 2개로 늘린다.
  3. Update할 때가 되면 Compute Shader를 호출하고 그 결과를 다음 프레임에 해당하는 ID3D11Buffer에 copy한다.
  4. 이전 프레임에 해당하는 ID3D11Buffer로부터 결과를 읽어온다.

코드 수정 후 놀랍게도 D3D11에서도 프레임이 1.5배 정도 뛰었다!

이렇게 해서 텍스처 스트리밍 기능 추가로인한 인한 성능 저하는 거의 없는 것으로….훈훈하게 마무리.

데모에서는 매 프레임마다 Tiled Resource의 residency를 갱신하고 있다.

D3D11: 779 FPS

D3D12: 540 FPS


댓글 남기기