다음을 통해 공유


다중 엔진 동기화

대부분의 최신 GPU에는 특수 기능을 제공하는 여러 개의 독립 엔진이 포함되어 있습니다. 대부분의 경우 하나 이상의 전용 복사 엔진과 함께 일반적으로 3D 엔진과는 별개인 컴퓨팅 엔진이 있습니다. 이러한 각 엔진은 서로 병렬로 명령을 실행할 수 있습니다. Direct3D 12는 큐 및 명령 목록을 사용하여 3D, 컴퓨팅 및 복사 엔진에 대한 세분화된 액세스를 제공합니다.

GPU 엔진

다음 다이어그램은 각각 하나 이상의 복사본, 컴퓨팅 및 3D 큐를 채우는 타이틀의 CPU 스레드를 보여 줍니다. 3D 큐는 세 개의 GPU 엔진을 모두 구동할 수 있습니다. 컴퓨팅 큐는 컴퓨팅 및 복사 엔진을 구동할 수 있습니다. 및 복사 큐는 단순히 복사 엔진입니다.

서로 다른 스레드가 큐를 채우므로 실행 순서를 간단하게 보장할 수 없으므로 타이틀에 필요한 경우 동기화 메커니즘이 필요합니다.

네 개의 스레드가 세 개의 큐로 명령을 전송

다음 이미지는 필요한 경우 엔진 간 동기화를 포함하여 타이틀이 여러 GPU 엔진에서 작업을 예약하는 방법을 보여줍니다. 엔진 간 종속성이 있는 엔진별 워크로드를 보여줍니다. 이 예제에서는 복사 엔진이 먼저 렌더링에 필요한 일부 기하 도형을 복사합니다. 3D 엔진은 이러한 복사가 완료될 때까지 대기하고 기하 도형에 대한 pre-pass를 렌더링합니다. 그런 다음, 컴퓨팅 엔진에서 사용됩니다. 복사 엔진의 여러 질감 복사 작업과 함께 컴퓨팅 엔진의 결과 디스패치는 마지막 그리기 호출에 대해 3D 엔진에서 사용됩니다.

통신하는 복사, 그래픽 및 컴퓨팅 엔진

다음 의사 코드에서는 타이틀이 그러한 워크로드를 제출하는 방법을 설명합니다.

// Get per-engine contexts. Note that multiple queues may be exposed
// per engine, however that design is not reflected here.
copyEngine = device->GetCopyEngineContext();
renderEngine = device->GetRenderEngineContext();
computeEngine = device->GetComputeEngineContext();
copyEngine->CopyResource(geometry, ...); // copy geometry
copyEngine->Signal(copyFence, 101);
copyEngine->CopyResource(tex1, ...); // copy textures
copyEngine->CopyResource(tex2, ...); // copy more textures
copyEngine->CopyResource(tex3, ...); // copy more textures
copyEngine->CopyResource(tex4, ...); // copy more textures
copyEngine->Signal(copyFence, 102);
renderEngine->Wait(copyFence, 101); // geometry copied
renderEngine->Draw(); // pre-pass using geometry only into rt1
renderEngine->Signal(renderFence, 201);
computeEngine->Wait(renderFence, 201); // prepass completed
computeEngine->Dispatch(); // lighting calculations on pre-pass (using rt1 as SRV)
computeEngine->Signal(computeFence, 301);
renderEngine->Wait(computeFence, 301); // lighting calculated into buf1
renderEngine->Wait(copyFence, 102); // textures copied
renderEngine->Draw(); // final render using buf1 as SRV, and tex[1-4] SRVs

다음 의사 코드에서는 링 버퍼를 통해 힙과 같은 메모리 할당을 수행하기 위해 복사 엔진과 3D 엔진 간의 동기화를 설명합니다. 타이틀에는 병렬성을 극대화하는 것(큰 버퍼를 통해)과 메모리 소비 및 대기 시간을 줄이는 것(작은 버퍼를 통해) 사이에서 적절한 균형을 선택할 수 있는 유연성이 있습니다.

