다음을 통해 공유


다중 엔진 동기화

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

GPU 엔진

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

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

다음 이미지는 타이틀이 필요한 경우 엔진 간 동기화를 포함하여 여러 GPU 엔진에서 작업을 예약하는 방법을 보여 줍니다. 엔진 간 종속성이 있는 엔진별 워크로드를 보여 줍니다. 이 예제에서 복사 엔진은 먼저 렌더링에 필요한 일부 기하 도형을 복사합니다. 3D 엔진은 이러한 복사본이 완료될 때까지 대기하고 기하 도형을 통해 사전 전달을 렌더링합니다. 그런 다음 컴퓨팅 엔진에서 사용됩니다. 복사 엔진에 대한 여러 텍스처 복사 작업과 함께 디스패치컴퓨팅 엔진의 결과는 최종 그리기 호출을 위해 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에서 자주(프레임당 약 한 번) 동기화해야 합니다.
  • 데이터 스트리밍 및 업로드. 별도의 복사 큐는 초기 데이터 및 업데이트 리소스의 D3D11 개념을 대체합니다. 애플리케이션은 Direct3D 12 모델의 자세한 내용을 담당하지만 이 책임은 전원과 함께 제공됩니다. 애플리케이션은 업로드 데이터를 버퍼링하는 데 얼마나 많은 시스템 메모리가 투입되는지 제어할 수 있습니다. 앱은 동기화할 시기와 방법(CPU 및 GPU, 차단 및 비차단)을 선택할 수 있으며 진행률을 추적하고 대기 중인 작업의 양을 제어할 수 있습니다.
  • 병렬 처리가 증가했습니다. 애플리케이션은 포그라운드 작업을 위해 별도의 큐가 있는 경우 백그라운드 워크로드(예: 비디오 디코딩)에 더 깊은 큐를 사용할 수 있습니다.

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

동기화 API

디바이스 및 큐

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

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

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

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

번들은 큐에서 사용되지 않으므로 이 형식을 사용하여 큐를 만들 수 없습니다.

울타리

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

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

펜스를 사용하면 현재 펜스 값에 대한 CPU 액세스를 허용하고 CPU 대기 및 신호를 허용합니다.

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

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

애플리케이션은 자체 펜스 값을 설정하며, 좋은 시작점은 프레임당 한 번 울타리를 늘릴 수 있습니다.

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

펜스 API는 강력한 동기화 기능을 제공하지만 문제를 디버그하기 어려울 수 있습니다. 각 펜스는 신호기 간의 경합을 방지하기 위해 하나의 타임라인에서 진행 상황을 나타내는 데만 사용하는 것이 좋습니다.

명령 목록 복사 및 컴퓨팅

명령 목록의 세 가지 유형은 모두 ID3D12GraphicsCommandList 인터페이스를 사용하나 복사 및 컴퓨팅에는 메서드의 하위 집합만 지원됩니다.

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

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

컴퓨팅 명령 목록은 SetPipelineState호출할 때 컴퓨팅 PSO를 설정해야 합니다.

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

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

이 예제에서는 펜스 동기화를 사용하여 큐 pGraphicsQueue그래픽 작업에서 사용되는 큐(pComputeQueue참조)에서 컴퓨팅 작업의 파이프라인을 만드는 방법을 보여 줍니다. 컴퓨팅 및 그래픽 작업은 여러 프레임에서 컴퓨팅 작업의 결과를 소비하는 그래픽 큐로 파이프라인되고 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+ComputeGraphicsLatency작성하기 전에 그래픽 큐가 프레임 N에 대한 데이터 읽기를 완료할 때까지 기다려야 합니다.

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

간접 참조를 방지하는 다른 메커니즘은 데이터의 각 "이름 바꾸기" 버전에 해당하는 여러 명령 목록을 만드는 것입니다. 다음 예제에서는 이전 예제를 확장하면서 이 기술을 사용하여 컴퓨팅 및 그래픽 큐가 더 비동기적으로 실행되도록 합니다.

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

다음 예제에서는 컴퓨팅 큐에서 그래픽을 비동기적으로 렌더링할 수 있습니다. 두 단계 사이에는 고정된 양의 버퍼링된 데이터가 있지만, 이제 그래픽 작업은 독립적으로 진행되며 그래픽 작업이 큐에 대기될 때 CPU에 알려진 컴퓨팅 단계의 가장 up-to날짜 결과를 사용합니다. 이는 그래픽 작업이 다른 원본(예: 사용자 입력)에 의해 업데이트되는 경우에 유용합니다. 그래픽의 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 + CommandListIndex);
            pGraphicsQueue->Signal(pGraphicsFence, FrameNumbers[Stage]);
            break;
        }
        }
    }
}

다중 큐 리소스 액세스

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

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

    요약할 수 있습니다.

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

  • 리소스 상태는 모든 Compute 및 3D 큐에서 공유되지만 다른 큐에서 리소스에 동시에 쓸 수는 없습니다. 여기서 "동시에"는 동기화되지 않음을 의미하며, 일부 하드웨어에서는 동기화되지 않은 실행이 불가능합니다. 다음 규칙이 적용됩니다.

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

Direct3D 12 프로그래밍 가이드

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

Direct3D 12 메모리 관리