Fence-basierte Ressourcenverwaltung
Zeigt, wie Sie die Lebensdauer von Ressourcendaten verwalten, indem Sie den GPU-Fortschritt über Fences nachverfolgen. Arbeitsspeicher kann effektiv mit Fences erneut verwendet werden, um die Verfügbarkeit von freiem Speicherplatz im Arbeitsspeicher sorgfältig zu verwalten, z. B. in einer Ringpufferimplementierung für einen Hochladen Heap.
Ringpufferszenario
Im Folgenden finden Sie ein Beispiel, in dem für eine App ein seltener Bedarf an Upload heap memory (Heapspeicher hochladen) besteht.
Ein Ringpuffer ist eine Möglichkeit, einen Upload-Heap 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 der Ressourcendaten für diesen Frame zu erfassen.
Eine App erstellt einen Ringpuffer basierend auf einem Puffer, um Daten für jeden Frame in die GPU hochzuladen. 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 unter zugeordnet werden.
Abbildung 1: Die App versucht, eine Unterteilung für den konstanten Puffer zu erstellen, findet jedoch nicht genügend freien Arbeitsspeicher.

Abbildung 2: Durch das Fence Polling stellt die App fest, dass Frame 3 gerendert wurde, die Frameoffsetwarteschlange dann aktualisiert wird und der aktuelle Zustand des Ringpuffers folgt. Der freie Arbeitsspeicher ist jedoch immer noch nicht groß genug, um den konstanten Puffer aufnehmen zu können.

Abbildung 3: Angesichts der Situation blockiert sich die CPU selbst (über einen Fence Waiting), bis Frame 4 gerendert wurde, wodurch der für Frame 4 unter zugeordnete Arbeitsspeicher frei wird.

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 Arbeitsspeicher, der zuvor von Ressourcendaten für die Frames 3 und 4 verwendet wurde. Der aktuelle Eingabezeiger wird schließlich aktualisiert.

Wenn eine App einen Ringpuffer implementiert, muss der Ringpuffer groß genug sein, um mit dem schlechteren Szenario der Größen von Ressourcendaten umgehen zu können.
Ringpufferbeispiel
Der folgende Beispielcode zeigt, wie ein Ringpuffer verwaltet werden kann, und berücksichtigt dabei die Unterzuordnungsroutine, die das Abrufen und Warten von Fence verarbeitet. Der Einfachheit halber verwendet das Beispiel NICHT GENÜGEND ARBEITSSPEICHER, um die Details von "nicht genügend freiem Arbeitsspeicher im Heap" auszublenden, da diese Logik (basierend auf _ m _ _ pDataCur und Offsets innerhalb von FrameOffsetQueue) nicht eng mit Heaps oder Fences verknüpft ist. Das Beispiel wird vereinfacht, um die Bildfrequenz anstelle der Arbeitsspeicherauslastung zu verfingen.
Beachten Sie, dass die Ringpufferunterstützung ein beliebtes Szenario sein wird. Der Heapentwurf schließt jedoch keine andere Verwendung aus, z. B. die Parametrisierung und Erneutverwendung 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();
}
}