Библиотека

Что может наша видеокарта, что можем сделать мы и как это узнать?

Mr.Snow DirectX8 Column

   Источник: http://xdev.ru/dxgp/rgd_articles_r.asp?s=columns&art=mrsdx8_0021

По прошествии некоторого времени после празднования Нового Года и всего, что с ним связано, предлагаю вашему вниманию новый урок. Посвящен он будет извлечению информации о возможностях вашей видео карты (или видео карт, если у вас их несколько), таких как поддерживаемые разрешения экрана, возможные форматы поверхностей, различные форматы глубинных буферов. Также сюда отнесем тему получения и использования возможностей видео карты, или, точнее, чип сета на котором она построена (например GeForce1-2-3, TNT1-2, Radeon, 3dfx Voodoo и так далее), это такие возможности как возможность рендерить в оконном режиме, аппаратной реализации TnL, максимальные размеры текстур и т.д.

Данная тема появилась тогда, когда мне потребовалось сделать, что бы можно было запускать мою программу во всех возможных режимах, на всех устройствах, при любом разрешении. Мое прежнее решение (в виде диалога прямо при загрузке программы) мне не понравилось еще и потому, что мне пришлось слишком часто запускать программу в одном и том же разрешении, при это изменяя саму программу, а вот каждый раз при этом устанавливать это самое разрешение оказалось достаточно утомительным занятием.

Отсюда появилось вполне ожидаемое решение - хранить эти настройки в файле. Казалось в чем тогда проблема - вопрос же вроде решен. Но что мне оказалось не правильным, так это то, что при этом пользователь может написать такие параметры, которые не будут работать на данной конкретной видео карте. Отсюда еще один вывод - нужно позволить пользователь самому выбрать настройки из заранее приготовленных. Н при этом брать эту информацию не с потолка, а выяснить эти возможности у видео карты.

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

Данная программка предназначена исключительно для создания конфигурационного файла, на основе предпочтений пользователя - он сам может выбрать какую именно видео карту использовать (если их несколько), какое устройство рендеринга, задать требуемый вариант работы (оконный или полноэкранный, с учетом разных форматов поверхностей - 16 бит, 24 бита или 32 бита), выбрать разрешение экрана, а также и частоту регенерации для монитора, установить требуемый формат буфера глубины и даже выбрать число вторичных поверхностей (2 или 3, соответственно Double или Triple Buffers). После того, как пользователь все выбрал и нажал кнопку 'Save and Exit' вся конфигурация сохраняется в файл config.txt, который затем используется непосредственно главной программой. Она из него загружает эти сохраненные параметры, и использует их по назначению - установке их, когда происходит инициализация Direct3D8.

Внешне все это понятно. Теперь давайте возьмемся за внутренности, за то, как все это устроено внутри, как происходит процесс в программе. Но для начала нужно объяснить, как это вообще все организовано, и что как и в какой последовательности нужно делать (я достаточно долго до этого добирался, и много на этом потратил времени).

1 - получаем количество доступных нам видео карт;
2 - для каждой видео карты выясняем, какие устройства рендеринга на ней доступны;
3 - для каждого устройства рендеринга получаем допустимые форматы поверхностей для рендеринга;
4 - далее для каждого формата поверхностей получаем список всех возможных форматов для буферов глубины;
5 - для каждой видео карты получаем список всех доступных видео разрешений, частоты обновления и глубины пикселя поверхности (16, 24, 32 бита);

Теперь возьмемся за рассмотрение практической части, покрывающей вышеозначенные пункты.

Замечу, что данный код я использую не только в этом конфигураторе, но и в основной программе. В основной программе я обернул все это в специальный класс, который будет отвечать за получение всей информации о возможностях вашей видео карты, а так же за загрузку ранее созданного конфигурационного файла. Также замечу, что данный код я приведу в конце (отдельно для конфигуратора и отдельно для основной программы, для конфигуратора это будут исходники единственного в проекте файла с программой, а для основной программы это будет код созданного при этом класса). Собственно теперь все мои проекты будут нуждаться в получаемом конфигурационном файле, конфигуратор при этом также будет включаться во все проекты (это увеличит каждый архив на 30 кб), за исключением исходников. Все исходники конфигуратора и исполнимый файл, а также его возможные обновления будут доступны отдельно.