device->CreateBuffer(&ringCB);
for(int i=1;i++){
  if(i > length) copyEngine->Wait(fence1, i - length);
  copyEngine->Map(ringCB, value%length, WRITE, pData); // copy new data
  copyEngine->Signal(fence2, i);
  renderEngine->Wait(fence2, i);
  renderEngine->Draw(); // draw using copied data
  renderEngine->Signal(fence1, i);
}

// example for length = 3:
// copyEngine->Map();
// copyEngine->Signal(fence2, 1); // fence2 = 1  
// copyEngine->Map();
// copyEngine->Signal(fence2, 2); // fence2 = 2
// copyEngine->Map();
// copyEngine->Signal(fence2, 3); // fence2 = 3
// copy engine has exhausted the ring buffer, so must wait for render to consume it
// copyEngine->Wait(fence1, 1); // fence1 == 0, wait
// renderEngine->Wait(fence2, 1); // fence2 == 3, pass
// renderEngine->Draw();
// renderEngine->Signal(fence1, 1); // fence1 = 1, copy engine now unblocked
// renderEngine->Wait(fence2, 2); // fence2 == 3, pass
// renderEngine->Draw();
// renderEngine->Signal(fence1, 2); // fence1 = 2
// renderEngine->Wait(fence2, 3); // fence2 == 3, pass
// renderEngine->Draw();
// renderEngine->Signal(fence1, 3); // fence1 = 3
// now render engine is starved, and so must wait for the copy engine
// renderEngine->Wait(fence2, 4); // fence2 == 3, wait

다중 엔진 시나리오

Direct3D 12를 사용하면 예기치 않은 동기화 지연으로 인한 비효율성이 실수로 발생하지 않도록 할 수 있습니다. 또한 필요한 동기화를 보다 확실하게 확인할 수 있는 더 높은 수준에서 동기화를 도입할 수 있습니다. 다중 엔진에서 다루는 두 번째 문제는 고가의 운영이 더 명확히 되는 것으로, 여러 커널 컨텍스트 간의 동기화로 인해 일반적으로 비용이 많이 드는 3D와 비디오 간의 전환이 포함됩니다.

특히 Direct3D 12를 사용하여 다음 시나리오를 해결할 수 있습니다.

  • 비동기 및 낮은 우선 순위 GPU 작업. 이를 통해 한 GPU 스레드가 차단 없이 다른 비동기 스레드의 결과를 사용할 수 있도록 하는 우선 순위가 낮은 GPU 작업과 원자 작동을 동시에 실행할 수 있습니다.
  • 우선 순위가 높은 컴퓨팅 작업. 백그라운드 컴퓨팅을 사용하면 우선 순위가 높은 컴퓨팅 작업을 소량 수행하기 위해 3D 렌더링을 중단할 수 있습니다. 이 작업의 결과는 CPU의 추가 처리를 위해 조기에 얻을 수 있습니다.
  • 백그라운드 컴퓨팅 작업. 컴퓨팅 워크로드에 대한 별도의 낮은 우선 순위 큐를 사용하면 애플리케이션은 예비 GPU 주기를 활용하여 기본 렌더링(또는 기타) 작업에 부정적인 영향을 끼치지 않고도 백그라운드 계산을 수행할 수 있습니다. 백그라운드 작업에는 리소스의 압출 풀기 또는 시뮬레이션이나 가속 구조의 업데이트가 포함될 수 있습니다. 백그라운드 작업은 포그라운드 작업이 지연되거나 느려지지 않도록 CPU에서 간헐적으로(프레임당 약 1회) 동기화해야 합니다.
  • 데이터 스트리밍 및 업로드. 별도의 복사 큐는 초기 데이터와 업데이트 리소스의 D3D11 개념을 대체합니다. 애플리케이션은 Direct3D 12 모델의 자세한 내용을 담당하지만 이 책임은 전원과 함께 제공됩니다. 애플리케이션은 업로드 데이터 버퍼링에 사용되는 시스템 메모리 양을 제어할 수 있습니다. 앱은 동기화할 시기와 방법(CPU 대 GPU, 차단 대 비차단)을 선택하고, 진행률을 추적하고, 대기 중인 작업의 양을 제어할 수 있습니다.
  • 향상된 병렬 처리. 애플리케이션은 배경 워크로드(예: 비디오 디코딩)를 위해 포그라운드 작업에 별도의 큐가 있는 경우 더 심층적인 큐를 사용할 수 있습니다.

