Synkronisering med flera motorer

De flesta moderna GPU:er innehåller flera oberoende motorer som tillhandahåller specialiserade funktioner. Många har en eller flera dedikerade kopieringsmotorer och en beräkningsmotor som vanligtvis skiljer sig från 3D-motorn. Var och en av dessa motorer kan köra kommandon parallellt med varandra. Direct3D 12 ger detaljerad åtkomst till 3D-, beräknings- och kopieringsmotorerna med hjälp av köer och kommandolistor.

GPU-motorer

Följande diagram visar en rubriks CPU-trådar, som var och en fyller i en eller flera av kopierings-, beräknings- och 3D-köerna. 3D-kön kan köra alla tre GPU-motorerna. beräkningskö kan driva beräknings- och kopieringsmotorerna. och kopieringskön helt enkelt kopieringsmotorn.

Eftersom de olika trådarna fyller köerna kan det inte finnas någon enkel garanti för körningsordningen, därav behovet av synkroniseringsmekanismer – när rubriken kräver dem.

fyra trådar som skickar kommandon till tre köer

Följande bild illustrerar hur en rubrik kan schemalägga arbete mellan flera GPU-motorer, inklusive synkronisering mellan motorer där det behövs: den visar arbetsbelastningar per motor med beroenden mellan motorer. I det här exemplet kopierar kopieringsmotorn först den geometri som krävs för återgivning. 3D-motorn väntar på att dessa kopior ska slutföras och renderar en förpassning över geometrin. Detta förbrukas sedan av beräkningsmotorn. Resultatet av beräkningsmotorn Dispatch, tillsammans med flera texturkopieåtgärder på kopieringsmotorn, förbrukas av 3D-motorn för det sista Draw-anropet.

kopierings-, grafik- och beräkningsmotorer som kommunicerar

Följande pseudokod visar hur en rubrik kan skicka en sådan arbetsbelastning.

// 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

Följande pseudokod illustrerar synkroniseringen mellan kopierings- och 3D-motorerna för att utföra heapliknande minnesallokering via en ringbuffert. Rubriker har flexibiliteten att välja rätt balans mellan att maximera parallelliteten (via en stor buffert) och minska minnesförbrukningen och svarstiden (via en liten buffert).

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

Scenarier med flera motorer

Med Direct3D 12 kan du undvika att oavsiktligt stöta på ineffektivitet som orsakas av oväntade synkroniseringsfördröjningar. Du kan också introducera synkronisering på en högre nivå där den nödvändiga synkroniseringen kan fastställas med större säkerhet. Ett andra problem som adresserar flera motorer är att göra dyra åtgärder mer explicita, vilket inkluderar övergångar mellan 3D och video som traditionellt var kostsamma på grund av synkronisering mellan flera kernelkontexter.

I synnerhet kan följande scenarier hanteras med Direct3D 12.

  • Asynkront och lågt GPU-arbete. Detta möjliggör samtidig körning av GPU-arbete med låg prioritet och atomiska åtgärder som gör att en GPU-tråd kan använda resultatet av en annan osynkroniserad tråd utan blockering.
  • Beräkningsarbete med hög prioritet. Med bakgrundsberäkning är det möjligt att avbryta 3D-återgivning för att utföra en liten mängd beräkningsarbete med hög prioritet. Resultatet av det här arbetet kan hämtas tidigt för ytterligare bearbetning av processorn.
  • Bakgrundsberäkningsarbete. En separat kö med låg prioritet för beräkningsarbetsbelastningar gör att ett program kan använda extra GPU-cykler för att utföra bakgrundsberäkning utan negativ inverkan på de primära återgivningsuppgifterna (eller andra). Bakgrundsuppgifter kan omfatta dekomprimering av resurser eller uppdatering av simuleringar eller accelerationsstrukturer. Bakgrundsaktiviteter bör synkroniseras på processorn sällan (ungefär en gång per bildruta) för att undvika att förgrundsarbetet stoppas eller saktas ned.
  • Strömma och ladda upp data. En separat kopieringskö ersätter begreppen D3D11 för inledande data och uppdatering av resurser. Även om programmet ansvarar för mer information i Direct3D 12-modellen, kommer det här ansvaret med kraft. Programmet kan styra hur mycket systemminne som ägnas åt buffring av uppladdningsdata. Appen kan välja när och hur (CPU vs GPU, blockering eller icke-blockering) ska synkroniseras och kan spåra förloppet och kontrollera mängden köat arbete.
  • Ökad parallellitet. Program kan använda djupare köer för bakgrundsarbetsbelastningar (t.ex. videoavkodning) när de har separata köer för förgrundsarbete.

