多引擎同步處理

大部分的新式 GPU 都包含多個提供特製化功能的獨立引擎。 許多都有一或多個專用複製引擎和計算引擎,通常與 3D 引擎不同。 每個引擎都可以彼此平行執行命令。 Direct3D 12 使用佇列和命令清單,提供 3D、計算和複製引擎的精細存取。

GPU 引擎

下圖顯示標題的 CPU 執行緒,每個執行緒都會填入一或多個複製、計算和 3D 佇列。 3D 佇列可以驅動這三個 GPU 引擎;計算佇列可以驅動計算和複製引擎;和複製佇列只是複製引擎。

當不同的執行緒填入佇列時,無法簡單保證執行順序,因此當標題需要它們時,需要同步處理機制。

將命令傳送至三個佇列的四個執行緒

下圖說明標題如何排程跨多個 GPU 引擎運作,包括必要時的引擎間同步處理:它會顯示具有引擎間相依性的個別引擎工作負載。 在此範例中,複製引擎會先複製轉譯所需的一些幾何。 3D 引擎會等候這些複本完成,並轉譯幾何的預先傳遞。 然後,計算引擎會取用此專案。 計算引擎 分派的結果以及複製引擎上的數個紋理複製作業,會由 3D 引擎取用,以供最終的 Draw 呼叫使用。

複製、圖形和計算引擎通訊

下列虛擬程式碼說明標題如何提交這類工作負載。

// 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方法。

  • ExecuteCommandLists :提交命令清單陣列以供執行。 每個命令清單都是由 ID3D12CommandList所定義。
  • 訊號 :在) GPU 上執行佇列 (達到特定點時,設定柵欄值。
  • 等候 :佇列會等到指定的柵欄到達指定的值為止。

請注意,任何佇列都不會取用套件組合,因此無法使用此類型來建立佇列。

柵欄

多引擎 API 提供明確的 API,以使用柵欄建立和同步處理。 柵欄是由 UINT64 值控制的同步處理建構。 柵欄值是由應用程式所設定。 訊號作業會修改柵欄值,而等候作業會封鎖,直到柵欄達到要求的值或更新值為止。 當柵欄達到特定值時,可以引發事件。

請參閱 ID3D12Fence 介面的方法。

柵欄允許 CPU 存取目前的柵欄值,以及 CPU 等候和訊號。

ID3D12Fence介面上的Signal方法會從 CPU 端更新柵欄。 此更新會立即發生。 ID3D12CommandQueue上的Signal方法會從 GPU 端更新柵欄。 此更新會在命令佇列上所有其他作業完成之後發生。

多引擎設定中的所有節點都可以讀取並回應到達正確值的任何柵欄。

應用程式會設定自己的柵欄值,良好的起點可能會在每個畫面上增加一次柵欄。

柵欄可能會反轉。 這表示柵欄值不需要只遞增。 如果 號作業在兩個不同的命令佇列上排入佇列,或者如果兩個 CPU 執行緒在柵欄上呼叫 Signal ,則可能會有競爭來判斷哪一個 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 queue 是一個類型類別,Copy 是第二種類型類別。 因此,有一個 3D 佇列上NON_PIXEL_SHADER_RESOURCE狀態的資源可以用於任何 3D 或計算佇列的該狀態,但受限於需要序列化大部分寫入的同步處理需求。 在兩個類型類別之間共用的資源狀態 (COPY_SOURCE和COPY_DEST) 會被視為每個類型類別的不同狀態。 因此,如果資源轉換至複製佇列上的COPY_DEST,則無法從 3D 或計算佇列存取,反之亦然。

    摘要說明。

    • 佇列 「object」 是任何單一佇列。
    • 佇列「類型」是下列三個其中一項:計算、3D 和複製。
    • 佇列「類型類別」是下列其中一個:計算/3D 和複製。
  • COPY 旗標 (COPY_DEST和COPY_SOURCE) 做為初始狀態,代表 3D/Compute 類型類別中的狀態。 若要一開始在複製佇列上使用資源,它應該會以 COMMON 狀態啟動。 COMMON 狀態可用於使用隱含狀態轉換之複製佇列上的所有使用方式。 

  • 雖然資源狀態會跨所有計算和 3D 佇列共用,但不允許在不同的佇列上同時寫入資源。 此處的「同時」表示某些硬體上無法進行未同步處理,表示無法同步處理執行。 適用下列規則。

    • 一次只能有一個佇列寫入資源。
    • 只要多個佇列不會讀取寫入器修改的位元組,就可以從資源讀取, (同時寫入的位元組會產生未定義的結果) 。
    • 在寫入之後,必須先使用柵欄進行同步處理,才能讓另一個佇列讀取寫入的位元組或進行任何寫入存取。
  • 正在呈現的後端緩衝區必須處於 Direct3D 12_RESOURCE_STATE_COMMON 狀態。 

Direct3D 12 程式設計指南

使用資源屏障同步處理 Direct3D 12 中的資源狀態

Direct3D 12 中的記憶體管理