Direct3D 12에서 명령 큐의 개념은 애플리케이션에서 제출한 대략적인 직렬 작업 시퀀스의 API 표현입니다. 장벽과 기타 기술을 통해 이 작업을 파이프라인에서나 순서 없이 실행할 수 있지만, 애플리케이션은 하나의 완료 타임라인만 볼 수 있습니다. 이는 D3D11의 직접 컨텍스트에 해당합니다.

동기화 API

디바이스 및 큐

Direct3D 12 디바이스에는 다양한 형식과 우선 순위의 명령 큐를 만들고 검색하는 메서드가 있습니다. 대부분의 애플리케이션은 다른 구성 요소의 공유 사용을 허용하기 때문에 기본 명령 큐를 사용해야 합니다. 추가 동시성 요건이 있는 애플리케이션은 추가 큐를 생성할 수 있습니다. 큐는 사용하는 명령 목록 유형에 의해 지정됩니다.

ID3D12Device의 다음 생성 메서드를 참조하세요.

모든 유형의 큐(3D, 컴퓨팅 및 복사)는 동일한 인터페이스를 공유하며 모든 명령 목록을 기반으로 합니다.

ID3D12CommandQueue의 다음 메서드를 참조하세요.

  • ExecuteCommandLists : 실행을 위해 명령 목록의 배열을 제출합니다. 각 명령 목록은 ID3D12CommandList에서 정의됩니다.
  • Signal : 큐(GPU에서 실행)가 특정 시점에 도달하는 경우 펜스 값을 설정합니다.
  • Wait : 큐는 지정된 펜스가 지정된 값에 도달할 때까지 대기합니다.

번들은 어떠한 큐에서도 사용되지 않으므로 이 유형은 큐를 생성하는 데 사용할 수 없습니다.

펜스

다중 엔진 API는 펜스를 사용하여 생성 및 동기화할 수 있는 명시적인 API를 제공합니다. 펜스는 UINT64 값으로 제어되는 동기화 구문입니다. 펜스 값은 애플리케이션에 의해 설정됩니다. 신호 작업은 펜스가 요청된 값 이상에 도달할 때까지 펜스 값과 대기 작업 블록을 수정합니다. 펜스가 특정 값에 도달하면 이벤트가 발생될 수 있습니다.

ID3D12Fence 인터페이스의 메서드를 참조하세요.

펜스는 CPU는 현재 펜스 값에 액세스할 수 있도록 하며 CPU는 대기하고 알립니다.

ID3D12Fence 인터페이스의 Signal 메서드는 CPU 쪽에서 펜스를 업데이트합니다. 이 업데이트는 즉시 발생합니다. ID3D12CommandQueueSignal 메서드는 GPU 쪽에서 펜스를 업데이트합니다. 이 업데이트는 명령 큐의 다른 모든 작업이 완료된 후에 발생합니다.

다중 엔진 설정에서 모든 노드는 올바른 값에 도달하는 모든 펜스를 읽고 대응할 수 있습니다.

애플리케이션은 자체 펜스 값을 설정하는데, 프레임당 한 번씩 펜스를 증가시키는 것이 좋은 출발점이 될 수 있습니다.

울타리가 되감수 있습니다. 즉, 펜스 값만 증분할 필요는 없습니다. 신호 작업이 서로 다른 두 명령 큐에 큐에 추가되거나 두 개의 CPU 스레드가 모두 펜스에서 Signal을 호출하는 경우 신호가 마지막으로 완료되는 것을 결정하기 위한 경합이 있을 수 있으므로 유지되는 펜스 값이 될 수 있습니다. 펜스가 다시 해제되면 새 대기( SetEventOnCompletion 요청 포함)가 새 하위 펜스 값과 비교되므로 펜스 값이 이전에 이를 만족시킬 만큼 충분히 높았던 경우에도 충족되지 않을 수 있습니다. 경합이 발생하는 경우 미해결 대기를 충족하는 값과 그렇지 않은 더 낮은 값 사이에는 나중에 유지되는 값에 관계없이 대기가 충족 됩니다 .