Для работы с темой данного урока я создал класс :

//-----------------------------------------------
// some classes for video datas determining process
//-----------------------------------------------

#define iDevType 2
#define iFormatType 5
#define iDepthType 7
	
class cAdapter
{
public:
	cAdapter ();
	~cAdapter ();

	static int iCount;
	char desc[MAX_DEVICE_IDENTIFIER_STRING];

	bool bDevice[iDevType];
	bool bDeviceW[iDevType];

	bool bFormat[iDevType][iFormatType];
	bool bDepthFormat[iDevType][iFormatType][iDepthType];

	class cMode
	{
	public:
		cMode ();
		~cMode ();

		static int iCount;

		int Width;
		int Height;
		int Refresh;
		D3DFORMAT Format;
		int BitDepth;
	} *mode;

private:
};

int cAdapter::iCount=0;
int cAdapter::cMode::iCount = 0;

cAdapter::cAdapter (void)
{
	for(int i=0; i<iDevType; i++) 
	{
		bDevice[i] = bDeviceW[i] = false;
		for(int j=0; j>iFormatType; j++) 
		{
			bFormat[i][j] = false;
			for(int k=0; k<iDepthType; k++) bDepthFormat[i][j][k] = false;
	};	};
};

cAdapter::~cAdapter (void)
{
};

cAdapter::cMode::cMode (void)
{
	Width = 0;
	Height = 0;
	Refresh = 0;
	Format = D3DFMT_UNKNOWN;
	BitDepth = 0;
};

cAdapter::cMode::~cMode (void)
{
};

cAdapter *Adapter;

//-----------------------------------------------
// -- some classes for video datas determining process
//----------------------------------------------->/pre>

 

Как тут все рассматривается?
Во первых идет объявление трех констант:

--- количество устройств рендеринга (на данный момент их всего два - D3DDEVTYPE_HAL - аппаратный режим, D3DDEVTYPE_REF - программный режим)
--- количество форматов поверхностей (всего пять - D3DFMT_X1R5G5B5, D3DFMT_R5G6B5, D3DFMT_R8G8B8, D3DFMT_X8R8G8B8, D3DFMT_A8R8G8B8)
--- количество форматов буфера глубины и буфера шаблонов (depth and stencil buffers - всего семь форматов - D3DFMT_D16_LOCKABLE, D3DFMT_D16, D3DFMT_D15S1, D3DFMT_D24X8, D3DFMT_D24S8, D3DFMT_D24X4S4, D3DFMT_D32)

Данный класс cAdapter представляет собой один видеоадаптер, если их несколько, то создастся несколько объектов данного класса. В каждом объекте представляющем видеоадаптер (т.е. для каждого видеоадаптера это все выясняется) мы производим поиск сперва всех доступных устройств рендеринга (которых всего два). Далее для каждого из устройств рендеринга мы выясняем какие возможны форматы поверхности для работы. Третий шаг - для каждого из возможных форматов поверхности мы выясняем, какие форматы буфера глубины с ним совместимы. В одномерных массивах bDevice (предназначен для хранения информации о том, какое устройство рендеринга для данного видеоадаптера присутствует) и bDeviceW (те же самые цели только для оконного режима), в двухмерном массиве bFormat (форматы поверхностей, для каждого устройства из bDevice), и в трехмерном массиве bDepthFormat (форматы буфера глубины для каждого формата поверхности для каждого устройства рендеринга) хранится информация о том, доступна ли данная 'штука' или нет. Как вы видите все они типа bool;

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

Далее рассмотрим сам процесс заполнения данных. Весь процесс происходит в следующей части кода:

