Programming/Win32

(공부중) #2 Pixel Streaming 시도해 보기

Dorasima 2024. 8. 5. 14:28

1편 : https://dorasima.tistory.com/71

 

(공부중) App 구조 예쁘게 다시 바꿔보기

유영천 프로그래머(@megayuchi )님 방송을 가끔 보는데... 그동안 공부했던 D3D12 책이 그다지 좋지 않다는.... 얘기를 들었다. 그래서뭐라도 좀 해야겠다 싶어서 앱 구조라도 일단 예쁘게? 다시 짜보

dorasima.tistory.com

 

 

9. 픽셀 스트리밍

- 서버 강의를 들은 김에 (@널널한 개발자)

https://inf.run/wiSK

 

Windows 소켓 프로그래밍 입문에서 고성능 서버까지! 강의 | 널널한 개발자 - 인프런

널널한 개발자 | 이 강의를 통해 응용 프로그램 수준 프로토콜 설계 기법과 IOCP기반 고성능 서버 개발 방법을 배울 수 있습니다!, 수준 높은 프로그래머로 도약하고 싶다면? 윈도우 소켓 프로그

www.inflearn.com

- 그래도 한번은 써먹어봐야겠다. 싶어서.... 픽셀 스트리밍을 시도해보려고 한다.

(Unreal Engine에서 제공하는 그 멋있는 기능이 아니라... 그냥 화면만 실시간으로 보내는걸 목표로 한다.)

 

- 일단 화면 이미지 데이터를 메모리에 올려야 하니깐...

- D3D12_HEAP_TYPE_READBACK 속성을 가진 화면 캡쳐용 ID3D12Resource를 만든다.

 

- 나는 클래스를 대충 하나 만들어줘서, 원래 App 설계 (D3D12Renderer 클래스에 붙일 수 있도록)에 맞춰서 작동하도록 했다. 클래스 이름은... 미리 ScreenStreamer로 만들고

클릭하면 커집니다.

- 초기화 할 때, D3D12Renderer 클래스의 SwapChain Texture를 받아서 똑같이 만들고

- 백퍼버 개수도 똑같이 맞춰서 그린다.

 

- D3D12Renderer - Rendering의 ExecuteCommandList 이전에,

- 맴버로 가진 ScreenStreamer의 copy resource에 RenderTarget Resource를 복사한다.

- 그리고 테스트 용으로 파일로 만들어본다. DirectXTex를 이용해보겠다.

- 대충 스레드 하나 만들어서, 파일화 시켜본다.

- 다행히 잘 작동한다.

 

참고 : SaveToWICFile을 스레드에서 사용하려면, COM 초기화를 COINIT_MULTITHREADED로 main에서 해줘야 한다.

 

요로콤

 

10. 이미지 데이터를 보낼 서버 로직 만들기

- 일단... 상호작용이 없고, 그다지 중요하지 않은 데이터의 경우 UDP로 하면 좋다고해서 UDP 프로토콜로 만드려고한다.
- 9번에서 메모리로 올리는걸 성공했으니

- 클라이언트에게 메시지를 보내기를 해본다.

- 3개의 swap chain이고, 비교적 크기가 큰 이미지 데이터를 넘겨야 하기 때문에 스레드 여러개로 draw가 끝난 Texture는 미리미리 보내서 Client가 처리를 할 수 있도록 하면 좋겠지만... 일단 기능 자체를 구현하는데 집중하는 거로 해보겠다.

- Renderer 에서 Copy와 Send를 요청하면, ScreenStreamer 클래스가 가지고 있는 Winsock_Properties를 이용해서, 스레드가 작업 중인지를 판별한다.
- 작업이 유효하지 않으면, 스킵하고 다음 프레임에 대해 시도한다.

 

11 . 이미지 데이터를 받을 클라이언트 만들기

- 클라이언트는 서버가 보내준 픽셀 데이터를 받아 그대로, 화면에 출력한다.
- 여기서도 스레드 하나만 이용해서 UDP recvfrom()을 받는다.
- 여러개의 Socket과 port를 가지고 픽셀을 미리 카피하는 것은... 다음에 해보겠다.

일단은 하나의 스레드만 사용한다.

- Renderer_Client에서는 메모리 텍스쳐를 그대로 화면에 그리는 기능을 기반으로 렌더링을 한다.

- 현재 프레임을 그리려고 할 때recv 스레드에서 작업이 끝나면, 그 픽셀 데이터를 화면에 출력하고,

- 그렇지 않으면, 이전 프레임 텍스쳐를 복사해서 그대로 다시 그린다.



결과물은...

큰일 났다.

- 이전 화면이 보이는 이유는 아마도...

- 이 친구 때문일 것이다. 
- 냅다 이전 프레임을 복사해서 다시 그리도록 되어있는데, Pixel Download가 오래걸려서 스킵이 여러번 일어나면,

- 이전 프레임의 업데이트가 더 오래걸릴 수 있기 때문... 여기 로직만 바꿔도 더 좋은 결과가 나오지 않을까...

 