펜스 API는 강력한 동기화 기능을 제공하지만, 잠재적으로 디버그하기 어려운 문제를 만들 수 있습니다. 각 펜스는 신호기 간의 경합을 방지하기 위해 한 타임라인 진행률을 나타내는 데만 사용하는 것이 좋습니다.

명령 목록 복사 및 컴퓨팅

세 가지 유형의 명령 목록은 ID3D12GraphicsCommandList 인터페이스를 사용하지만, 복사 및 컴퓨팅에 대해 메서드의 하위 세트만 지원됩니다.

복사 및 컴퓨팅 명령 목록은 다음 메서드를 사용할 수 있습니다.

컴퓨팅 명령 목록은 다음 메서드를 사용할 수도 있습니다.

컴퓨팅 명령 목록은 SetPipelineState를 호출하는 경우 컴퓨팅 PSO를 설정해야 합니다.

번들은 컴퓨팅 또는 복사 명령 목록 또는 큐와 함께 사용할 수 없습니다.

파이프라인된 컴퓨팅 및 그래픽 예제

이 예제에서는 펜스 동기화를 사용하여 큐에서 그래픽 작업에서 사용되는 큐(참조) pComputeQueue에서 pGraphicsQueue컴퓨팅 작업의 파이프라인을 만드는 방법을 보여 줍니다. 컴퓨팅 및 그래픽 작업은 여러 프레임의 컴퓨팅 작업 결과를 사용하는 그래픽 큐와 함께 파이프라인되고 CPU 이벤트는 전체 큐에 대기 중인 총 작업을 제한하는 데 사용됩니다.

void PipelinedComputeGraphics()
{
    const UINT CpuLatency = 3;
    const UINT ComputeGraphicsLatency = 2;

    HANDLE handle = CreateEvent(nullptr, FALSE, FALSE, nullptr);

    UINT64 FrameNumber = 0;

    while (1)
    {
        if (FrameNumber > ComputeGraphicsLatency)
        {
            pComputeQueue->Wait(pGraphicsFence,
                FrameNumber - ComputeGraphicsLatency);
        }

        if (FrameNumber > CpuLatency)
        {
            pComputeFence->SetEventOnFenceCompletion(
                FrameNumber - CpuLatency,
                handle);
            WaitForSingleObject(handle, INFINITE);
        }

        ++FrameNumber;

        pComputeQueue->ExecuteCommandLists(1, &pComputeCommandList);
        pComputeQueue->Signal(pComputeFence, FrameNumber);
        if (FrameNumber > ComputeGraphicsLatency)
        {
            UINT GraphicsFrameNumber = FrameNumber - ComputeGraphicsLatency;
            pGraphicsQueue->Wait(pComputeFence, GraphicsFrameNumber);
            pGraphicsQueue->ExecuteCommandLists(1, &pGraphicsCommandList);
            pGraphicsQueue->Signal(pGraphicsFence, GraphicsFrameNumber);
        }
    }
}

이 파이프라인을 지원하려면 컴퓨팅 큐에서 그래픽 큐로 전달되는 여러 데이터 복사본의 버퍼 ComputeGraphicsLatency+1 가 있어야 합니다. 명령 목록은 UAV 및 간접을 사용하여 버퍼에서 적절한 데이터의 “버전”을 읽고 써야 합니다. 컴퓨팅 큐는 프레임 N에 대한 데이터에서 그래픽 큐의 읽기가 완료될 때까지 기다려야 프레임 N+ComputeGraphicsLatency를 쓸 수 있습니다.

CPU를 기준으로 작동하는 컴퓨팅 큐의 양은 필요한 버퍼링 양에 직접 의존하지는 않지만 사용 가능한 버퍼 공간의 양을 초과하여 GPU 작업을 큐에 대기하는 것은 가치가 떨어집니다.