I Direct3D 12 är begreppet kommandokö API-representationen av en ungefär seriell sekvens av arbete som skickats av programmet. Hinder och andra tekniker gör att det här arbetet kan köras i en pipeline eller i fel ordning, men programmet ser bara en tidslinje för slutförande. Detta motsvarar den omedelbara kontexten i D3D11.

Synkroniserings-API:er

Enheter och köer

Direct3D 12-enheten har metoder för att skapa och hämta kommandoköer av olika typer och prioriteringar. De flesta program bör använda standardkommandoköerna eftersom dessa tillåter delad användning av andra komponenter. Program med ytterligare samtidighetskrav kan skapa ytterligare köer. Köer anges av den kommandolistetyp som de använder.

Se följande metoder för att skapa ID3D12Enhet.

Köer av alla typer (3D, beräkning och kopiering) delar samma gränssnitt och är alla kommandolistbaserade.

Se följande metoder för ID3D12CommandQueue.

  • ExecuteCommandLists : skickar en matris med kommandolistor för körning. Varje kommandolista som definieras av ID3D12CommandList.
  • Signal : anger ett stängselvärde när kön (körs på GPU:n) når en viss punkt.
  • Vänta : kön väntar tills det angivna stängslet når det angivna värdet.

Observera att paket inte används av några köer och därför inte kan den här typen användas för att skapa en kö.

Staket

API:et för flera motorer innehåller explicita API:er för att skapa och synkronisera med hjälp av staket. Ett staket är en synkroniseringskonstruktion som styrs av ett UINT64-värde. Stängselvärden anges av programmet. En signalåtgärd ändrar stängselvärdet och en väntande åtgärd blockerar tills stängslet har nått det begärda värdet eller större. En händelse kan utlöses när ett staket når ett visst värde.

Se metoderna för ID3D12Fence-gränssnittet.

Stängsel ger CPU-åtkomst till det aktuella stängselvärdet och CPU-väntetider och signaler.

Metoden SignalID3D12Fence-gränssnittet uppdaterar ett staket från CPU-sidan. Den här uppdateringen sker omedelbart. Metoden SignalID3D12CommandQueue uppdaterar ett staket från GPU-sidan. Den här uppdateringen inträffar när alla andra åtgärder i kommandokön har slutförts.

Alla noder i en installation med flera motorer kan läsa och reagera på alla stängsel som når rätt värde.

Program anger sina egna stängselvärden, en bra utgångspunkt kan vara att öka ett staket en gång per bildruta.

Ett stängsel kan. Det innebär att stängselvärdet inte bara behöver ökas. Om en Signal-åtgärd placeras i två olika kommandoköer, eller om två CPU-trådar båda anropar Signal på ett staket, kan det finnas en kapplöpning för att avgöra vilken Signal slutförs sist och därför vilket stängselvärde som kommer att finnas kvar. Om ett staket återskapas jämförs alla nya väntetider (inklusive SetEventOnCompletion begäranden) mot det nya lägre stängselvärdet och kanske därför inte uppfylls, även om stängselvärdet tidigare hade varit tillräckligt högt för att uppfylla dem. Om en tävling inträffar, mellan ett värde som uppfyller en utestående väntan och ett lägre värde som inte kommer att göra det, kommer väntetiden uppfyllas oavsett vilket värde som återstår efteråt.

Stängsel-API:erna ger kraftfulla synkroniseringsfunktioner men kan skapa potentiellt svåra att felsöka problem. Vi rekommenderar att varje stängsel endast används för att indikera förloppet på en tidslinje för att förhindra tävlingar mellan signaler.

Kopiera och beräkna kommandolistor

Alla tre typerna av kommandolistan använder gränssnittet ID3D12GraphicsCommandList, men endast en delmängd av metoderna stöds för kopiering och beräkning.

Kopiera och beräkna kommandolistor kan använda följande metoder.

Beräkningskommandolistor kan också använda följande metoder.

Beräkningskommandolistor måste ange en PSO för beräkning när du anropar SetPipelineState.

Paket kan inte användas med beräknings- eller kopieringskommandolistor eller köer.

Exempel på pipelined beräkning och grafik

Det här exemplet visar hur stängselsynkronisering kan användas för att skapa en pipeline för beräkningsarbete i en kö (som refereras av pComputeQueue) som används av grafikarbete i kön pGraphicsQueue. Beräknings- och grafikarbetet pipelines med grafikkön som förbrukar resultatet av beräkningsarbete från flera ramar tillbaka, och en CPU-händelse används för att begränsa det totala antalet arbetsköer totalt sett.

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);
        }
    }
}