고쳐야 할점...

1) 송수신 측에서 여러개의 스레드를 사용하는 방법을 시도하면 좀 괜찮을까?
2) 괜히 UDP loss가 있을 까봐 1200으로 제한했는데, 더 좋은 방법을 찾아봐야 한다.
- 물론 Loopback으로 테스트 해서 loss가 생길리가 없다. ㅋㅋㅋㅋㅋ

3) IOCP랑 UDP랑... 엮을 수 있나?

 

12. Overlapped I/O + 프레임 제한

- 좀 더 편하게 보내기 위해, Overlapped I/O를 사용하려고 한다.
(IOCP와 UDP는 뭔가... 어울리는 느낌이 안들어서 중간에 포기했다.)

- 프레임을 서버와 클라이언트 각각 60으로 제한했다.

- 서버 송신과 클라이언트 수신은 여전히 단일 스레드로 작동한다.

 

- 코드는 대충 요로콤 작성했고,

(더 자세한 거는 GitHub가서 보십시오)

 

- 서버 쪽

 

- 클라이언트 쪽

 

- 결과는 다음과 같다.

- 결국 클라이언트에서 잘 receive 해야 한다. 그렇지 않으면 제대로된 픽셀 스트리밍을 구현할 수 없다.

 

13. 괜히 압축 전송 해보기

- 압축 알고리즘 중에 LZ4가 엄청 빠르다고 한다.

- 공짜에다가, vcpkg도 제공한다.

https://github.com/lz4/lz4

 

GitHub - lz4/lz4: Extremely Fast Compression algorithm

Extremely Fast Compression algorithm. Contribute to lz4/lz4 development by creating an account on GitHub.

github.com

 

- 일단 프로젝트에 적용을 하자.

https://learn.microsoft.com/ko-kr/vcpkg/get_started/get-started-msbuild?pivots=shell-cmd

 

Visual Studio에서 MSBuild를 사용하여 패키지 설치 및 사용

자습서에서는 MSBuild 및 Visual Studio에서 패키지를 설치하고 사용하는 프로세스를 안내합니다.

learn.microsoft.com

- 이거를 기반으로 

(괜히 관리자 모드를 킨 cmd에서)
> git clone https://github.com/Microsoft/vcpkg.git
> cd vcpkg && bootstrap-vcpkg.bat
(을 걸어주고)

- 시스템 속성 환경 변수 편집에서

- vcpkg.exe 가 있는 폴더를 설정해주고

(다시 관리자 모드로 킨 cmd에서)
> vcpkg integrate install
> vcpkg install lz4
(으로 lz4를 설치 해준 다음)

> cd (프로젝트 파일로 가서)
> vcpkg new --application
> vcpkg add port lz4
(을 걸어준다.)

 

- 프로젝트 속성에서 vcpkg 설정을 적절히 해주면?  
- 요로콤 lib가 프로젝트 안에 맞춰서 생성이 되고, vcpkg manifest file이 생성이 되고,

- 무사히 lz4.h 를 인클루드 할 수 있게 된다.

 

- 이제 좀 써먹어 보기

 

- 보낼때 LZ4_compress_fast로 압축을 하고, 받을 때 LZ4_decompress_safe_partial()을 이용해서 해제를 해보겠다.

- 압축률은 매우 괜찮아 보인다.

 

- 이제 테스트를 해보면?

- 큰일 났다...

(손실이 난 상태에서, 해제를 하면 저렇게 되는 건가?)

(압축 알고리즘과 해제 알고리즘이 다른 매커니즘이여서 저런가?)

 

14. Client에서 괜히 여러개의 버퍼로 받아보기

- 여러개의 스레드와 여러개의 버퍼를 Client에서 받는 것을 시도해보았다. (16개의 버퍼와, 16개의 스레드, 16개의 포트)

- 서버 업로드와, 클라이언트 다운로드를 15FPS으로 제한했다.
(lock은 안쓰는 방향으로 했다.)

 

결과는 훨씬 느려지고, 손실도 많아졌다. (WaitForSingleObject에서 10milsec을 타임아웃으로 정했다.)

 

- 이번에 시도한 ID3DResource의 Map으로 얻은 포인터에 바로 memcpy를 하는 방법이 뭔가 ... 느린느낌이...든다.
(이전에는 압축을 해제한 화면 데이터를 저장하는 버퍼가 따로 있었고 ,그것을 Upload용 ID3DResource에 copy하는 로직이 분리가 되어있었다.)

- 괜히 client에서 buffering을 한다고, 여러개의 버퍼와 스레드를 두었는데, 이것이 성능 저하를 일으킨듯하다.

- 지금 생각해보면 App에서 Port와 스레드를 많이 만들어봤자, Overlapped I/O와 Socket는 OS가 제어하고, (지금은 loopback이지만)  하나밖에 없는 NIC을 타고 들어올텐데... 

 

15. 수신을 단일 스레드에서 하고, 버퍼만 여러개 

- 그리고 세션으로 구분해서 넣어준다음

