스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
Metal 3로 바인드리스 구현
Metal 3로 바인드리스를 구현할 때 레이 트레이싱과 같은 강력한 렌더링 기술을 활용하는 방법을 배울 수 있습니다. Argument Buffers를 간소화하고, 힙에서 가속화 구조를 할당하며, Metal의 유효성 검사 계층 및 디버거 도구의 향상된 기능의 이점을 통해 앱의 바인드리스 구현 과정을 원활하게 진행하는 방법을 보여드립니다. 또한 장기 리소스 구조를 통해 CPU 및 GPU 성능을 더 많이 활용하는 방법을 살펴보겠습니다.
리소스
관련 비디오
WWDC23
WWDC22
WWDC21
-
다운로드
♪♪ 바인드리스 Metal 3 안녕하세요, 반갑습니다 저는 Apple GPU 소프트웨어팀 Alè Segovia Azapian입니다 저는 GPU 소프트웨어팀 Mayur입니다 이번 세션에서는 바인드리스 렌더링에 대해 알아볼 겁니다 바인드리스 바인딩 모델은 셰이더에 리소스를 제공하는 현대적인 방법으로 레이 트레이싱 같은 발전된 렌더링 기술을 선보입니다 오늘은 바인드리스 바인딩 모델이 작동하는 원리부터 시작해서 Metal 3를 이용해 게임과 앱에 쉽게 적용하는 법을 알아봅니다
바인드리스 렌더링으로 데이터를 종합해서 CPU와 GPU의 성능을 향상시킬 새로운 기회를 창출할 수 있습니다 CPU와 GPU 타임을 향상시키는 두 가지 팁을 알려 드릴게요 그다음에는 Mayur가 맡아서 바인드리스 모델을 적용하는 데 도움이 되는 도구를 소개할 겁니다 바인드리스 모델에서는 리소스를 종합해서 인수 버퍼와 연결합니다 개념상으로 따지자면 이런 식으로 보입니다 이 예시는 신 안의 모든 메시가 합쳐진 배열을 보여 줍니다 파이프라인을 특정 슬롯에 연결해서 리소스를 독립적으로 바인딩하는 전형적인 바인딩 모델과 달리 바인드리스 모델은 리소스가 메모리에 우선 연결됩니다 이러면 싱글 버퍼와 바인딩해서 셰이더가 자유롭게 이동하며 필요한 리소스에 접근해서 정교한 표면과 빛을 계산할 수 있습니다 앱에서 바인딩이 사라지면 레이 트레이싱 셰이더는 반사를 아름답게 표현하기 위해 필요한 모든 데이터에 접근할 수 있습니다 앱에서 3D 모델과 텍스처를 만드는데 바닥과 트럭, 여러 물질과 하늘까지 포함되며 이는 모든 데이터를 인수 버퍼에 저장함으로써 레이 트레이싱 셰이더에서 사용할 수 있게 합니다 바인드리스 렌더링이 Heaps와 같은 다른 Metal 기능과 결합되면 앱과 게임의 성능이 나아질 겁니다 CPU의 부담을 줄인 덕분이죠 바인드리스 렌더링에 유용하게 쓸 수 있는 Metal 3의 네 가지 개선 사항에 대해 말씀드릴게요 인수 버퍼는 Metal의 기본이 되는 구조로 이를 통해 리소스끼리 연결할 수 있습니다 인수 버퍼는 다른 버퍼나 텍스처 같은 리소스를 참고합니다 Metal 3는 인수 버퍼 입력을 전보다 쉽게 만들어 줍니다 더는 인수 인코더 객체가 필요하지 않기 때문이죠 무제한 배열도 마찬가지입니다 이제 가속화 구조를 Metal Heap에서 할당할 수 있습니다 셰이더 밸리데이션 레이어는 리소스가 GPU 메모리에 없는 경우 경고를 보냅니다 이 네 가지 기능을 함께 사용하면 쉽게 바인딩을 없앨 수 있습니다
Metal 3에서 인수 버퍼를 입력하는 건 즐거운 일입니다 신을 인수 버퍼로 인코딩하려면 이 버퍼들을 신 데이터로 입력해야 합니다 인스턴스나 메시, 머터리얼 텍스처 같은 것들이죠 Metal 2에서는 인수 인코더로 이 작업을 수행합니다 우선 어떻게 이 객체들이 작동하는지 다시 한번 설명하고 Metal 3가 코드를 단순화하는 데 어떻게 도움이 되는지 보여 드리죠 우선, 인수 인코더에서는 인코더 인스턴스를 생성해야 하죠 셰이더 함수 리플렉션을 통해 생성하거나 구조체 멤버를 Metal에 생성합니다 인코더 인스턴스를 사용해 기록 대상과 오프셋을 타깃 인수 버퍼로 설정합니다 그리고 그 메서드를 사용해 버퍼에 데이터를 입력합니다 작년에 진행한 관련 세션을 확인해 보세요 인수 버퍼와 인수 인코더에 대한 자세한 내용이 담겨 있습니다 이 메커니즘은 훌륭합니다 하지만 인코더 객체는 관리가 어려울 때가 있습니다 Metal은 인수 인코더를 생성하는 두 가지 메커니즘을 제공합니다 어떤 메커니즘이 여러분의 앱에 적합한지는 알 수 없습니다 인수 인코더를 여러 스레드에서 사용하면 주의가 필요합니다 개발자들이 C 구조체 입력법을 직관적으로 이해하면 Metal 3를 사용해 인수 버퍼에 바로 적용할 수 있죠 Metal 3는 인수 버퍼 입력을 단순화합니다 다른 CPU 사이드 구조처럼 직접 입력하도록 하는 거죠 여러분은 이제 가상 GPU 주소뿐 아니라 여러분 리소스의 리소스 ID에 접근할 수 있죠 여러분이 인수 버퍼에 직접 입력하면 어떤 리소스를 참고하는지 Metal이 알 수 있습니다 기능적으로는 인수 인코더로 레퍼런스를 인코딩한 것과 같죠 더는 인코더가 필요하지 않다는 점을 제외하면 말입니다 이 기능은 인수 버퍼 티어 2를 지원하는 모든 기기에 지원됩니다 그건 2016년 이후 Mac과 A13 Bionic 칩 이상의 칩이 탑재된 iOS 기기를 뜻하죠
기기가 인수 버퍼 티어 2를 지원하는지 모르겠다면 여러분이 사용할 수 있는 MTLDevice 객체 쿼리가 있습니다 Metal 3에서 진행되는 프로세스를 보여 드리죠 버퍼 주소에 64비트 체제를 사용해 CPU 구조체를 정의하고 텍스처에는 MTLResourceID를 사용합니다 그다음 인수 버퍼를 할당합니다 MTLDevice나 MTLHeap에서 버퍼를 직접 할당합니다 버퍼의 콘텐츠를 받아 인수 버퍼 구조체에 캐스트합니다 마지막으로 주소와 리소스 ID를 구조체 멤버에 입력합니다 하이브리드 렌더링 데모에서 어떻게 실행되는지 살펴보세요 코드가 얼마나 간단한지 보세요 호스트의 구조체는 일반 버퍼의 GPU 주소를 직접 저장합니다 이건 64비트 무부호 정수라서 uint64_t를 사용했습니다 여기에 인코더 객체가 없기 때문에 여러분의 인수 버퍼 구조체 크기를 이용하면 됩니다 Metal은 GPU와 CPU 구조체의 크기와 정렬 상태를 보장해 클랭과 Metal 셰이더 컴파일러에서 일치하도록 합니다
그리고 버퍼를 할당합니다 버퍼의 스토리지 모드가 Managed나 Shared인 경우 그 위치에 포인터를 두고 직접 구조체 타입으로 변경합니다 그리고 마지막으로 일반 멤버를 gpuAddress로 설정하고 GPU의 메모리 요구량에 맞춰 오프셋을 조정하면 됩니다
제가 강조하고 싶은 건 구조체 선언이 변하는 방식입니다 Metal 셰이딩 랭귀지와 C 선언 사이에서 말이죠 이 예시를 보면 두 가지가 나뉘어 있습니다 하지만 원하면 공유된 헤더에서 단일 구조체 선언을 할 수 있고 조건부 컴파일을 이용해서 셰이더 컴파일러와 C 유형을 구분할 수 있습니다 여기에 C로 통합된 선언이 있습니다 __METAL_VERSION__ 매크로는 셰이더 코드만 컴파일합니다 그걸 사용해 헤더 선언에서 GPU와 CPU 코드를 분리합니다 여러분의 앱의 타깃이 C++라면 여기서 더 나아가 템플릿을 사용해 선언을 더 균일하게 만들 수 있습니다
인수 버퍼 샘플 코드의 모범 사례를 확인해 보세요 이렇게 하나의 구조체를 입력하면 됩니다 하지만 무제한 배열을 이용해 여러 개를 입력할 수도 있죠 Metal에서 인수 인코더를 사용해 무제한 배열을 구현할 수 있습니다 하지만 Metal 3는 구조체 배열을 채우면서 프로세스를 훨씬 더 단순하게 만들었습니다
하나의 구조체를 쓰는 것과는 뭐가 다른지 보여 드리죠 저장하고자 하는 모든 구조체에 저장 공간이 충분해야 합니다 그다음 각 구조체에 대한 데이터를 입력하며 배열을 반복합니다
코드 샘플로 돌아가 버퍼의 크기를 키워서 신의 구조체만큼 많은 메시를 저장합니다 지금 하는 과정은 CPU 버퍼에 하는 것과 동일합니다 구조체 크기에 메시 수를 곱하는 거죠 이게 얼마나 효과적인지 짚고 넘어가도록 할게요 이 단일 변수가 배열의 크기를 제어합니다 셰이더는 Metal 셰이더 컴파일러에 어느 때든 이 크기를 선언할 필요가 없고 어떤 위치든 자유롭게 표시할 수 있죠 이게 바인드리스 모델이 Metal에서 융통성 있는 이유입니다 제약 없이 모든 배열에 접근해서 셰이더를 입력하기 때문이죠 이게 끝입니다!
다음으로는 같은 크기의 버퍼를 할당하고 올바른 메시 구조체 유형에 포인터를 둡니다
이제 버퍼가 충분히 커졌으니 메시 구조체의 크기에 걸쳐 간단한 루프를 걸어 줍니다 그리고 마지막으로 각 구조체의 GPUAddress를 직접 설정합니다 오프셋을 조절해 설정해도 됩니다
셰이더의 GPU를 보면 이건 무제한 배열을 나타내는 한 가지 방법입니다 그걸 메시 포인터 매개 변수로 선언하고 셰이더에 전달합니다
이를 통해 콘텐츠에 직접적으로 자유롭게 접근할 수 있습니다 다른 C 배열과 마찬가지죠
다른 옵션은 모든 무제한 배열을 구조체로 끌어오는 겁니다 이렇게 하면 데이터를 한곳에 모아 셰이더를 깔끔하게 유지할 수 있죠 이 예시에서는 모든 메시와 머터리얼이 하나의 신 구조체로 합쳐집니다
신은 신 구조체를 사용해 단일 버퍼를 바인딩하고 셰이더로 직접 전달합니다 무제한 배열을 개별적으로 전달하지 않고 말이죠
접근 방식은 전과 같지만 지금은 신 구조체를 통해서 메시 배열에 도달합니다 이렇게 Metal 3에서 인수 버퍼와 무제한 배열을 입력합니다 완전히 개선된 API를 통해 더욱 직관적으로 CPU 구조체 또는 구조체 배열과 일치하도록 하는 겁니다 올해 레이 트레이싱 업데이트를 통해 레이 트레이싱 가속화 구조를 버퍼 및 텍스처와 함께 Metal Heaps에 할당할 수 있습니다
그 요소들끼리는 물론이고 다른 리소스와도 결합할 수 있다는 거죠 이러면 모든 가속화 구조체를 Heaps로 통합했을 때 useHeap을 사용해 한 번에 보관할 수 있기 때문에 좋습니다 이건 여러분 응용 프로그램의 렌더 스레드에서 CPU 용량을 줄일 수 있는 엄청난 기회입니다
Heaps에서 가속화 구조를 사용하는 팁을 알려 드릴게요 우선, Heaps에 할당될 때 가속과 구조는 기기마다 정렬이나 크기 및 요구 사항이 다릅니다 힙 할당을 위한 가속화 구조의 크기나 배열을 확인하기 위한 새로운 쿼리가 있습니다 heapAccelerationStructureSize andAlignWithDescriptor는 MTLDevice의 메서드를 이용해 구조 기술어를 위한 SizeAndAlignment를 정합니다 이건 다른 메서드니 잘 기억해 두세요 accelerationStructureSizes WithDescriptor 메서드와 다릅니다 MTLDevice에서 말이죠
이제 가속화 구조가 MTLHeap 객체에 포함됐습니다 useHeap:을 이용해서 모든 사용자에게 한 번에 알립니다 useResource를 이용해 각 리소스에 전하는 것보다 빠르죠 그리고 기억해 둬야 할 건 위험한 추적이 아닌 이상 Metal이 할당된 리소스에 대한 경쟁 조건을 막지 않는단 겁니다 그래서 다른 것과 가속화 구조 구축을 동기화하고 레이 트레이싱 작업을 해야 합니다 잠시 후에 더 얘기할 테니 걱정하지 마세요
이것에 대한 자세한 내용과 더불어 Metal 3 레이 트레이싱의 향상된 성능을 더 알고 싶다면 올해 만든 영상을 확인해 보세요 'Metal 레이 트레이싱 성능의 극대화'란 영상입니다 힙에 할당된 가속화 구조를 사용하면 CPU 사용량을 줄일 수 있는 기회가 생깁니다 마지막은 올해 제가 가장 좋아하는 기능 중 하나로 셰이더 밸리데이션 기능이 향상됐습니다
useResource와 useHeap에 대해 설명할 때 앱은 간접적으로 접근하는 모든 리소스에 대해 Metal에 레지던시를 표시하는 게 중요합니다 이 작업을 잊으면 리소스를 지원하는 메모리 페이지가 렌더링 시간에 표시되지 않을 수도 있습니다 명령 버퍼 오류, GPU 재시작 이미지 손상이 일어날 수 있죠 안타깝게도 바인드리스 과정을 시작할 때 이런 문제가 나타나는 건 흔한 일입니다 바인드리스에선 대부분 신 리소스가 간접적으로 접근하는 데다 셰이더는 런타임에서 포인터 탐색 결정을 내리기 때문이죠 올해 Metal 3에 새로운 기능이 도입됐습니다 셰이더 밸리데이션 레이어는 명령 버퍼 실행 시에 사라진 리소스 레지던시를 추적하는 걸 돕습니다 제가 구체적인 예를 보여 드릴게요 하이브리드 렌더링 앱의 업데이트 프로세스 중에 저희는 반사면이 부정확해 보이는 문제를 발견했습니다 밸리데이션 레이어가 어떻게 문제를 진단하고 해결했는지 보여 드리죠
Metal에 레지던시를 표시하기 위해 앱은 Heaps에 의해 백업되지 않은 모든 리소스를 변환 가능한 세트에 저장합니다 앱이 버퍼를 추가하고 텍스처를 추가합니다 레이 트래킹 커널로 보내기 전 렌더링 시간에 앱은 세트의 모든 리소스를 사용한단 걸 Metal에 표시합니다 이건 앱이 세트 위에 반복되는 간단한 프로세스로 각 요소에 useResource를 요청합니다 Metal은 레이 트레이싱 작업을 시작하기 전에 모든 리소스를 옮겨옵니다 이건 앱이 이 세트에 모은 리소스 코드의 일부입니다 앱은 인수 버퍼의 입력 프로세스의 일부로 이 일을 실행합니다 앱의 로딩 기능은 각 서브메시에서 반복됩니다 인수 버퍼에 입력하는 데 필요한 데이터를 모으는데 그게 머터리얼의 인덱스 데이터와 텍스처 데이터입니다 그다음 인덱스 버퍼 주소를 인수 버퍼에 저장합니다 머터리얼의 경우 텍스처 배열에 루프를 걸칩니다 텍스처 GPU 리소스 ID를 인수 버퍼에 입력합니다 그리고 마지막에는 개별적으로 텍스처를 추가합니다 서브메시 머터리얼부터 sceneResources 세트까지요 그러면 처리 시간에 보관 상태로 표시할 수가 있죠
안타깝게도 여기에 미세한 버그가 있습니다 앱이 명령 버퍼를 실행하는데 일부 경우에는 반사면이 누락될 수도 있죠 전에는 이것을 추적하는 게 어려웠습니다 Metal 3에서는 셰이더 밸리데이션 레이어가 해결책이죠 이런 문제가 명령 버퍼 실행 중 오류를 일으키면 문제가 무엇인지 나타납니다 오류 메시지에는 문제를 일으킨 셰이더 함수의 이름과 경로 이름 접근을 감지한 Metal 파일과 코드 라인 버퍼의 라벨과 크기는 물론이고 Metal에 속하지 않았다는 사실까지 포함되죠 그래서 Metal 객체에는 늘 라벨을 지정하는 게 좋습니다 이런 도구는 여러분의 앱에서 디버깅하는 동안 객체 식별을 도와주는 라벨을 사용합니다 이런 상세한 정보를 알고 있으면 셰이더 코드에서 누락된 리소스를 쉽게 찾을 수 있습니다 더 좋은 점은 디버깅 중단점이 활성화되면 Xcode가 셰이더 밸리데이션이 셰이더 코드에서 문제를 탐지한 부분을 편리하게 보여 준다는 겁니다 데모 앱에 인덱스 버퍼는 포함되지 않습니다 해결 방법은 간단합니다 코드로 돌아가서 앱은 누락된 인덱스 버퍼를 레지던트 리소스 세트에 저장합니다 이런 변화를 통해 레이 트레이싱 시간에 Metal이 인덱스 버퍼를 이용해 GPU에서 문제를 해결합니다 이건 필수적이면서 완전히 판도를 바꾸는 도구로 바인드리스 과정에서 몇 시간의 디버깅 시간을 절약할 수 있죠 Metal 3의 향상된 기능은 바인드리스 리소스를 구성하고 참조하는 데 도움이 됩니다 이제 주제를 바꿔서 바인딩이 없이 게임 성능을 최대화하는 법을 얘기해 보죠 이번 섹션에서는 두 가지 주제를 다룰 겁니다 유지되지 않는 리소스와 추적되지 않는 리소스입니다 이 두 가지는 오래되고 통합된 리소스가 있을 때 여러분은 CPU와 GPU의 성능을 높여 줍니다 CPU에 오래된 리소스가 있을 때 성능을 향상시키는 법을 말해 보죠 우선 Metal 리소스의 주기를 살펴보겠습니다 Objective-C와 스위프트는 참조 카운트로 객체 주기를 처리하죠 Metal 리소스는 이 모델을 따릅니다 리소스는 retainCount of 1으로 시작하고 영향력 있는 참조가 사라지면 런타임은 할당을 해제합니다 CPU와 GPU는 동시에 작동하기 때문에 CPU가 리소스 할당을 해제하면 문제가 됩니다 GPU는 여전히 작동 중인데 retianCount가 0이 되면 말이죠
이를 방지하기 위해서 Metal의 명령 버퍼는 사용하는 리소스에 대해 강한 참조를 생성하고 retainCount가 적어도 1을 유지하도록 합니다 Metal은 파이프라인에 직접 바인딩하는 리소스에 대해 강한 참조를 생성하는데 이때 setVertexBuffer나 setFragmentTexture를 이용합니다 여기엔 렌더링 첨부 파일도 포함되는데 여러분이 useHeap API를 통해 보관하기로 한 Metal 힙 객체와 useResource API를 통해 보관하기로 한 간접 리소스죠 두 가지가 힙의 일부라고 해도 마찬가지입니다 Metal 객체 주기에 대한 자세한 내용은 'metal-cpp와 C++의 Metal 프로그램'을 참고하세요 Metal이 이런 참조를 만드는 건 아주 유용합니다 프로그래머로서 GPU가 개체를 사용하는 동안 할당을 취소할 걱정은 할 필요가 없기 때문이죠 안전이 보장되는 Metal은 실행 속도가 아주 빠릅니다 하지만 CPU 비용은 아주 적죠 바인드리스 모델에서 앱이 Heaps에 리소스를 모으면 응용 프로그램의 도메인과 일치한 상태로 지속됩니다 예를 들어, 게임에서 리소스는 레벨이 올라도 계속 남아 있죠 이 경우에 리소스 주기에 대한 추가 보증은 Metal에서 필요하지 않게 됩니다 여러분은 Metal 명령 버퍼가 참조하는 리소스를 남기지 않도록 요청해 CPU 비용을 회수할 수 있죠
Metal의 리소스 자동 유지를 해제하려면 유지되지 않는 참조로 명령 버퍼를 생성하면 됩니다 MTLCommanQueue에서 다른 명령 버퍼처럼 여러분이 직접 생성하면 됩니다 앱을 변경할 필요도 없습니다 이미 리소스 주기가 보장된 경우가 아니라면 말이죠 세세하게 설정한 이 환경에서는 기본이 명령 버퍼란 걸 기억하세요 이 설정에서는 참조 리소스가 전부 유지되거나 사라질 겁니다
작은 마이크로벤치마크에서는 CPU 명령 버퍼 주기가 2% 감소했다고 측정됐습니다 유지되지 않는 참조가 있는 명령 버퍼로 전환되면서 말이죠 소소하게 성공하긴 했지만 이번에는 전체적으로 불필요하고 영향력이 센 참조가 생성되거나 파괴됐습니다
유지되지 않는 리소스는 CPU를 절약할 수 있는 기회를 줍니다 여러분이 이미 주기가 있는 리소스를 가진 경우에 말이죠 유지되지 않는 리소스와 비슷하게 추적되지 않는 리소스도 안전 기능을 비활성화할 수 있는 기회를 줍니다 더 좋은 성능을 얻기 위해서죠 시각적인 여러 효과는 중간 텍스처로 렌더링하고 버퍼에 입력한 다음 나중에 전달하면서 소비합니다 셰도우 매핑이나 스키닝 후처리 과정이 좋은 예시죠 이제 리소스를 제작하고 바로 소비하면 기록 후 판독 위험이 있습니다 여러 전달 과정에서 같은 리소스에 입력할 때 두 개의 렌더 패스에서 같은 첨부 파일을 연달아 그려 넣거나 2비트 인코더가 같은 리소스를 입력하는 경우엔 기록 후 기록 위험이 발생하죠 Metal 스케줄이 GPU에서 작동하기 때문입니다 추적된 리소스를 사용할 때 Metal은 자동으로 동기화 기본 요소를 사용하여 GPU 타임라인의 위험을 방지하죠 예를 들어, Metal은 버퍼에 입력을 마치기 위한 스키닝 패스를 산출하는 과정에서 GPU를 대기시킵니다 동일한 버퍼에서 읽은 신 렌더링 패스를 시작하기 전에요
이건 훌륭한 기능입니다 Metal이 접근하기 쉬운 그래픽 API인 이유 중 하나죠 하지만 Heaps에 리소스를 통합하는 과정에서 몇 가지 고려해야 할 사항이 있습니다
예시를 들어 보죠 여기에 사용 중인 GPU가 있습니다 버텍스 스키닝을 하는 두 프레임을 그린 다음 신을 렌더링하고 톤 매핑을 차례대로 적용하죠 앱이 GPU를 계속 사용하면서 Metal은 리소스 의존성을 바탕으로 리소스 렌더링과 산출 작업이 겹칠 수 있는 순간을 파악합니다 의존성이 없고 조건이 맞다면 Metal은 작업 일정을 겹치게 잡고 동시에 실행합니다 GPU를 포화시켜서 같은 시간 동안에도 더 많은 작업을 수행할 수 있게 하는 거죠
이제 앱이 힙에 리소스를 통합하면 모든 하위 리소스는 Metal에서 단일 리소스로 표시됩니다 이로 인해 Heaps가 효율적으로 작업할 수 있지만 Metal이 같은 리소스에서 기록 및 판독 작업을 수행하고 경쟁 조건을 피하기 위해 작업을 적게 잡아야 한단 뜻이죠 실제 위험이 없는 경우라고 해도요
이 상황을 '거짓 공유'라고 하며 여러분이 예상한 것처럼 이는 GPU 작업의 작업 시간을 증가시킵니다 성능과 관련해 팁을 알려 드릴게요 힙의 리소스 사이 의존성이 없는 경우 이런 일은 피할 수 있습니다
거짓 공유를 피하기 위해 위험 추적에서 리소스를 제외하고 세분화된 의존성 신호를 Metal에 직접 보낼 수 있습니다 리소르 추적을 중지하려면 리소스 기술어 설정에서 hazardTracking 프로퍼티를 Untracked로 설정합니다 이 과정은 매우 중요하며 Heaps의 기본 절차입니다 이를 통해 GPU에 더 많은 기회를 제공하기 때문에 작업을 바로 동시에 진행할 수 있습니다 추적되지 않는 리소스를 사용하기 시작하면 다음 기본 요소를 통해 의존성을 표시해야 합니다 상황에 따라서 Fences나 Events Shared Events나 Memory Barriers 중 고르면 되죠 Metal Fences는 여러 렌더링과 패스 산출 과정에서 하나 이상의 리소스에 대한 접근을 동기화합니다 단일 명령 대기열의 콘텍스트 내에서요 이건 일종의 원시적인 장벽인 셈입니다 소비자 패스는 제작자가 Fence에 신호를 보낼 때까지 기다리죠
여러분이 명심해야 할 건 Fence를 사용할 때 명령 버퍼를 입력하기 전에 제작자 명령 버퍼를 커밋하거나 대기열에 올려야 한다는 겁니다 이 순서를 지킬 수 없거나 같은 기기에서 여러 개의 대기열에 동기화해야 하는 경우에는 MTL Events를 사용하세요 Events를 통해 소비자 명령 버퍼는 제작자의 명령 버퍼가 지정된 값으로 Event 신호를 보낼 때까지 기다립니다 이 값으로 신호를 보낸 후에는 리소스를 읽어도 안전합니다 Events를 이용해 신호가 올 때까지 GPU 작업을 중지시키는 거죠 MTLSharedEvents는 Events와 비슷하게 작동하지만 단일 GPU를 넘어서 더 광범위하게 작업합니다 이를 이용해 다른 Metal 기기와 CPU에서 리소스에 대한 접근을 동기화할 수 있습니다 예를 들어 Shared Events를 사용해서 CPU에서 GPU 계산 결과를 처리할 수 있는 거죠 예시를 보여 드릴게요 이 경우, GPU는 산출된 패스에서 메시로 스킨을 표현하고 CPU는 디스크에 자세를 저장합니다 이 둘은 독립적인 기기라서 Shared Event를 사용해 GPU가 리소스를 생성할 때까지 CPU가 대기하도록 합니다 처음에 CPU는 GPU가 Shared Event 신호를 보낼 때까지 무조건 대기합니다 GPU가 리소스를 생성하고 통합 메모리에 배치하면 Shared Event에 신호를 보냅니다 그때 CPU에서 대기하던 스레드가 움직이며 리소스를 안전하게 소비하는 거죠
마지막 기본 요소는 Memory Barriers입니다 Memory Barrier는 싱글 렌더나 산출된 패스 내에서 이전 명령이 완료될 때까지 후속 명령은 강제로 대기하게 하죠 Barrier 비용과 Fence 비용은 거의 모든 경우에서 비슷합니다 하지만 한 가지 예외가 있죠
렌더 패스의 프래그먼트 스테이지 이후의 배리어가 예외가 됩니다 이런 배리어에는 비용이 많이 듭니다 렌더 패스를 분할하는 것과 비슷하죠 GPU의 프래그먼트 스테이지 이후 Metal에서 배리어를 비활성화해 여러분의 앱이 가장 빠른 경로를 유지하도록 해 줍니다 Apple GPU에서 애프터 프래그먼트 배리어를 추가하는 경우에도 Metal 디버그 레이어가 밸리데이션 에러를 일으킵니다 프래그먼트 스테이지 후 리소스를 동기화하려면 Fence를 이용하는 게 좋습니다
다음은 동기화 기본 요소와 사용 시기에 대해 간단하게 정리한 내용입니다 제작자의 단일 명령 대기열에 작업을 올리는 경우에는 오버헤드를 최소화하려면 Fence를 사용하는 것이 좋습니다 Fence는 일반적인 경우에는 대부분 적합합니다 제출 명령을 보장할 수 없을 때나 명령 대기열이 여러 개일 경우 Metal Events를 사용하세요 Shared Events를 통해 여러 개의 GPU끼리는 물론이고 CPU와도 동기화할 수 있습니다 이는 특정 다중 기기에서만 사용할 수 있죠 패스 내에서 동기화하는 경우는 Memory Barrier를 이용하세요 기본 요소 Barriers는 대부분의 경우 빠르게 작동합니다 동시에 패스를 산출할 때나 드로우 콜 사이 버텍스 단계에서요 하지만 프래그먼트 스테이지 후엔 여러 패스를 동기화하려면 Barrier 대신에 Fence를 사용하세요 이런 Barrier가 매우 비싸서 Apple GPU에서 허용하지 않거든요
추적되지 않는 리소스와 세밀한 수동 추적을 통해 데이터 수집의 모든 이점을 누릴 수 있습니다 GPU의 병렬성을 극대화하는 거죠 이것은 바인딩 없는 작업에서 CPU와 GPU를 최대한 활용할 수 있는 성능 관련 팁입니다 Metal 3가 바인딩 없이 단순하고 효율적인 작업 환경을 어떻게 만드는지 말씀드렸죠 하지만 코드를 입력하는 건 절반에 불과합니다 나머지 반은 사용 가능한 도구가 GPU에서 작업을 확인하고 그걸 실행하는 데 있어 여러분에게 도움이 되는 방법이죠 여기서부터는 Mayur가 바인드리스 Metal 3 도구의 새 기능에 대해 얘기해 드릴 겁니다 고마워요, Alè 여러분께 Metal 디버거의 새 기능을 알려 드리게 되어 기쁩니다 이는 바인드리스 앱을 디버깅하고 최적화하는 데 도움이 되죠 제가 방금 HybridRendering 앱의 프레임 캡처를 했는데요 앞서 Alè가 여러분께 보여 드린 화면이죠 Metal 디버거로 프레임을 캡처하면 Summary 페이지로 이동하는데 여러분 프레임에 대한 개요를 제공함과 동시에 앱의 성능 향상에 도움이 되는 법도 알려 줍니다 하지만 오늘은 새로운 의존성 뷰어를 보여 드릴 겁니다 왼쪽에 있는 Dependencies를 클릭해 페이지를 열어 줍니다 그러면 새로운 의존성 뷰어가 나타납니다 강력하고 새로운 기능이 가득한 새 디자인이 특징이죠 의존성 뷰어는 여러분 작업을 그래프를 기반으로 한 프레젠테이션으로 보여 줍니다 그래프의 각 교점은 패스를 의미하는데 명령 인코더와 출력 리소스에 의해 인코딩되는 부분이죠 모서리는 패스 간의 리소스 의존성을 나타냅니다 올해는 두 가지 유형의 의존성에 초점을 맞춰 여러분의 작업을 분석할 수 있습니다 바로 데이터 흐름과 동기화입니다 실선은 데이터 흐름을 나타내는데 여러분의 앱에서 데이터가 어떻게 움직이는지 보여 줍니다 점선은 동기화를 나타내며 의존성을 나타내며 패스 간의 GPU 동기화를 보여 줍니다 자세한 내용을 보려면 인코더나 리소스, 모서리를 클릭해 보세요 디버거는 새로운 사이드바에 다양한 세부 정보를 보여 줍니다 예를 들면, 이 모서리는 동기화를 추가하고 패스 간의 데이터 흐름도 표시하고 있습니다 기본적으로 의존성 뷰어는 데이터의 흐름과 함께 동기화된 의존성을 보여 줍니다 하지만 아래쪽에 있는 이 메뉴를 사용해서 두 가지 유형 중 하나만 볼 수 있습니다 저는 동기화만 집중적으로 보도록 하겠습니다 앞서 Alè가 말했듯이 거짓 공유는 흔한 문제입니다 추적된 힙에서 다른 리소스를 기록 및 판독할 때 발생하죠 의존성 뷰어를 사용하면 쉽게 이런 문제를 발견할 수 있습니다 이 데모는 그런 문제가 있었던 초기 개발 버전에서 제가 캡처한 겁니다 제가 이 힙을 누르면 의존성 뷰어는 힙이 추적되고 있음을 보여 주고 두 패스 사이 동기화를 추가합니다 또한 의존성 뷰어는 힙 내부에 할당된 리소스도 강조합니다 렌더 인코더가 저장하는 렌더 타깃 텍스처나 추출 인코더가 기록하고 판독하는 버퍼 같은 거죠 문제는 이 두 패스 사이 동기화가 필요치 않다는 겁니다 추출 인코더가 이전 인코더의 리소스를 사용하지 않기 때문이죠 앱의 의존성을 제거하기 위해 추적되지 않는 힙을 사용 설정하고 동기화가 필요한 곳에 Fence를 삽입할 수 있습니다 이렇게 변경하면 두 패스를 동시에 실행할 수 있습니다 바인드리스 앱의 디버깅을 돕는 Xcode 14에서 크게 개선된 또 다른 점은 새로운 리소스 목록입니다 디버깅하고자 하는 드로우 콜을 탐색하고 열 수 있습니다 바인딩이 사라지면 수백, 수천 개의 리소스를 언제든 GPU에서 사용할 수 있습니다 올해 Metal 디버거에서는 어떤 리소스가 드로우 콜에 접근했는지 확인할 수 있게 해 줍니다 맨 위의 'Accessed' 모드만 클릭하면 되죠 이제 디버거에 일부 리소스만 보이는데 드로우 콜에 접근한 리소스와 각 접근 유형을 보여 줍니다 여러분의 셰이더가 인수 버퍼에서 접근한 리소스를 이해하는 데 아주 유용합니다 여러분의 드로우 콜이 쓰는 리소스를 아는 건 중요합니다 하지만 여러분이 예상치 못한 리소스가 나오면 세이더 디버거를 이용해서 무슨 일인지 파악할 수 있죠 셰이더 디버거를 시작하려면 아래쪽에 있는 디버그 버튼을 클릭합니다 그리고 디버그하고자 하는 픽셀을 선택하고 디버그 버튼을 누릅니다 이렇게 하면 셰이더 디버거 창이 뜹니다 셰이더 디버거는 여러분의 코드가 실행된 과정을 보여 줍니다 어떤 리소스가 접근했는지도 포함되죠 이 줄에선 셰이더가 인수 버퍼의 텍스처를 읽은 걸 알 수 있죠 오른쪽에 있는 사이드바에서 자세히 보기를 확장해서 어떤 리소스가 읽혔는지 확인할 수 있습니다 이 기능은 여러분의 셰이더가 어떤 잘못된 인수 버퍼에 접근했는지 문제 파악을 도와줍니다 이 데모에서는 새로운 의존성 뷰어를 사용해 리소스 의존성을 분석하고 검증하는 방법과 새로운 리소스 목록을 사용해 어떤 리소스가 드로우 콜에 접근했는지 알아보는 법 셰이더 디버거를 사용해 셰이더가 실행되는 방식을 줄별로 분석하는 법까지 알아봤습니다 여러분이 이 기능들을 사용해서 어떤 Metal 바인드리스 앱을 만들지 궁금하군요 이제 다시 Alè 차례예요 수고했어요, Mayur 정말 끝내주는 데모였어요 Metal 3는 바인딩 없는 작업을 위한 많은 특징을 지녔습니다 단순화된 인수 버퍼 인코딩 과정 Heaps의 가속화 구조 밸리데이션 레이어와 도구 개선이죠 Metal 3는 뛰어난 API로 여러분의 게임과 앱이 바인딩 없이 효과적인 성능을 내게 해 줍니다 하이브리드 렌더링 앱이 기능 향상을 통해 더 좋아졌습니다 저희가 Metal 샘플 코드 갤러리에 전체 소스 코드가 포함된 업데이트 버전 앱을 배포 중입니다 그걸 다운받아서 연구하고 수정해 보세요 그리고 연습 삼아 더 발전시켜 보시길 바랍니다 거울 표면에 반복적으로 반사되는 모습을 추가해 보세요 여러분의 작업물이 빨리 보고 싶군요 Metal 3를 이용해 바인딩 없이 작업하기에 완벽한 타이밍이죠 시청해 주셔서 감사합니다
-
-
5:38 - Write argument buffers in Metal 3
// Write argument buffers in Metal 3 struct Mesh { uint64_t normals; // 64-bit uint for constant packed_float3* }; NSUInteger meshArgumentSize = sizeof(struct Mesh); id<MTLBuffer> meshArgumentBuffer = [device newBufferWithLength:meshArgumentSize options:storageMode]; struct Mesh* meshes = (struct Mesh *)(meshArgumentBuffer.contents); meshes->normals = normalBuffer.gpuAddress + normalBufferOffset;
-
6:31 - // Shader struct:
// Shader struct: struct Mesh { constant packed_float3* normals; }; // Host-side struct: struct Mesh { uint64_t normals; };
-
6:53 - Shared struct:
// Shared struct: #if __METAL_VERSION__ #define CONSTANT_PTR(x) constant x* #else #define CONSTANT_PTR(x) uint64_t #endif struct Mesh { CONSTANT_PTR(packed_float3) normals; };
-
7:53 - Write unbounded arrays of resources in Metal 3
// Write unbounded arrays of resources in Metal 3 struct Mesh { uint64_t normals; // 64-bit uint for constant packed_float3* }; NSUInteger meshArgumentSize = sizeof(struct Mesh) * meshes.count; id<MTLBuffer> meshArgumentBuffer = [device newBufferWithLength:meshArgumentSize options:storageMode]; struct Mesh* meshes = (struct Mesh *)(meshArgumentBuffer.contents); for ( NSUInteger i = 0; i < meshes.count; ++i ) { meshes[i].normals = normalBuffers[i].gpuAddress + normalBufferOffsets[i]; }
-
9:03 - Metal shading language: unbounded arrays option 1
// Metal shading language: struct Mesh { constant packed_float3* normals; }; fragment half4 fragmentShader(ColorInOut v [[stage_in]], constant Mesh* meshes [[buffer(0)]] ) { /* determine mesh to read, e.g. geometry_id */ packed_float3 n0 = meshes[ geometry_id ].normals[0]; packed_float3 n1 = meshes[ geometry_id ].normals[1]; packed_float3 n2 = meshes[ geometry_id ].normals[2]; /* interpolate normals and calculate shading */ }
-
9:25 - Metal shading language: unbounded arrays option 2
// Metal shading language: struct Mesh { constant packed_float3* normals; }; struct Scene { constant Mesh* meshes; // mesh array constant Material* materials; // material array }; fragment half4 fragmentShader(ColorInOut v [[stage_in]], constant Scene& scene [[buffer(0)]] ) { /* determine mesh to read, e.g. geometry_id */ packed_float3 n0 = scene.meshes[ geometry_id ].normals[0]; packed_float3 n1 = scene.meshes[ geometry_id ].normals[1]; packed_float3 n2 = scene.meshes[ geometry_id ].normals[2]; /* interpolate normals and calculate shading */ }
-
11:00 - Size and alignment for MTLAccelerationStructure in a MTLHeap
heapAccelerationStructureSizeAndAlignWithDescriptor:
-
13:49 - Store individual indirect resources in NSMutableSet
// Argument buffer loading for (NSUInteger i = 0; i < mesh.submeshes.count; ++i) { Submesh* submesh = mesh.submeshes[i]; id<MTLBuffer> indexBuffer = submesh.indexBuffer; NSArray* textures = submesh.textures; // Copy index buffer into argument buffer submeshAB[i].indices = indexBuffer.gpuAddress; // Copy material textures into argument buffer for (NSUInteger m = 0; m < textures.count; ++m) { submeshAB[i].textures[m] = textures[m].gpuResourceID; } // Remember indirect resources [sceneResources addObject:indexBuffer]; [sceneResources addObjectsFromArray:textures]; }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.