Share via


Fence-basierte Ressourcenverwaltung

Zeigt, wie Sie die Lebensdauer von Ressourcendaten verwalten, indem Sie den GPU-Fortschritt über Zäune nachverfolgen. Arbeitsspeicher kann effektiv wiederverwendet werden, indem Zäune die Verfügbarkeit von freiem Speicherplatz im Arbeitsspeicher sorgfältig verwalten, z. B. in einer Ringpufferimplementierung für einen Upload-Heap.

Ringpufferszenario

Im Folgenden finden Sie ein Beispiel, in dem eine App einen seltenen Bedarf an Heapspeicher zum Hochladen von Heap-Speicher aufweist.

Ein Ringpuffer ist eine Möglichkeit, einen Uploadheap zu verwalten. Der Ringpuffer enthält Daten, die für die nächsten Frames erforderlich sind. Die App verwaltet einen aktuellen Dateneingabezeiger und eine Frameoffsetwarteschlange, um jeden Frame und Startoffset von Ressourcendaten für diesen Frame aufzuzeichnen.

Eine App erstellt einen Ringpuffer basierend auf einem Puffer zum Hochladen von Daten in die GPU für jeden Frame. Derzeit wurde Frame 2 gerendert, der Ringpuffer umschließt die Daten für Frame 4, alle für Frame 5 erforderlichen Daten sind vorhanden, und ein großer konstanter Puffer, der für Frame 6 erforderlich ist, muss untergeordnet werden.

Abbildung 1 : Die App versucht, den Konstantenpuffer unterzuteilen, findet jedoch nicht genügend freien Arbeitsspeicher.

Unzureichender freier Arbeitsspeicher in diesem Ringpuffer

Abbildung 2 : Durch die Zaunabfrage erkennt die App, dass Frame 3 gerendert wurde, die Frameoffsetwarteschlange dann aktualisiert wird, und der aktuelle Zustand des Ringpuffers folgt . Allerdings ist der freie Arbeitsspeicher immer noch nicht groß genug, um den Konstantenpuffer aufzunehmen.

nach dem Rendern von Frame 3 noch nicht genügend Arbeitsspeicher

Abbildung 3 : Unter Berücksichtigung der Situation blockiert sich die CPU selbst (über den Zaun wartet), bis Frame 4 gerendert wurde, wodurch der für Frame 4 unter zugeordnete Arbeitsspeicher freigegeben wird.

Durch Das Rendern von Frame 4 wird mehr vom Ringpuffer freigegeben.

Abbildung 4 : Jetzt ist der freie Arbeitsspeicher groß genug für den konstanten Puffer, und die Unterzuordnung ist erfolgreich; Die App kopiert die großen konstanten Pufferdaten in den Speicher, der zuvor von Ressourcendaten für Frames 3 und 4 verwendet wurde. Der aktuelle Eingabezeiger wird schließlich aktualisiert.

jetzt ist Platz ab Frame 6 im Ringpuffer

Wenn eine App einen Ringpuffer implementiert, muss der Ringpuffer groß genug sein, um dem schlechteren Szenario der Ressourcendaten zu begegnen.

Ringpufferbeispiel

Der folgende Beispielcode zeigt, wie ein Ringpuffer verwaltet werden kann, wobei auf die Unterzuordnungsroutine geachtet wird, die das Abfragen und Warten von Zaunen verarbeitet. Der Einfachheit halber verwendet das Beispiel NOT_SUFFICIENT_MEMORY, um die Details von "nicht genügend freier Arbeitsspeicher im Heap" auszublenden, da diese Logik (basierend auf m_pDataCur und Offsets in FrameOffsetQueue) nicht eng mit Heaps oder Zäunen zusammenhängt. Das Beispiel wurde vereinfacht, um die Framerate anstelle der Speicherauslastung zu opfern.

Beachten Sie, dass die Unterstützung von Ringpuffern ein beliebtes Szenario sein wird. Der Heapentwurf schließt jedoch keine andere Verwendung aus, z. B. die Parameterisierung und Wiederverwendung von Befehlslisten.

struct FrameResourceOffset
{
    UINT frameIndex;
    UINT8* pResourceOffset;
};
std::queue<FrameResourceOffset> frameOffsetQueue;

void DrawFrame()
{
    float vertices[] = ...;
    UINT verticesOffset = 0;
    ThrowIfFailed(
        SetDataToUploadHeap(
            vertices, sizeof(float), sizeof(vertices) / sizeof(float), 
            4, // Max alignment requirement for vertex data is 4 bytes.
            verticesOffset
            ));

    float constants[] = ...;
    UINT constantsOffset = 0;
    ThrowIfFailed(
        SetDataToUploadHeap(
            constants, sizeof(float), sizeof(constants) / sizeof(float), 
            D3D12_CONSTANT_BUFFER_DATA_PLACEMENT_ALIGNMENT,
            constantsOffset
            ));

    // Create vertex buffer views for the new binding model. 
    // Create constant buffer views for the new binding model. 
    // ...

    commandQueue->Execute(commandList);
    commandQueue->AdvanceFence();
}

HRESULT SuballocateFromHeap(SIZE_T uSize, UINT uAlign)
{
    if (NOT_SUFFICIENT_MEMORY(uSize, uAlign))
    {
        // Free up resources for frames processed by GPU; see Figure 2.
        UINT lastCompletedFrame = commandQueue->GetLastCompletedFence();
        FreeUpMemoryUntilFrame( lastCompletedFrame );

        while ( NOT_SUFFICIENT_MEMORY(uSize, uAlign)
            && !frameOffsetQueue.empty() )
        {
            // Block until a new frame is processed by GPU, then free up more memory; see Figure 3.
            UINT nextGPUFrame = frameOffsetQueue.front().frameIndex;
            commandQueue->SetEventOnFenceCompletion(nextGPUFrame, hEvent);
            WaitForSingleObject(hEvent, INFINITE);
            FreeUpMemoryUntilFrame( nextGPUFrame );
        }
    }

    if (NOT_SUFFICIENT_MEMORY(uSize, uAlign))
    {
        // Apps need to create a new Heap that is large enough for this resource.
        return E_HEAPNOTLARGEENOUGH;
    }
    else
    {
        // Update current data pointer for the new resource.
        m_pDataCur = reinterpret_cast<UINT8*>(
            Align(reinterpret_cast<SIZE_T>(m_pHDataCur), uAlign)
            );

        // Update frame offset queue if this is the first resource for a new frame; see Figure 4.
        UINT currentFrame = commandQueue->GetCurrentFence();
        if ( frameOffsetQueue.empty()
            || frameOffsetQueue.back().frameIndex < currentFrame )
        {
            FrameResourceOffset offset = {currentFrame, m_pDataCur};
            frameOffsetQueue.push(offset);
        }

        return S_OK;
    }
}

void FreeUpMemoryUntilFrame(UINT lastCompletedFrame)
{
    while ( !frameOffsetQueue.empty() 
        && frameOffsetQueue.first().frameIndex <= lastCompletedFrame )
    {
        frameOffsetQueue.pop();
    }
}

ID3D12Fence

Unterzuweisung innerhalb von Puffern