Gestion des ressources en fonction des limites
Montre comment gérer la durée de vie des données de ressources en suivant la progression du GPU via des clôtures. La mémoire peut être réutilisée efficacement avec des clôtures qui gèrent soigneusement la disponibilité de l’espace libre dans la mémoire, par exemple dans une implémentation de mémoire tampon en anneau pour un tas de chargement.
Scénario de mémoire tampon en anneau
Voici un exemple dans lequel une application rencontre une demande rare de chargement de mémoire tas.
Une mémoire tampon en anneau est un moyen de gérer un tas de chargement. La mémoire tampon en anneau contient les données requises pour les images suivantes. L’application gère un pointeur d’entrée de données actuel et une file d’attente de décalage de trame pour enregistrer chaque image et le décalage de départ des données de ressource pour cette image.
Une application crée une mémoire tampon en anneau basée sur une mémoire tampon pour charger des données sur le GPU pour chaque image. Actuellement, l’image 2 a été rendue, la mémoire tampon en anneau entoure les données de l’image 4, toutes les données requises pour l’image 5 sont présentes et une mémoire tampon constante importante requise pour l’image 6 doit être sous-allouée.
Figure 1 : l’application tente de sous-allouer la mémoire tampon constante, mais trouve une mémoire libre insuffisante.
Figure 2 : grâce à l’interrogation de clôture, l’application découvre que l’image 3 a été rendue, que la file d’attente de décalage d’image est ensuite mise à jour et que l’état actuel de la mémoire tampon en anneau suit. Toutefois, la mémoire libre n’est toujours pas assez grande pour prendre en charge la mémoire tampon constante.
Figure 3 : compte tenu de la situation, le processeur se bloque (via une clôture en attente) jusqu’à ce que l’image 4 ait été rendue, ce qui libère la mémoire sous-allouée pour l’image 4.
Figure 4 : la mémoire libre est maintenant suffisamment grande pour la mémoire tampon constante et la sous-allocation réussit ; l’application copie les données de mémoire tampon à grande constante dans la mémoire précédemment utilisée par les données de ressources pour les images 3 et 4. Le pointeur d’entrée actuel est enfin mis à jour.
Si une application implémente une mémoire tampon en anneau, la mémoire tampon en anneau doit être suffisamment grande pour faire face au pire scénario des tailles de données de ressources.
Exemple de mémoire tampon en anneau
L’exemple de code suivant montre comment gérer une mémoire tampon en anneau, en prêtant attention à la routine de sous-allocation qui gère l’interrogation de clôture et l’attente. Par souci de simplicité, l’exemple utilise NOT_SUFFICIENT_MEMORY pour masquer les détails de « mémoire libre insuffisante trouvée dans le tas », car cette logique (basée sur les m_pDataCur et les décalages à l’intérieur de FrameOffsetQueue) n’est pas étroitement liée aux tas ou aux clôtures. L’exemple est simplifié pour sacrifier la fréquence d’images au lieu de l’utilisation de la mémoire.
Notez que la prise en charge de la mémoire tampon en anneau est censée être un scénario populaire ; toutefois, la conception du tas n’empêche pas d’autres utilisations, telles que le paramétrage de la liste de commandes et la réutilisation.
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();
}
}
Rubriques connexes