[DX11] Resource Manager (경로기반, 바이너리기반)

ChangJin·2025년 11월 20일

DirectX11

목록 보기
12/13
post-thumbnail

글에 사용된 모든 그림과 내용은 직접 작성한 것입니다.

[유튜브 영상]


[깃허브 보러가기]
https://github.com/Chang-Jin-Lee/D3D11-AliceTutorial/tree/main/28_Scene_Shared3DModel_Animation

[풀리퀘 보러가기]
https://github.com/Chang-Jin-Lee/D3D11-AliceTutorial/pull/58


글의 목적

D3D11에서 리소스 매니저가 필요한 이유와 어떤 방법으로 구현할 수 있는지 정리하기 위함

2개 있을때같은 모델이 여러 개 그려져도 VRAM이 증가하지 않는다
VRAM 1963.24 MB
VRAM 1963.24 MB

리소스 매니저가 왜 필요할까

  • 같은 애셋을 가진 새로운 인스턴스가 만들어질 때, 매번 애셋을 새로 로드하면 시간, 메모리 낭비가 큽니다
  • 한 번 로드된 결과를 캐싱해두고 이를 shared_ptr, weak_ptr로 재사용하면 로딩 지연을 줄이고 VRAM 낭비가 획기적으로 줄어듭니다.
  • FBX, PMX같은 버텍스가 1만개 이상되는 무거운 포맷은 CPU에서 데이터를 로드하는 것도 느리지만 GPU의 버퍼, 텍스쳐 로드도 시간이 오래걸리므로 리소스 매니저가 꼭 필요합니다.
  • 예시 FBX 파일의 버텍스가 61872개이고, 본 개수가 255개입니다. 매번 스켈레톤을 로드하고 각각 애니메이션을 업데이트하면 매우 비용이 커집니다.
자원을 공유하고 있는 모델

경로 기반

  • 리소스 매니저 캐싱의 키로 파일의 경로를 넣습니다. 구현이 쉽고 간단하며 디버깅이 쉽습니다. 애초에 경로가 제대로 안되면 로드 자체가 안됩니다. 단점은 경로가 바뀌면 같은 파일이어도 캐시에서 찾을 수 없습니다. 그리고 문자열 비교이므로 매우 느립니다. 또한 대소문자와 슬래시 /에 대한 처리가 골치아플 수 있습니다.
  • 코드는 간단합니다. 다음처럼 클래스를 선언합니다.
 struct AssetKey
{
	std::wstring PathW;
	EAssetKind Kind;
	
	bool operator==(const AssetKey& other) const
	{
		return Kind == other.Kind && PathW == other.PathW;
	}
};

class AssetManager : public Singleton<AssetManager>
{
public:
	AssetManager() = default;
	std::shared_ptr<FbxModel> GetFbxModel(ID3D11Device* device, const std::wstring& pathW);  
private:
	std::unordered_map<AssetKey, std::weak_ptr<FbxModel>, AssetKey::Hash> m_FbxCache;};
}
  • 캐시를 찾아서 반환합니다.
std::shared_ptr<FbxModel> AssetManager::GetFbxModel(ID3D11Device* device, const std::wstring& pathW)
{
	if (!device || pathW.empty()) return nullptr;
 
	// AssetKey로 캐시 조회
	AssetKey key{ pathW, EAssetKind::FbxModel };
	if (auto it = m_FbxCache.find(key); it != m_FbxCache.end())
	{
		if (auto sp = it->second.lock())
		{
			return sp; // 재사용
		}
		else
		{
			m_FbxCache.erase(it); // 만료된 weak_ptr 정리
		}
	}
 
	// 새로 로드
	auto model = std::make_shared<FbxModel>();
	if (!model->Load(device, pathW))
	{
		return nullptr;
	}
	m_FbxCache[key] = model;
	return model;
}

바이너리 기반

  • 파일 내용이 같으면 경로가 달라도 동일한 리소스로 캐시를 찾습니다. 키 비교가 정수의 비교이므로 매우 빠릅니다. FNV-1a 해시 함수를 사용해서 간단하게 데이터 기반 키를 만들겠습니다.
    • FNV-1a 해시 함수를 선택한 이유는 일단 매우 빠르고 구현이 간단하며 충돌이 드물기 때문입니다.
    • 다만 순수하게 FNV-1a만 사용하면 곱셈 전에 XOR을 사용하는데 이로 인해 관련 입력의 해시 입력 값 사이에 예측 가능한 관계나 패턴이 생길수 있는 문제가 있습니다. 보조 피처인 파일 사이즈랑 수정 날짜를 한번 더 연산해서 해시 충돌을 줄입니다.
    • 다음의 url에서 더 다양한 해시 함수를 찾아볼 수 있습니다.
      https://www.ietf.org/archive/id/draft-eastlake-fnv-22.html#name-fnv-1a-c-code
  • 다음의 함수로 해시 값을 찾아서 키 값으로 넣으면 됩니다.
uint64_t AssetManager::ComputeFileHash(const std::wstring& pathW)
{
	if (!std::filesystem::exists(pathW)) return 0;
 	auto fileSize = std::filesystem::file_size(pathW);
	auto writeTime = std::filesystem::last_write_time(pathW);
	std::uint64_t timeHash = static_cast<std::uint64_t>(writeTime.time_since_epoch().count());
	uint64_t sizeHash = fileSize;
 
	const size_t SAMPLE_SIZE = 4096; 
	std::ifstream file(pathW, std::ios::binary);
	if (!file.is_open()) return 0;
	uint8_t buffer[SAMPLE_SIZE * 2] = { 0 };
	size_t readSize = 0;
	if (fileSize > 0)
	{
		size_t toRead = (std::min)(SAMPLE_SIZE, static_cast<size_t>(fileSize));
		file.read(reinterpret_cast<char*>(buffer), toRead);
		readSize += static_cast<size_t>(file.gcount());
	}
	if (fileSize > SAMPLE_SIZE)
	{
		file.seekg(-static_cast<std::streamoff>((std::min)(SAMPLE_SIZE, static_cast<size_t>(fileSize))), std::ios::end);
		file.read(reinterpret_cast<char*>(buffer + readSize), SAMPLE_SIZE);
		readSize += static_cast<size_t>(file.gcount());
	}
	file.close();
	uint64_t sampleHash = FNV1a64(buffer, readSize);
 	uint64_t finalHash = sizeHash;
	finalHash ^= (timeHash << 1);
	finalHash ^= (sampleHash << 2);
 
	return finalHash;
}
profile
게임 프로그래머

0개의 댓글