if(cAdapter::iCount==0)
{
	cAdapter::iCount = p_d3d->GetAdapterCount ();
	Adapter = new cAdapter [cAdapter::iCount];
	for (i=0; i<cAdapter::iCount; i++)
	{
		D3DADAPTER_IDENTIFIER8 info;
		p_d3d->GetAdapterIdentifier (i, D3DENUM_NO_WHQL_LEVEL, &info);
		sprintf (Adapter[i].desc, "%s", info.Description);

		D3DCAPS8 pCaps;
			
		for (j=0; j<iDevType; j++)
		{
			if(SUCCEEDED(p_d3d->GetDeviceCaps (i, DeviceTypes[j], &pCaps)))
			{
				Adapter[i].bDevice[j] = true;
				if(pCaps.Caps2 & D3DCAPS2_CANRENDERWINDOWED)
					Adapter[i].bDeviceW[j] = true;
			};

			for (k=0; k<iFormatType; k++)
			{
				if(SUCCEEDED(p_d3d->CheckDeviceType (i, DeviceTypes[j],
 						FormatTypes[k], FormatTypes[k], true)))
				{
					Adapter[i].bFormat[j][k] = true;

					for (l=0; l<iDepthType; l++)
					{
					if(SUCCEEDED(p_d3d->CheckDeviceFormat (i, DeviceTypes[j],
 						FormatTypes[k], D3DUSAGE_DEPTHSTENCIL,
 						D3DRTYPE_SURFACE, DepthTypes[l])))
					if(SUCCEEDED(p_d3d->CheckDepthStencilMatch (i,
 							DeviceTypes[j], FormatTypes[k],
 							FormatTypes[k], DepthTypes[l])))
						Adapter[i].bDepthFormat[j][k][l] = true;
		};	};	};	};



		Adapter[i].cMode::iCount = p_d3d->GetAdapterModeCount (i);
		Adapter[i].cMode::iCount++; // 0 - current desktop format
		Adapter[i].mode = new cAdapter::cMode [Adapter[i].cMode::iCount];

		D3DDISPLAYMODE td3dm;
		p_d3d->GetAdapterDisplayMode (i, &td3dm);
		Adapter[i].mode[0].Width = td3dm.Width;
		Adapter[i].mode[0].Height = td3dm.Height;
		Adapter[i].mode[0].Refresh = td3dm.RefreshRate;
		Adapter[i].mode[0].Format = td3dm.Format;

		for (j=1; j<Adapter[i].cMode::iCount; j++)
		{
			p_d3d->EnumAdapterModes (i, j, &td3dm);
			Adapter[i].mode[j].Width = td3dm.Width;
			Adapter[i].mode[j].Height = td3dm.Height;
			Adapter[i].mode[j].Refresh = td3dm.RefreshRate;
			Adapter[i].mode[j].Format = td3dm.Format;
		};

		for (j=0; j<Adapter[i].cMode::iCount; j++)
		{
			int bpp=0;
			D3DFORMAT tFormat = Adapter[i].mode[j].Format;

			if(tFormat == D3DFMT_X1R5G5B5) bpp=16;
			else if(tFormat == D3DFMT_R5G6B5) bpp=16;
			else if(tFormat == D3DFMT_R8G8B8) bpp=24;
			else if(tFormat == D3DFMT_X8R8G8B8) bpp=32;
			else if(tFormat == D3DFMT_A8R8G8B8) bpp=32;

			Adapter[i].mode[j].BitDepth = bpp;
};	};	};>/pre>

Вы видите, как происходит весь процесс. Надеюсь вам в коде все понятно, но постараюсь все же объяснить по порядку, что и как делается.

Во первых весь процесс происходит только один раз, при загрузке программы, а также включена проверка на количество обнаруженных видеоадаптеров, если их ноль - т.е. ни одного еще нет, то производим их поиск.

Следующими строками мы узнаем, сколько видеоадаптеров у нас есть, и соответственно выделяем необходимый объем памяти. Весь последующий процесс происходит одинаково для всех найденных карт.

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

Затем, для каждого из устройств рендеринга (которых два) мы находим, какие форматы поверхностей и глубинных буферов оно поддерживает.