간접을 피하기 위한 대안적인 메커니즘은 각 데이터의 “이름이 변경된” 버전에 해당하는 다중 명령 목록을 만드는 것입니다. 다음 예제에서는 컴퓨팅 및 그래픽 큐가 더 비동기적으로 실행되도록 이전 예제를 확장하는 동안 이 기술을 사용합니다.

비동기 컴퓨팅 및 그래픽 예제

이 다음 예제에서는 그래픽이 컴퓨팅 큐에서 비동기적으로 렌더링되도록 허용합니다. 두 단계 사이에는 여전히 고정된 양의 버퍼링된 데이터가 있지만, 지금은 그래픽 작업이 독립적으로 진행되며 그래픽 작업이 대기 중일 때 CPU에 알려진 컴퓨팅 단계의 가장 최신 결과를 사용합니다. 이는 그래픽 작업이 다른 소스(예: 사용자 입력)에 의해 업데이트되는 경우에 유용합니다. 한 번에 ComputeGraphicsLatency 프레임의 그래픽 작업을 허용하려면 명령 목록이 여러 개 있어야 하며, UpdateGraphicsCommandList 함수는 명령 목록을 업데이트하여 최신 입력 데이터를 포함하고 적절한 버퍼에서 컴퓨팅 데이터를 읽습니다.

컴퓨팅 큐는 여전히 파이프 버퍼로 그래픽 큐가 완료될 때까지 기다려야 하지만, 그래픽 읽기 컴퓨팅 작업의 진행률과 그래픽 진행률을 전반적으로 추적할 수 있도록 세 번째 펜스(pGraphicsComputeFence)가 도입됩니다. 이는 현재 연속된 그래픽 프레임이 동일한 컴퓨팅 결과에서 읽을 수 있거나 컴퓨팅 결과를 건너뛸 수 있다는 사실을 반영합니다. 더 효율적이되 약간 더 복잡한 디자인에서는 단일 그래픽 펜스만 사용하고 각 그래픽 프레임에서 사용되는 컴퓨팅 프레임에 대한 매핑을 저장할 것입니다.

void AsyncPipelinedComputeGraphics()
{
    const UINT CpuLatency{ 3 };
    const UINT ComputeGraphicsLatency{ 2 };

    // The compute fence is at index 0; the graphics fence is at index 1.
    ID3D12Fence* rgpFences[]{ pComputeFence, pGraphicsFence };
    HANDLE handles[2];
    handles[0] = CreateEvent(nullptr, FALSE, TRUE, nullptr);
    handles[1] = CreateEvent(nullptr, FALSE, TRUE, nullptr);
    UINT FrameNumbers[]{ 0, 0 };

    ID3D12GraphicsCommandList* rgpGraphicsCommandLists[CpuLatency];
    CreateGraphicsCommandLists(ARRAYSIZE(rgpGraphicsCommandLists),
        rgpGraphicsCommandLists);

    // Graphics needs to wait for the first compute frame to complete; this is the
    // only wait that the graphics queue will perform.
    pGraphicsQueue->Wait(pComputeFence, 1);

    while (true)
    {
        for (auto i = 0; i < 2; ++i)
        {
            if (FrameNumbers[i] > CpuLatency)
            {
                rgpFences[i]->SetEventOnCompletion(
                    FrameNumbers[i] - CpuLatency,
                    handles[i]);
            }
            else
            {
                ::SetEvent(handles[i]);
            }
        }


        auto WaitResult = ::WaitForMultipleObjects(2, handles, FALSE, INFINITE);
        if (WaitResult > WAIT_OBJECT_0 + 1) continue;
        auto Stage = WaitResult - WAIT_OBJECT_0;
        ++FrameNumbers[Stage];

        switch (Stage)
        {
        case 0:
        {
            if (FrameNumbers[Stage] > ComputeGraphicsLatency)
            {
                pComputeQueue->Wait(pGraphicsComputeFence,
                    FrameNumbers[Stage] - ComputeGraphicsLatency);
            }
            pComputeQueue->ExecuteCommandLists(1, &pComputeCommandList);
            pComputeQueue->Signal(pComputeFence, FrameNumbers[Stage]);
            break;
        }
        case 1:
        {
            // Recall that the GPU queue started with a wait for pComputeFence, 1
            UINT64 CompletedComputeFrames = min(1,
                pComputeFence->GetCompletedValue());
            UINT64 PipeBufferIndex =
                (CompletedComputeFrames - 1) % ComputeGraphicsLatency;
            UINT64 CommandListIndex = (FrameNumbers[Stage] - 1) % CpuLatency;
            // Update graphics command list based on CPU input and using the appropriate
            // buffer index for data produced by compute.
            UpdateGraphicsCommandList(PipeBufferIndex,
                rgpGraphicsCommandLists[CommandListIndex]);

            // Signal *before* new rendering to indicate what compute work
            // the graphics queue is DONE with
            pGraphicsQueue->Signal(pGraphicsComputeFence, CompletedComputeFrames - 1);
            pGraphicsQueue->ExecuteCommandLists(1,
                rgpGraphicsCommandLists + PipeBufferIndex);
            pGraphicsQueue->Signal(pGraphicsFence, FrameNumbers[Stage]);
            break;
        }
        }
    }
}

