Кэширование шейдеров в DirectX 9

Совсем недавно (сегодня 03.02.2018) прикрутил кэширование шейдеров. Результаты очень сильно порадовали. Теперь загрузка уровня стала еще быстрее.

На данный момент у нас на тестовом уровне используется 24 вершинных и 78 пиксельных шейдеров. Время загрузки шейдеров в релизе составляет примерно 16 секунд, после внедрения кэширования, загрузка кэша всех шейдеров происходит за 1 секунду (1,1-1,3 примерно). Если исходный шейдер будет изменен то кэш будет считаться просроченным и загружаться будет исходный шейдер, так что изменять код шейдеров можно без опсаки.  Что несомненно радует bravo

Однако, кэширование надо использовать только для релизной компиляции шейдеров big_boss

Задача и условия

При загрузке шейдера проверять, а не сохранена ли его кэшированная версия:

  • если сохранена сравнивать даты последнего изменения загружаемого шейдера и кэшироованной версии:
    • если разницы нет, значит загружать кэшированную версию
    • если есть разница создавать кэшированную версию и сохранять
  • если не сохранена создавать кэшированную версию и сохранять.

Хранилище

Все как и с обычными файлами, структура хранения файлов кэша аналогична структуре некэшированных шейдеров и сохраняется в отдельной директории (cache) в папке shaders (где лежат все некэшированные шейдеры).

Файл кэша

Каждый шейдер имеет кэшированную версию. То есть один файл шейдера имеет один или несколько своих кэшированных файлов (это связано с дефайнами, об этом ниже).

Файл кэшированной версии шейдера должен быть бинарным и сохраняться в отдельный файл. Структура файла кэшированного шейдера:

  • uint32_t время последнего изменения файла
  • uint32_t длина имени шейдера
  • char имя шейдера
  • uint32_t количество констант в шейдере
  • цикл сохранения/чтения данных о константах:
    • uint32_t длина имени константы
    • char имя константы
    • uint32_t тип регистра (bool, int4, float4)
    • uint32_t номер регистра в который будут передаваться значения
    • uint32_t количество занимаемых регистров
    • uint32_t класс константы (скаляр, вектор, матрица)
    • uint32_t тип константы (вообще не используем но на всякий случай записываем)
  • uint32_t количество дефайнов
  • цикл сохранения/чтения данных о дефайнах:
    • uint32_t длина имени
    • char имя
    • uint32_t длина значения
    • char значение
  • uint32_t длина бинарного кода
  • pointer бинарный код шейдера (подробнее ниже)

Defines и загрузка шейдеров

Но шейдеры могут загружаться с различным набором defines. Это несколько затрудняет реализацию.

Внутри движка SkyXEngine у нас используется именование шейдеров, то есть каждый шейдер имеет имя (не путать с именем файла). Поэтому при разработке кэширования, мы условились что каждый шейдер должен иметь оригинальное название. При вызове функции загрузки шейдера указывается имя файла и имя этого шейдера, которое будет присвоено ему после загрузки:

//! поставить шейдер в очередь загрузки
SX_LIB_API ID SGCore_ShaderLoad(
	SHADER_TYPE type_shader, //!< тип шейдера
	const char *szPath, //!< имя файла шейдера с расширением
	const char *szName, //!< имя шейдера которое присвоится при загрузке
	SHADER_CHECKDOUBLE check_double,//!< проверять ли на уникальность
	D3DXMACRO *pMacro = 0 //!< макросы
);

Таким образом, мы сами закрепляем за каждым шейдером имя, но это имя состоит из имени файла + имена дефайнов либо его функциональность. Но имя шейдера должно быть уникальным для используемого набора дефайнов big_boss

К примеру у нас есть шейдер lighting_com.ps, который имеет две версии:

  • с дефайном IS_SHADOWED (учет теней)
  • без дефайна IS_SHADOWED (без учета теней)

а имена шейдеров назначены вот так:

  • lighting_com_shadow.ps
  • lighting_com_nonshadow.ps

Код загрузки указанных версий одного шейдера:

GData::IDsShaders::PS::ComLightingNonShadow = SGCore_ShaderLoad(SHADER_TYPE_PIXEL, "lighting_com.ps", "lighting_com_nonshadow.ps", SHADER_CHECKDOUBLE_NAME);
D3DXMACRO Defines_IS_SHADOWED[] = { { "IS_SHADOWED", "" }, { 0, 0 } };
GData::IDsShaders::PS::ComLightingShadow = SGCore_ShaderLoad(SHADER_TYPE_PIXEL, "lighting_com.ps", "lighting_com_shadow.ps", SHADER_CHECKDOUBLE_NAME, Defines_IS_SHADOWED);
GData::IDsShaders::PS::BlendAmbientSpecDiffColor = SGCore_ShaderLoad(SHADER_TYPE_PIXEL, "lighting_blend.ps", "lighting_blend.ps",SHADER_CHECKDOUBLE_PATH);

