gestione risorse Fence-Based
Illustra come gestire l'intervallo di vita dei dati delle risorse monitorando lo stato della GPU tramite recinzioni. La memoria può essere riutilizzata in modo efficace con recinti che gestiscono attentamente la disponibilità di spazio libero in memoria, ad esempio in un'implementazione del buffer circolare per un heap di caricamento.
Scenario del buffer circolare
Di seguito è riportato un esempio in cui un'app riscontra una rara richiesta di caricamento della memoria heap.
Un buffer circolare è un modo per gestire un heap di caricamento. Il buffer circolare contiene i dati necessari per i fotogrammi successivi. L'app gestisce un puntatore di input dati corrente e una coda di offset dei fotogrammi per registrare ogni fotogramma e l'offset iniziale dei dati delle risorse per tale frame.
Un'app crea un buffer circolare basato su un buffer per caricare i dati nella GPU per ogni fotogramma. Attualmente è stato eseguito il rendering del frame 2, il buffer circolare esegue il wrapping dei dati per il frame 4, tutti i dati necessari per il frame 5 è presente e un buffer costante di grandi dimensioni necessario per frame 6 deve essere sottoallocato.
figura 1: l'app tenta di allocare in modo secondario per il buffer costante, ma trova memoria insufficiente.
figura 2 : tramite il polling di isolamento, l'app rileva che è stato eseguito il rendering del frame 3, la coda di offset dei fotogrammi viene quindi aggiornata e lo stato corrente del buffer circolare segue, ma la memoria libera non è ancora sufficientemente grande per contenere il buffer costante.
figura 3 : data la situazione, la CPU si blocca (tramite recinto in attesa) fino a quando non viene eseguito il rendering del frame 4, che libera la memoria secondaria allocata per il frame 4.
figura 4: ora la memoria disponibile è sufficientemente grande per il buffer costante e l'allocazione secondaria ha esito positivo; l'app copia i dati del buffer costante big in memoria usati in precedenza dai dati delle risorse per entrambi i frame 3 e 4. Il puntatore di input corrente viene infine aggiornato.
Se un'app implementa un buffer circolare, il buffer circolare deve essere sufficientemente grande da far fronte allo scenario peggiore delle dimensioni dei dati delle risorse.
Esempio di buffer circolare
Il codice di esempio seguente illustra come è possibile gestire un buffer circolare, prestando attenzione alla routine di allocazione secondaria che gestisce il polling e l'attesa del recinto. Per semplicità, l'esempio usa NOT_SUFFICIENT_MEMORY per nascondere i dettagli di "memoria libera insufficiente trovata nell'heap", poiché tale logica (basata su m_pDataCur e offset all'interno di FrameOffsetQueue) non è strettamente correlata agli heap o ai recinto. L'esempio è semplificato per sacrificare la frequenza dei fotogrammi anziché l'utilizzo della memoria.
Si noti che il supporto del buffer circolare dovrebbe essere uno scenario popolare; Tuttavia, la progettazione dell'heap non impedisce altri utilizzi, ad esempio la parametrizzazione dell'elenco dei comandi e il riutilizzo.
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();
}
}
Argomenti correlati