Для начала мы выясняем, поддерживается ли такое устройство вообще, и если поддерживается, то может ли оно производить рендеринг в оконном режиме.

Далее мы проверяем, какие форматы поверхностей рендеринга данное устройство поддерживает. Если мы узнаем, что какой либо формат поддерживается, то следующим шагом у нас будет обнаружение всех доступных форматов буфера глубины, поддерживающихся данным устройством рендеринга для данного формата поверхности.

Собственно на этом работа по обнаружению устройств и форматов всех мастей для текущей видео карты закончена. Далее весь процесс сосредотачивается на обнаружении доступных видеорежимов.

Делается это достаточно просто.

Сперва мы выясняем, сколько видеорежимов вообще доступны нам для этой видео карты, прибавляем к полученному числу единицу (это нужно для хранения всех доступных видеорежимов, плюс хранение информации о текущем разрешении обычно рабочего стола), и выделяем требуемый объем памяти.

Затем мы получаем информацию о текущем разрешении и заносим эту информацию под нулевым номером. Далее мы подобным образом получаем информацию о всех остальных доступных нам видеорежимах, и сохраняем всю эту информацию.

Последним шагом я произвожу как бы отборку, какие глубины пикселей у нас присутствуют. Я делаю это для удобства, нежели по большой необходимости, вообще же это не обязательно.

И конечно же, чуть не забыл, проверку - поддерживает ли выбранная видеокарта аппаратную реалаизаю TnL, и если поддерживает, мы даем возможность это использовать :

Button_Enable (hUseHardwareTnL, false);
i = ComboBox_GetCurSel (hListVideoAdapter);
j = ComboBox_GetCurSel (hListRenderingDevice);
if(SUCCEEDED(p_d3d->GetDeviceCaps (i, DeviceTypes[j], &pCaps)))
{
    if (pCaps.DevCaps & D3DDEVCAPS_HWTRANSFORMANDLIGHT) Button_Enable (hUseHardwareTnL, true);
};

Как вы еще должны были заметить, в коде использовались несколько массивов, в которых содержадась информация о форматах. Вот их объявление и инициализация:

const D3DDEVTYPE DeviceTypes[] = { D3DDEVTYPE_HAL, D3DDEVTYPE_REF };
const D3DFORMAT FormatTypes[] = { D3DFMT_X1R5G5B5, D3DFMT_R5G6B5, D3DFMT_R8G8B8,
 				D3DFMT_X8R8G8B8, D3DFMT_A8R8G8B8 };
const D3DFORMAT DepthTypes[] = { D3DFMT_D16_LOCKABLE, D3DFMT_D16,
 				D3DFMT_D15S1, D3DFMT_D24X8, D3DFMT_D24S8,
 				D3DFMT_D24X4S4, D3DFMT_D32 };
const char* sDeviceTypes[] = { "HAL", "REF" };
const char* sFormatTypes[] = { "16 bit Full Screen Mode",
 				"16 bit Full Screen Mode",
 				"24 bit Full Screen Mode",
 				"32 bit Full Screen Mode",
 				"32 bit Full Screen Mode" };
const char* sDepthTypes[] = { "D3DFMT_D16_LOCKABLE", "D3DFMT_D16",
 				"D3DFMT_D15S1", "D3DFMT_D24X8",
 				"D3DFMT_D24S8", "D3DFMT_D24X4S4", "D3DFMT_D32" };

В них просто хранится информация о возможных форматах, а также их текстовое написание для использования, например, на диалоге.

В принципе это вся информация для данного урока. Вы можете скачать полный проект конфигуратора, с исходниками, а также проект программы, которая использует полученный конфигурационный файл. Это та же самая программа, как и в прошлых уроках, немного подправленная, из нее удален диалог, который появлялся при загрузке, и он использует вышеописанный класс и методы для обнаружения возможностей, а также в него добавлена возможность читать конфигурационные данные их файла, которые потом используются при инициализации и последующей работе. Собственно сам клас не категорически такой же, он немного изменился - но совсем немного.