다중 큐 리소스 액세스

둘 이상의 큐의 리소스에 액세스하려면 애플리케이션은 다음 규칙을 준수해야 합니다.

  • 리소스 액세스( Direct3D 12_RESOURCE_STATES 참조)는 큐 개체가 아닌 큐 유형 클래스에 따라 결정됩니다. 큐에는 두 가지 형식 클래스가 있습니다. Compute/3D 큐는 하나의 형식 클래스이고 Copy는 두 번째 형식 클래스입니다. 따라서 하나의 3D 큐에서 NON_PIXEL_SHADER_RESOURCE 상태에 장벽이 있는 리소스는 대부분의 쓰기를 직렬화해야 하는 동기화 요구 사항에 따라 모든 3D 또는 컴퓨팅 큐의 해당 상태에서 사용할 수 있습니다. 두 형식 클래스(COPY_SOURCE 및 COPY_DEST) 간에 공유되는 리소스 상태는 각 형식 클래스에 대해 서로 다른 상태로 간주됩니다. 따라서 리소스가 복사 큐의 COPY_DEST 전환되는 경우 3D 또는 Compute 큐에서 복사 대상으로 액세스할 수 없으며 그 반대의 경우도 마찬가지입니다.

    요약할 수 있습니다.

    • “object” 큐는 임의의 단일 큐입니다.
    • 큐 "type"은 컴퓨팅, 3D 및 복사의 세 가지 중 하나입니다.
    • 큐 "type 클래스"는 Compute/3D 및 Copy의 두 가지 중 하나입니다.
  • 초기 상태로 사용되는 COPY 플래그(COPY_DEST 및 COPY_SOURCE)는 3D/Compute 형식 클래스의 상태를 나타냅니다. 복사 큐에 리소스를 처음 사용하기 위해서는 COMMON 상태에서 시작해야 합니다. COMMON 상태는 암시적 상태 전환을 사용하여 복사 큐의 모든 사용량에 사용할 수 있습니다. 

  • 리소스 상태는 모든 컴퓨팅 및 3D 큐에서 공유되지만, 다른 큐에서 동시에 리소스에 쓰는 것은 허용되지 않습니다. 여기서 “동시에”란 비동기화됨을 의미하며, 일부 하드웨어에서는 비동기화된 실행이 불가능하다는 점을 나타냅니다. 다음 규칙이 적용됩니다.

    • 하나의 큐만 한 번에 하나의 리소스를 쓸 수 있습니다.
    • 작성자가 수정 중인 바이트를 읽지 않는 한, 여러 큐는 리소스를 읽을 수 있습니다(동시에 작성된 바이트를 읽으면 정의되지 않은 결과가 생성됨).
    • 다른 큐가 작성된 바이트를 읽거나 쓰기 액세스를 수행하려면 쓰기 후 펜스를 사용하여 동기화해야 합니다.
  • 표시되는 백 버퍼는 Direct3D 12_RESOURCE_STATE_COMMON 상태여야 합니다. 

Direct3D 12 프로그래밍 가이드

Direct3D 12에서 리소스 장벽을 사용하여 리소스 상태 동기화

Direct3D 12의 메모리 관리