- Renderer에서 그릴 때, 압축 해제해서 Upload 해주는 방식으로 진행해주었다.

 

- 결과는?

 

- 이전보다 훨씬 빠르지만.... 움직일때 화면이 깨지는건 여전하다.

- 서버 송신측에서 움직일때 화면 텍스쳐를 뽑을때 문제가 생기는건가...
- 아니면 클라이언트 측에서 화면 수신을 완료하는 플래그가 너무 부실해서 그런건가...?

 

16. 혹시 송신측에서 잘못된 텍스쳐를 보냈을까?

- 압축/해제한 텍스쳐를 파일로 찍어보니 이렇게 깨지는 프레임이 간혹 나온다.

- 물론 깨지는 프레임의 비율은, 클라이언트에서 수신한 것 보다 훨씬 적다. (여전히 클라이언트에 문제가 있다는 뜻)

 

- 카메라가 움직일 때 화면이 깨진다... 

> LZ4_compress_fast 
> LZ4_decompress_safe 

을 readback data에 거는 과정에서 corruption이 일어나는 걸까?

(내 App의 해상도에 해당하는 이미지를 처리 할 때, 테스트 해본적이 있는데 수십ms가 걸린거로 어렴풋이 기억난다.)

- Fence 처리가 뭔가 문제가 있었나?

 

- 내 App은 3개의 swapchain Texture와 2개의 CommandList를 사용하는데...
- 원래는 현재 프레임을 ReadBack 대상으로 했는다.

- 이번에는 back buffer가 될 차례인 중간(intermediate) 버퍼를 ReadBack 대상으로 해보겠다.

- 현시점(?)에서 상대적으로 가장 먼저 drawcall을 받은 텍스쳐다.

- 사실 이건 상관 없다.

 

-  CopyTextureRegion 함수가 commandlist로 GPU에 올라가는 거라, 아예 타임라인이 다르다.

- 하나의 command list에서 drawcall과 copy call(?)이 차례대로 올라간다.

- fence를 통해 작업완료를 기다리고, 사용할 때는 아예 copy를 해서 사용한다.

 

- GPU의 타임라인... 혹은 DXGI API 만의 타임라인.... 뭐 이런저런 상황들이 겹쳐서,

- 저렇게 조금씩 깨지는거는 어쩔 수 없는 거라고 하고 넘어가야 할 것 같다.

(결론 : 잘 모르겠다.)

 

17. 문제 해결(!)

- 왠만하면 Client 문제이다.

- 그래서 수신하는 Packet에 대한 정보를 냅다 출력해보기로 했다.

- https://learn.microsoft.com/ko-kr/windows/win32/api/winsock2/nf-winsock2-wsarecv

 

WSARecv 함수(winsock2.h) - Win32 apps

연결된 소켓 또는 바인딩된 연결 없는 소켓에서 데이터를 받습니다. (WSARecv)

learn.microsoft.com

- WSARecv()의 4번째 파라미터로 수신한 바이트 수 out 변수가, OVERLAPPED 구조체와 함께 사용할 수 없다는 것을 몰랐다.

- 몇번 찍어보니, 제대로 값이 나오지 않고 쓰레기 값 혹은 초기화 했던 값이 그대로 나와 작동이 제대로 되지 않았다.

- OVERLAPPED에 맞는 APC 제어용(?) 함수인 

- GetOverlappedResultEx() 를 사용해서, 수신 바이트 수를 제대로 얻었다.

https://learn.microsoft.com/ko-kr/windows/win32/api/ioapiset/nf-ioapiset-getoverlappedresultex

 

GetOverlappedResultEx 함수(ioapiset.h) - Win32 apps

지정된 시간 제한 간격 내에 지정된 파일, 명명된 파이프 또는 통신 디바이스에서 겹치는 작업의 결과를 검색합니다. 호출 스레드는 경고 대기를 수행할 수 있습니다.

learn.microsoft.com

- 아래처럼 바꾸고, discard나 buffering 로직이 이상했던 것을 다시 제대로 바꿨다.

 

- 그 결과는

 

 

- 잘 작동한다. 후후후

- 하지만 Client 쪽에서 프레임이 15FPS 정도로 많이 아쉽다...

 

18. 느낀점

- 사진이나 영상에 맞는 기술을 써본적이 없어서... 미디어 데이터에 맞는 기술을 사용해야 했다. 결국은 기술력 부족이다.

- 그리고 Client Buffering이 제일 어려운것 같다.... 영상처럼 용량이 큰 데이터를 적은 손실로 어떻게 받나... 참 신기하다.

(FFmpeg나 WebRTC 같은거를 사용하면 좀 괜찮나?)

 

19. 참고자료

https://learn.microsoft.com/en-us/windows/win32/direct3ddxgi/d3d10-graphics-programming-guide-dxgi#multithread-considerations:~:text=The%20rendering%20thread,created%20the%20window.

 

DXGI overview - Win32 apps

This topic contains the following sections.

learn.microsoft.com