1편 : https://dorasima.tistory.com/71
9. 픽셀 스트리밍
- 서버 강의를 들은 김에 (@널널한 개발자)
- 그래도 한번은 써먹어봐야겠다. 싶어서.... 픽셀 스트리밍을 시도해보려고 한다.
(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://learn.microsoft.com/ko-kr/vcpkg/get_started/get-started-msbuild?pivots=shell-cmd
- 이거를 기반으로
(괜히 관리자 모드를 킨 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()의 4번째 파라미터로 수신한 바이트 수 out 변수가, OVERLAPPED 구조체와 함께 사용할 수 없다는 것을 몰랐다.
- 몇번 찍어보니, 제대로 값이 나오지 않고 쓰레기 값 혹은 초기화 했던 값이 그대로 나와 작동이 제대로 되지 않았다.
- OVERLAPPED에 맞는 APC 제어용(?) 함수인
- GetOverlappedResultEx() 를 사용해서, 수신 바이트 수를 제대로 얻었다.
https://learn.microsoft.com/ko-kr/windows/win32/api/ioapiset/nf-ioapiset-getoverlappedresultex
- 아래처럼 바꾸고, discard나 buffering 로직이 이상했던 것을 다시 제대로 바꿨다.
- 그 결과는
- 잘 작동한다. 후후후
- 하지만 Client 쪽에서 프레임이 15FPS 정도로 많이 아쉽다...
18. 느낀점
- 사진이나 영상에 맞는 기술을 써본적이 없어서... 미디어 데이터에 맞는 기술을 사용해야 했다. 결국은 기술력 부족이다.
- 그리고 Client Buffering이 제일 어려운것 같다.... 영상처럼 용량이 큰 데이터를 적은 손실로 어떻게 받나... 참 신기하다.
(FFmpeg나 WebRTC 같은거를 사용하면 좀 괜찮나?)
19. 참고자료