W przypadku alokacji i dealokacji zasobów istotna często jest kolejność. Przykładowo: tekstury Direct3D zależne są od istnienia urządzenia, natomiast samo urządzenie od obiektu Direct3D. Czasem problemem jest zwalnianie zasobów w odpowiedni sposób, zgodnie z niezbędną kolejnością. Wzorując się na Symbianowym pomyśle CleanupStacka postanowiłem przygotować opracowanie tego tematu.
Najczęstszym, najprostszym, ale niekoniecznie najlepszym podejściem jest ręczne sterowanie zwalnianiem zasobów. Przykładowo: zwalniając urządzenie D3D wcześniej zwalniamy wszystkie tekstury. Niby wszystko ładnie, niby w porządku, co jednak jeśli trafimy na sytuację zgubionego zasobu? Co przy wielowątkowości? Co z obiektami posiadającymi wspólne dla kilku "poziomów alokacji" zasoby? Obsługa tych problemów niekiedy może być uciążliwa.
Podejście drugie: autopointery.
Stosuje się też inne, ciekawe rozwiązanie. Każdy zasób posiada uchwyt (ze zliczaniem referencji) do "rodzica", który musi istnieć do poprawnego działania zasobu. Gwarantuje nam to, że w momencie zwolnienia zasobu, jego rodzic na pewno nadal będzie istniał. Ponadto, w razie konieczności, mamy dostęp do rodzica z poziomu zasobu. Gdzie są wady? Otóż dla każdego gotowego zasobu (np. IDirect3D10Texture2D) musimy pisać specjalne opakowanie, trzymające też wskaźnik do rodzica. Problemem jest również np. zestaw rodziców (jak device i swapchain w D3D10), gdzie wspomnianych wskaźników musiałoby być więcej. O wydajności jeśli chcemy "poprawnie" zliczać referencje nie wspomnę.
Podejście trzecie: w końcu nasz stos.
Czemu by nie przygotować globalnego obiektu stosu alokacji? W momencie tworzenia obiektu zasobu, umieszczany jest on na stosie. Zwalniając zasób, zdejmujemy go ze stosu. Stos sam w sobie zajmuje się zwalnianiem (my tylko zdejmujemy zasób), automatycznie kontrolując kolejność - zawsze będzie odwrotna do kolejności tworzenia. Oczywiście ma to jedną wadę: nie da się zwolnić obiektu "w środku", wkładając na stos tekstury 1, 2, 3, 4 musimy je zwalniać w kolejności 4, 3, 2, 1. W innym przypadku można spodziewać się problemów. Dlatego też wymyślono...
Podejście czwarte: ramki.
Zwykle zasoby alokujemy w grupach. Zestaw zasobów dla danej lokacji, sceny, planszy, trybu gry. Po prostu paczka tekstur, modeli i skryptów. Czemu by tego nie wykorzystać przy (de)alokacji? Tworząc nasz CleanupStack dopisujemy opcję oznaczania "ramek". Dana "ramka" to zestaw zasobów, zwalniany jednocześnie całą grupą. Kiedy ramkujemy? To zależy od implementacji. Można zastosować model na zasadzie BeginFrame()/EndFrame(), można zastosować oznaczanie ramki przed dodaniem pierwszego elementu (CreateNewFrame()), można też (i to polecam) oznaczyć zasoby już dodane jako kompletną ramkę (MarkFrame()). Zwalnianie ramki/kilku ramek będzie prostsze, gdy dodamy jakiś sposób ich tytułowania/oznaczania.
Przykładowa implementacja:
#pragma once
#include <string>
#include <list>
#include <vector>
class CleanupStack;
class IResource
{
friend class CleanupStack;
protected:
virtual ~IResource(){}
};
class CleanupStack
{
public:
CleanupStack();
~CleanupStack();
static void Push(IResource* res);
static bool Pop(IResource* res);
static void Pop(int count = 1);
static void MarkFrame(const std::string& frame);
static void PopFrame();
static bool PopFrame(const std::string& frame);
static void DestroyAll();
private:
struct FRAME
{
std::string name;
std::vector<IResource*> objects;
};
std::list lockedFrames;
FRAME currentFrame;
static CleanupStack* Self;
};
#include "IResource.h"
#include <cassert>
CleanupStack* CleanupStack::Self = NULL;
CleanupStack::CleanupStack()
{
assert(Self == NULL && "Cleanup stack exists!");
Self = this;
}
CleanupStack::~CleanupStack()
{
assert(Self != NULL && "Cleanup stack don't exists!");
DestroyAll();
Self = NULL;
}
void CleanupStack::Push(IResource *res)
{
assert(Self != NULL && "Cleanup stack don't exists!");
Self->currentFrame.objects.push_back(res);
}
bool CleanupStack::Pop(IResource *res)
{
assert(Self != NULL && "Cleanup stack don't exists!");
if(Self->currentFrame.objects.empty() || Self->currentFrame.objects[Self->currentFrame.objects.size()-1] != res)
return false;
else
Pop();
return true;
}
void CleanupStack::Pop(int count)
{
assert(Self != NULL && "Cleanup stack don't exists!");
for(int i = 0; !Self->currentFrame.objects.empty() && i <>currentFrame.objects[Self->currentFrame.objects.size()-1];
Self->currentFrame.objects.pop_back();
}
}
void CleanupStack::MarkFrame(const std::string &frame)
{
assert(Self != NULL && "Cleanup stack don't exists!");
Self->currentFrame.name = frame;
Self->lockedFrames.push_back(Self->currentFrame);
Self->currentFrame = FRAME();
}
void CleanupStack::PopFrame()
{
assert(Self != NULL && "Cleanup stack don't exists!");
if(!Self->currentFrame.objects.empty())
Pop(Self->currentFrame.objects.size());
Self->currentFrame = Self->lockedFrames.back();
Self->lockedFrames.pop_back();
Pop(Self->currentFrame.objects.size());
}
bool CleanupStack::PopFrame(const std::string &frame)
{
assert(Self != NULL && "Cleanup stack don't exists!");
if(Self->lockedFrames.back().name != frame)
return false;
if(!Self->currentFrame.objects.empty())
Pop(Self->currentFrame.objects.size());
Self->currentFrame = Self->lockedFrames.back();
Self->lockedFrames.pop_back();
Pop(Self->currentFrame.objects.size());
return true;
}
void CleanupStack::DestroyAll()
{
assert(Self != NULL && "Cleanup stack don't exists!");
while(!Self->lockedFrames.empty())
PopFrame();
}
Brak komentarzy:
Prześlij komentarz