Бинарный код шейдера

Приведу кусок кода загрузки шейдера:

ID3DXBuffer *pBufShader = 0;
ID3DXBuffer *pBufError = 0;
ID3DXConstantTable *pConstTable;
IDirect3DVertexShader9 *pVertexShader;
D3DXHANDLE aHandles[SXGC_SHADER_VAR_MAX_COUNT];

HRESULT hr = D3DXCompileShaderFromFile(
	szFullPath,
	aMacro,
	0,
	"main",
	"vs_3_0",
	SHADER_FLAGS,
	&pBufShader,
	&pBufError,
	&pConstTable
);

if (pBufError && pBufShader == 0)
{
	LibReport(REPORT_MSG_LEVEL_ERROR, "%s - failed to load vertex shader [%s], msg: %s\n", GEN_MSG_LOCATION, szPath, (char*)pBufError->GetBufferPointer());
	return;
}

if (FAILED(hr))
{
	LibReport(REPORT_MSG_LEVEL_ERROR, "%s - download function vertex shader fails, path [%s]\n", GEN_MSG_LOCATION, szPath);
	return;
}

hr = g_pDXDevice->CreateVertexShader(
	(DWORD*)pBufShader->GetBufferPointer(),
	&pVertexShader
);

if (FAILED(hr))
{
	LibReport(REPORT_MSG_LEVEL_ERROR, "%s - error creating vertex shader [%s]\n", GEN_MSG_LOCATION, szPath);
	return;
}

Здесь (DWORD*)pBufShader->GetBufferPointer() и есть получение бинарного кода шейдера.

Для записи бинарного кода в файл можно использовать следуюший код:

uint32_t iSizeBinCode = pShaderFileCache->m_pCode->GetBufferSize();
fwrite(&iSizeBinCode, sizeof(uint32_t), 1, pFile);
fwrite(pShaderFileCache->m_pCode->GetBufferPointer(), iSizeBinCode, 1, pFile);

То есть: получаем размеру буфера в байтах, записываем в файл это число, затем записываем в файл весь буфер размером iSizeBinCode байт. m_pCode в данном случае является ID3DXBuffer.

Считывать из файла бинарный код можно следующим образом:

uint32_t iSizeBinCode;
fread(&iSizeBinCode, sizeof(uint32_t), 1, pFile);

D3DXCreateBuffer(iSizeBinCode, &(pSFC->m_pCode));
fread(pSFC->m_pCode->GetBufferPointer(), iSizeBinCode, 1, pFile);

Считываем размер бинарного кода, создаем буфер в соотвествии с этим размером, а после в указатель буфера считываем весь бинарный код.

Для создания шейдера из бинарного кода достаточно сделать так:

HRESULT hr = g_pDXDevice->CreateVertexShader(
	(DWORD*)pShader->m_pCode->GetBufferPointer(),
	&(pShader->m_pVertexShader)
 );

Время последнего изменения файла

Функция не элегантная, но зато рабочая hi :

UINT Core_0GetTimeLastModify(const char *szPath)
{
	HANDLE hFile = CreateFile(szPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);

	if (hFile == INVALID_HANDLE_VALUE)
		return 0;

	SYSTEMTIME stUTC, stLocal;
	FILETIME ftCreate, ftAccess, ftWrite;
	GetFileTime(hFile, &ftCreate, &ftAccess, &ftWrite);
	FileTimeToSystemTime(&ftWrite, &stUTC);

	tm tmObj;
	ZeroMemory(&tmObj, sizeof(tm));
	tmObj.tm_year = stUTC.wYear - 1900;
	tmObj.tm_mon = stUTC.wMonth;
	tmObj.tm_mday = stUTC.wDay;
	tmObj.tm_hour = stUTC.wHour;
	tmObj.tm_min = stUTC.wMinute;
	tmObj.tm_sec = stUTC.wSecond;

	uint32_t tLastModify = mktime(&tmObj);

	return tLastModify;
}

Оказалось здесь нет ничего сложного, все очень просто, только надо сделать и все. Зато теперь при каждом тесте мы экономим примерно 15 секунд blum

Поделиться:

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*