För att stödja den här pipeliningen måste det finnas en buffert med ComputeGraphicsLatency+1 olika kopior av data som skickas från beräkningskön till grafikkön. Kommandolistorna måste använda UAV:er och indirection för att läsa och skriva från lämplig "version" av data i bufferten. Beräkningskö måste vänta tills grafikkön har lästs klart från data för bildruta N innan den kan skriva ram N+ComputeGraphicsLatency.

Observera att mängden beräkningskö som fungerade i förhållande till processorn inte är direkt beroende av mängden buffring som krävs, men att köa GPU-arbete utöver mängden tillgängligt buffertutrymme är mindre värdefullt.

En alternativ mekanism för att undvika indirektion skulle vara att skapa flera kommandolistor som motsvarar var och en av de "omdöpta" versionerna av data. I nästa exempel används den här tekniken samtidigt som föregående exempel utökas så att beräknings- och grafikköerna kan köras mer asynkront.

Asynkron beräkning och grafikexempel

I nästa exempel kan grafik återges asynkront från beräkningskö. Det finns fortfarande en fast mängd buffrade data mellan de två stegen, men nu fortsätter grafikarbetet oberoende av varandra och använder det mest up-to- datumresultatet av beräkningssteget som det kallas på PROCESSORn när grafikarbetet placeras i kö. Detta skulle vara användbart om grafikarbetet uppdaterades av en annan källa, till exempel användarindata. Det måste finnas flera kommandolistor för att de ComputeGraphicsLatency bildrutorna i grafikarbetet ska kunna köras i taget, och funktionen UpdateGraphicsCommandList representerar uppdatering av kommandolistan så att den innehåller de senaste indata och läser från beräkningsdata från lämplig buffert.

Beräkningskö måste fortfarande vänta tills grafikkön har slutförts med rörbuffertarna, men ett tredje staket (pGraphicsComputeFence) introduceras så att förloppet för grafikläsning av beräkningsarbete jämfört med grafikförloppet i allmänhet kan spåras. Detta återspeglar det faktum att grafikramar i följd nu kan läsas från samma beräkningsresultat eller hoppa över ett beräkningsresultat. En mer effektiv men lite mer komplicerad design skulle bara använda det enda grafikstaketet och lagra en mappning till de beräkningsramar som används av varje grafikram.

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;
        }
        }
    }
}

Resursåtkomst för flera köer

För att få åtkomst till en resurs i mer än en kö måste ett program följa följande regler.

  • Resursåtkomst (se Direct3D-12_RESOURCE_STATES) bestäms av kötypklass, inte köobjekt. Det finns två typklasser av kön: Beräkning/3D-kö är en typklass, Kopiera är en andra typklass. En resurs som har ett hinder för NON_PIXEL_SHADER_RESOURCE tillstånd i en 3D-kö kan därför användas i det tillståndet i valfri 3D- eller beräkningskö, med förbehåll för synkroniseringskrav som kräver att de flesta skrivningar serialiseras. Resurstillstånden som delas mellan de två typklasserna (COPY_SOURCE och COPY_DEST) anses vara olika tillstånd för varje typklass. Så att om en resurs övergår till COPY_DEST i en kopieringskö är den inte tillgänglig som ett kopieringsmål från 3D- eller Beräkningsköer och vice versa.

    Sammanfatta.

    • Ett köobjekt är en enskild kö.
    • En kötyp är någon av dessa tre: Compute, 3D och Copy.
    • En kö av typen "type class" är någon av dessa två: Compute/3D och Copy.
  • COPY-flaggorna (COPY_DEST och COPY_SOURCE) som används som inledande tillstånd representerar tillstånd i klassen 3D/Compute-typ. Om du vill använda en resurs från början i en kopieringskö bör den starta i COMMON-tillståndet. COMMON-tillståndet kan användas för alla användningar i en kopieringskö med hjälp av implicita tillståndsövergångar. 

  • Även om resurstillståndet delas mellan alla Beräknings- och 3D-köer, är det inte tillåtet att skriva till resursen samtidigt i olika köer. "Samtidigt" innebär här att osynkroniserad, att notera osynkroniserad körning inte är möjligt på viss maskinvara. Följande regler gäller.

    • Endast en kö kan skriva till en resurs i taget.
    • Flera köer kan läsa från resursen så länge de inte läser byte som ändras av skrivaren (läsning av byte som skrivs samtidigt ger odefinierade resultat).
    • Ett staket måste användas för att synkronisera efter skrivning innan en annan kö kan läsa de skrivna byteen eller göra någon skrivåtkomst.
  • Bakåtbuffertar som visas måste vara i Direct3D-12_RESOURCE_STATE_COMMON tillstånd. 

Programmeringsguide för Direct3D 12

Använda resursbarriärer för att synkronisera resurstillstånd i Direct3D 12

Minneshantering i Direct3D 12