Графика для Windows средствами DirectDraw

       

Битные поверхности


Начнем с самой простой из четырех функций, Copy_Bmp08_Surface08(). Она выглядит так:


BOOL DirectDrawWin::Copy_Bmp08_Surface08( LPDIRECTDRAWSURFACE surf, BYTE* bmpbuf, int w, int h ) { if (surf==0 || bmpbuf==0) return FALSE; DDSURFACEDESC desc; ZeroMemory( &desc, sizeof(desc) ); desc.dwSize = sizeof(desc); HRESULT r=surf->Lock( 0, &desc, DDLOCK_WAIT | DDLOCK_WRITEONLY, 0 ); if (r!=DD_OK) { TRACE("ShowBmp: Lock() failed\n"); return FALSE; } int bytesgiven=(w+3) & ~3; BYTE* surfbits = (BYTE*)desc.lpSurface; BYTE* imagebits = (BYTE*)(&bmpbuf[(h-1)*bytesgiven]); for( int i=0; i<h; i++ ) { memcpy( surfbits, imagebits, w ); surfbits += desc.lPitch; imagebits -= bytesgiven; } surf->Unlock( 0 ); return TRUE; }


После проверки обоих аргументов-указателей мы подготавливаем экземпляр структуры DDSURFACEDESC (desc) и используем его при вызове функции Lock() интерфейса DirectDrawSurface. После возвращения из функции Lock() поле lpSurface содержит указатель на память поверхности, и мы можем спокойно изменять содержимое поверхности через этот указатель до вызова Unlock(). Безопасная работа с поверхностью стала возможной только потому, что мы указали флаг DDLOCK_WRITEONLY. Если вы собираетесь осуществлять и чтение, и запись, не устанавливайте этот флаг.

Далее мы инициализируем целую переменную bytesgiven. Присваиваемое значение определяется шириной изображения (w), выровненного по границе параграфа. Получившаяся величина равна объему памяти, необходимой для хранения одной строки пикселей. Если ширина изображения кратна четырем, переменная bytesgiven совпадает с w.

Указатель на поверхность (surfbits) инициализируется значением поля lpSurface. Этот указатель используется для обращений к памяти поверхности. Указатель на графические данные (imagebits) инициализируется адресом последней строки пикселей BMP-файла, поскольку в формате BMP изображение хранится в перевернутом виде.

Затем мы в цикле перебираем все строки пикселей изображения.
Благодаря тому, что формат графических данных BMP-файла совпадает с форматом поверхности, для копирования можно воспользоваться функцией memcopy(). Для поверхностей остальных типов такая удобная возможность часто отсутствует. Поле lPitch определяет смещение для указателя на поверхность при переходе к следующей строке. Вспомните, что в этом поле хранится шаг поверхности, который может не совпадать с ее шириной. Целая переменная bytesgiven аналогичным образом используется для перехода к следующей строке буфера графических данных. Поскольку чтение начинается с конца буфера, указатель imagebits уменьшается с каждой очередной итерацией.

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



Битные поверхности


Загрузка 8-битных изображений выполняется достаточно просто. Давайте перейдем к 16-битным поверхностям, с ними дело обстоит значительно сложнее. Помимо учета разных типов 16-битных форматов пикселей нам придется сокращать количество цветов. 24-битные данные передаются на 16-битную поверхность, поэтому во время передачи необходимо «урезать» каждую цветовую составляющую. Функция Copy_Bmp24_Surface16() приведена в листинге 5.2.

Листинг 5.2. Функция Copy_Bmp24_Surface16()


BOOL DirectDrawWin::Copy_Bmp24_Surface16( LPDIRECTDRAWSURFACE surf, BYTE* bmpbuf, int w, int h ) { if (surf==0 || bmpbuf==0) return FALSE;

DDSURFACEDESC desc; ZeroMemory( &desc, sizeof(desc) ); desc.dwSize = sizeof(desc); HRESULT r=surf->Lock( 0, &desc, DDLOCK_WAIT | DDLOCK_WRITEONLY, 0 ); if (r!=DD_OK) { TRACE("Copy_Bmp24_Surface16: Lock() failed\n"); return FALSE; } int bytesrequired=w*3; int bytesgiven=(bytesrequired+3) & ~3; BYTE* surfbits = (BYTE*)desc.lpSurface; BYTE* imagebits = (BYTE*)(&bmpbuf[(h-1)*bytesgiven]);

float REDdiv=(float)256/(float)pow( 2, numREDbits ); float GREENdiv=(float)256/(float)pow( 2, numGREENbits ); float BLUEdiv=(float)256/(float)pow(2, numBLUEbits );

for( int i=0; i<h; i++ ) { USHORT* pixptr=(unsigned short*)surfbits; RGBTRIPLE* triple=(RGBTRIPLE*)imagebits; for (int p=0;p<w;p++) { float rf=(float)triple->rgbtRed/REDdiv; float gf=(float)triple->rgbtGreen/GREENdiv; float bf=(float)triple->rgbtBlue/BLUEdiv;

WORD r=(WORD)((WORD)rf<<loREDbit); WORD g=(WORD)((WORD)gf<<loGREENbit); WORD b=(WORD)((WORD)bf<<loBLUEbit); *pixptr = (WORD)(r|g|b); triple++; pixptr++; } surfbits += desc.lPitch; imagebits –= bytesgiven; } surf->Unlock( 0 );

return TRUE; }




Хотя по своей структуре функция Copy_Bmp24_Surface16() напоминает Copy_Bmp 08_Surface08(), она устроена сложнее по причинам, уже упоминавшимся, а также потому, что значение каждого пикселя приходится задавать отдельно. Давайте посмотрим, что происходит в этой функции.



Сначала функция Lock() интерфейса DirectDrawSurface используется для получения указателя на поверхность. Затем мы инициализируем две целые переменные, bytesrequired и bytesgiven. Значение bytesrequired равно количеству байт, необходимых для представления строки пикселей. Поскольку мы работаем с 24-битными пикселями, для получения этой величины достаточно умножить ширину изображения на три (по три байта на пиксель). По значению bytesrequired рассчитывается значение bytesgiven, которое равно количеству байт для хранения строки пикселей в памяти (с учетом выравнивания по границе параграфа). Значение bytesgiven используется для перебора строк пикселей в графических данных BMP-файла.

Затем мы инициализируем указатели surfbits и imagebits; первый указывает на память поверхности, а второй - на буфер графических данных. Как и в функции Copy_Bmp08_Surface08(), imagebits указывает на последнюю строку буфера.

Три следующие строки связаны с сокращением цветов. Мы вычисляем три величины (по одной для каждой цветовой составляющей), которые будут использоваться для обработки составляющих, полученных из буфера графических данных. Они зависят от количества бит в представлении каждой цветовой компоненты на поверхности (чаще всего 5 или 6 бит). Обратите внимание на то, что эти величины вычисляются за пределами цикла. Операция деления и вызов функции pow() внутри цикла могли бы существенно замедлить работу программы.

Назначение пикселей происходит во вложенном цикле. Внешний цикл перебирает строки пикселей, а внутренний задает значение для каждого пикселя строки. Внутренний цикл инициализирует два указателя, pixptr и triple, которые используются для обращения к текущему пикселю. Переменная pixptr указывает на память поверхности, а triple - на буфер графических данных. Обратите внимание - pixptr объявлен как указатель на 16-битный тип USHORT. В этом случае для перехода к следующему пикселю достаточно увеличить значение указателя. Аналогично triple указывает на 24-битный тип RGBTRIPLE.

Внутренний цикл извлекает три цветовые составляющие каждого пикселя и делит их на ранее вычисленную величину. Значения с плавающей точкой, использованные при вычислениях, преобразуются к целым и сдвигаются к нужной позиции в соответствии с переменными loREDbit, loGREENbit и loBLUEbit. Окончательный результат представляет собой тройку «урезанных» цветовых составляющих. Побитовый оператор OR упаковывает составляющие в единую величину, и результат заносится в память поверхности. Указатели pixptr и triple инкрементируются для перехода к следующему пикселю.



Битные поверхности


Мы рассмотрели доступ к 16-битным поверхностям, и все самое сложное осталось позади. Для 24- и 32-битных поверхностей сокращение цветов уже не требуется, поэтому вычислить значение пикселя оказывается проще. В основном нам нужно лишь извлечь цветовые составляющие и сдвинуть их в позицию, определяемую расположением и форматом пикселя. Для 24-битных поверхностей процесс можно оптимизировать, если формат пикселей поверхности совпадает с форматом пикселей BMP-файла. 24-битные поверхности обрабатываются функцией Copy_Bmp24_Surface24() (см. листинг 5.3).

Листинг 5.3. Функция Copy_Bmp24_Surface24()


BOOL DirectDrawWin::Copy_Bmp24_Surface24( LPDIRECTDRAWSURFACE surf, BYTE* bmpbuf, int w, int h ) { if (surf==0 || bmpbuf==0) return FALSE;

DDSURFACEDESC desc; ZeroMemory( &desc, sizeof(desc) ); desc.dwSize = sizeof(desc); HRESULT r=surf->Lock( 0, &desc, DDLOCK_WAIT | DDLOCK_WRITEONLY, 0 ); if (r!=DD_OK) { TRACE("Copy_Bmp24_Surface24: Lock() failed\n"); return FALSE; } int bytesrequired=w*3; int bytesgiven=(bytesrequired+3) & ~3; BYTE* surfbits = (BYTE*)desc.lpSurface; BYTE* imagebits = (BYTE*)(&bmpbuf[(h-1)*bytesgiven]);

// Проверить, совпадает ли формат файла с форматом поверхности // Если совпадает, пересылку можно ускорить функцией memcpy() if (loREDbit==16 && loGREENbit==8 && loBLUEbit==0) { TRACE("using optimized code...\n"); for (int i=0;i<h;i++) { memcpy( surfbits, imagebits, bytesrequired ); surfbits += desc.lPitch; imagebits -= bytesgiven; } } else { TRACE("not using optimated code...\n"); for(int i=0; i<h; i++ ) { RGBTRIPLE* surf=(RGBTRIPLE*)surfbits; RGBTRIPLE* image=(RGBTRIPLE*)imagebits; for (int p=0;p<w;p++) { DWORD r=image->rgbtRed << loREDbit; DWORD g=image->rgbtGreen << loGREENbit; DWORD b=image->rgbtBlue << loBLUEbit; DWORD* data=(DWORD*)surf; *data = r|g|b; surf++; image++; } surfbits += desc.lPitch; imagebits -= bytesgiven; } } surf->Unlock( 0 );

return TRUE; }


Функция Copy_Bmp24_Surface24() учитывает две возможные ситуации. Если формат пикселей поверхности совпадает с форматом графических данных, целые строки пикселей копируются в цикле функцией memcpy() без всяких изменений. В противном случае используется второй цикл.

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

Каждая цветовая составляющая извлекается из буфера графических данных и сдвигается в соответствии со значением переменных loREDbit, loGREENbit и loBLUEbit. Затем компоненты объединяются и заносятся в память поверхности. Наконец, инкрементирование указателей surf и image перемещает их к следующему пикселю.



Битные поверхности


Последняя функция, Copy_Bmp24_Surface32(), предназначена для 32-битных поверхностей и очень напоминает функцию Copy_Bmp24_Surface24(). Если бы в 32-битной поверхности все 32 бита использовались для хранения цветовых составляющих, нам пришлось бы выполнять расширение цветов, но так как используется только 24 бита, в этом нет необходимости. Функция Copy_Bmp24_Surface32() приведена в листинге 5.4.

Листинг 5.4. Функция Copy_Bmp24_Surface32()


BOOL DirectDrawWin::Copy_Bmp24_Surface32( LPDIRECTDRAWSURFACE surf, BYTE* bmpbuf, int w, int h ) { if (surf==0 || bmpbuf==0) return FALSE;

DDSURFACEDESC desc; ZeroMemory( &desc, sizeof(desc) ); desc.dwSize = sizeof(desc); HRESULT r=surf->Lock( 0, &desc, DDLOCK_WAIT | DDLOCK_WRITEONLY, 0 );

if (r!=DD_OK) { TRACE("Copy_Bmp24_Surface32: Lock() failed\n"); return FALSE; } int bytesrequired=w*3; int bytesgiven=(bytesrequired+3) & ~3; BYTE* surfbits = (BYTE*)desc.lpSurface; BYTE* imagebits = (BYTE*)(&bmpbuf[(h-1)*bytesgiven]);

for(int i=0; i<h; i++ ) { DWORD* surf=(DWORD*)surfbits; RGBTRIPLE* image=(RGBTRIPLE*)imagebits; for (int p=0;p<w;p++) { DWORD r=image->rgbtRed << loREDbit; DWORD g=image->rgbtGreen << loGREENbit; DWORD b=image->rgbtBlue << loBLUEbit; DWORD* data=(DWORD*)surf; *data = r|g|b; surf++; image++; } surfbits += desc.lPitch; imagebits -= bytesgiven; } surf->Unlock( 0 );

return TRUE; }


Для работы с пикселями каждой строки используются два указателя, surf и image. Первый является указателем на 32-битный тип DWORD и используется для перебора 32-битных пикселей в памяти поверхности. Второй является указателем на 24-битный тип RGBTRIPLE и используется для доступа к пикселям графических данных. Функция вряд ли нуждается в пояснениях, поскольку она ничем не отличается от своего аналога для 24-битных поверхностей, кроме типа указателя surf и отсутствия оптимизированного варианта цикла.



Активизация видеорежима


На предпоследнем этапе происходит активизация выбранного режима. Для этого используется функция ActivateDisplayMode(), которая на самом деле выполняет и задачу последнего этапа (создание поверхностей приложения). Код этой функции приведен в листинге 3.2.

Листинг 3.2. Функция ActivateDisplayMode()


BOOL DirectDrawWin::ActivateDisplayMode( int mode ) { if ( mode<0 || mode>=totaldisplaymodes ) return FALSE;

DWORD width = displaymode[mode].width; DWORD height = displaymode[mode].height; DWORD depth = displaymode[mode].depth;

displayrect.left=0; displayrect.top=0; displayrect.right=width; displayrect.bottom=height; displaydepth=depth;

ddraw2->SetDisplayMode( width, height, depth, rate, 0 ); curdisplaymode = mode;

TRACE("------------------- %dx%dx%d (%dhz) ---------------\n", width, height, depth, rate); if (CreateFlippingSurfaces()==FALSE) { FatalError("CreateFlippingSurfaces() failed"); return FALSE; }

StorePixelFormatData(); if (CreateCustomSurfaces()==FALSE) { FatalError("CreateCustomSurfaces() failed"); return FALSE; }

return TRUE; }


Нужный видеорежим определяется параметром mode, который сначала проверяется на правильность. Затем его ширина, высота и глубина извлекаются из массива displaymode и заносятся в переменные displayrect и displaydepth. Доступ к этим переменным в производных классах осуществляется с помощью функций GetDisplayRect() и GetDisplayDepth().

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

Перед тем как рассматривать оставшуюся часть функции, следует сделать одно важное замечание. До сих пор, если функция заканчивалась неудачей и требовалось вывести сообщение, можно было использовать функцию MFC AfxMessageBox(). Пока видеорежим не изменялся, все было нормально, но после изменения видеорежима для вывода сообщений и завершения программы применяется функция FatalError(). Эта функция класса DirectDrawWin восстанавливает видеорежим Windows, выводит окно сообщения и завершает программу.



Аннотация


Книга, предназначенная для опытных программистов, раскрывает секреты создания сложных графических приложений в среде Windows 95 и Windows NT. В качестве средства разработки описана последняя версия библиотеки DirectDraw, которая образует идеальную основу для программирования приложений с быстрой графикой, в первую очередь - компьютерных игр. Воспроизведение AVI-файлов, эффективная проверка спрайтовых столкновений, отображение курсора в приложениях - многочисленные примеры наглядно демонстрируют эти и другие нетривиальные возможности DirectDraw. В книге также описана работа с DirectInput - другой, менее известной библиотекой семейства DirectX, предназначенной для получения данных от внешних устройств в обход традиционных механизмов Windows. Прилагаемый к книге компакт-диск содержит множество готовых графических приложений, исходный код всех примеров из книги, свободно распространяемое программное обеспечение DirectDraw и другие полезные программные инструменты.

Ну, а теперь я скажу пару слов... Книга взята с www.piter.com с письменного разрешения. Привожу ответ на мою проcьбу:

Добрый день.

Вы можете разместить на своем сайте данную книгу, при условии указания

источника и ссылки на сайт www.piter.com

Мы также будем рады видеть Ваш сайт в числе партнеров издательства.

Условия партнерской программы описаны тут:

http://shop.piter.com/ePartners/

BOLT> Здравствуйте уважаемый издательский дом "Питер".

BOLT> На вашем сайте (http://shop.piter.com) есть книга "Библиотека: С.

BOLT> Трухильо. Графика для Windows, б-ка программиста". Прошу у вас

BOLT> разрешение скачать книгу и опубликовать ее на моем сайте

BOLT> (http://prog.dax.ru).

--

С уважением,

Татьяна Кобельская mailto:kotavi@piter.com

Издательский Дом "Питер"

Менеджер интернет-проектов

http://www.piter.com

http://www.iworld.ru

http://yp.piter.com

http://psy.piter.com

тел. (812) 327-1311

факс (812) 294-13-15



Так, с формальностями покончили, хочу сказать в чем моя заслуга... Собственно, сама книга была в плачевном состоянии, т.е. все исходники (примеры) были испорчены редактором этой книги в электронном варианте. Если не верите, то сходите на сайт издателя, то бишь www.piter.com и посмотрите. Я исправил исходники, "подсветил" в стиле MS Visual Studio (мой любимый стиль). Собрал книгу в удобный вариант для чтения. Теперь вам не придется проклинать редактора этой книги.

Многие, скажут, что книжонка бесполезна, т.к. на дворе уже 2003, а книга была издана в 1998 (в США - 1997). Но я ради эксперимента запустил на своем Windows XP(DirectX 8.1) примеры из этой книги, которые разрабатывались для Windows 95(DirectX 5.0) и, о чудо, они прекрасно работают... Так что по этой книге можно начинающим свободно учится. Затруднения может лишь вызвать среда разработки. Поясню: все примеры в книги разработаны специально для MS Visual C++ 5.0, а сейчас уже в ходу MS Visual Studio (или 7.0), но я думаю что нетрудно будет перенести код. Также можно поступить и с совместимостью DirectX 8.1. В заголовочных файлах современной библиотеки DirectX предусмотрен механизм совместимости с кодом для ранних версий (С этим вы познакомитесь в книге).

Ну что ж, думаю, я вас заинтересовал. Тогда в путь - в чудесный мир компьютерной графики.

P.S.
Не забудьте посетить мой сайт - www.prog.dax.ru. Со мной можно связаться по e-mail.

О! Чуть не забыл. Все исходники, откомпилированные приложения и др. файлы для этой книги можно взять с моего сайта.
С уважением, BOLT.



Аппаратная часть быстрее программной


Если вы уже видели хорошее приложение DirectDraw за работой, то уже знаете это. Приложение, которое работает в режиме 640x480x16 и при этом обеспечивает вывод 60 кадров в секунду, было бы невозможно написать без аппаратного ускорения. Так как DirectDraw автоматически использует все возможности для аппаратного ускорения, вам даже не придется беспокоиться на этот счет.

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



Аппаратное ускорение


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



Аппаратные требования


Вам потребуется компьютер с процессором Pentium и выше. Под Windows NT необходимо иметь 32 Мбайт RAM, а под Windows 95 — не менее 16 Мбайт. Общий принцип остается прежним — чем больше, тем лучше. Также потребуется дисковод CD-ROM.

Наконец, понадобится видеокарта, поддерживаемая библиотекой DirectDraw (на данный момент DirectDraw поддерживают практически все современные видеокарты).

Пора заняться делом. Начнем с краткого курса DirectDraw.

Следующая глава

С. Трухильо. Графика для Windows, б-ка программистаwww.piter.comprog.dax.ru



AVI-файлы


Формат AVI (Audio Video Interleave) был разработан Microsoft для хранения и воспроизведения видеороликов. AVI-файл кроме последовательности растров содержит одну или несколько звуковых дорожек, сопровождающих видеоролик. В этой главе нам придется часто пользоваться AVI-файлами.

Почему для хранения видеоинформации нужен специальный файловый формат? Почему нельзя представить видеоролик в виде последовательности растров и звукового файла? Существует несколько причин, но, видимо, самая важная из них — сжатие. Если кадры видеоролика будут храниться без сжатия (например, в BMP-файлах), то даже относительно короткий ролик будет занимать слишком много места. Например, 1-минутный ролик при разрешении 320ґ200 и частоте вывода 30 кадров в секунду займет на диске свыше 100 Мбайт.

Разумеется, для практического применения видеороликов необходимо сжатие. Возникает вопрос: какое именно? Существует много различных алгоритмов сжатия, каждый из которых обладает своими достоинствами и недостатками. Маловероятно, чтобы один алгоритм подошел сразу по всем критериям.

Поэтому в AVI-файлах была предусмотрена поддержка разнообразных методов сжатия. Конечно, без развитого API чтение AVI-файлов было бы очень сложной задачей. К счастью, в Windows существует специальный API Video For Windows, который мы вскоре рассмотрим.



Блиттинг


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

В блиттинге обычно участвуют две поверхности: источник (source) и приемник (destination). Содержимое поверхности-источника копируется в поверхность-приемник. В результате операции содержимое поверхности-источника остается неизменным; блиттинг влияет лишь на поверхность-приемник. Кроме того, блиттинг не всегда изменяет все содержимое приемника; любой прямоугольный фрагмент источника можно скопировать в любое место приемника.

При блиттинге непрямоугольных областей (например, спрайтов) применяется эффект прозрачности. Для этого некоторые пиксели поверхности помечаются так, чтобы они не копировались в ходе блиттинга. Такая пометка осуществляется с помощью цветовых ключей (color key).

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

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

К сожалению, некоторые возможности вообще не эмулируются DirectDraw (например, цветовые ключи приемника). Их использование оказывается рискованным, потому что без поддержки со стороны видеокарты такие операции могут закончиться неудачей. У разработчика остаются два варианта: отказаться от данной возможности или включить в приложение собственную программную эмуляцию.



Блокировка поверхностей


Для прямого доступа к поверхности необходимо предварительно вызвать функцию Lock() интерфейса DirectDrawSurface. Lock() получает экземпляр структуры DDSURFACEDESC и возвращает указатель на левый верхний пиксель поверхности, шаг поверхности, ее размеры и даже формат пикселей (структура DDSURFACEDESC содержит экземпляр DDPIXELFORMAT, поэтому вызов GetPixelFormat() интерфейса DirectDrawSurface оказывается необязательным). Прототип функции Lock() выглядит так:


HRESULT Lock(LPRECT rect, LPDDSURFACEDESC desc, DWORD flags, HANDLE event);


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

Второй аргумент функции Lock() - структура DDSURFACEDESC, которая используется для возвращения указателя на память поверхности (поле lpSurface) и шага поверхности (поле lPitch). Функция Lock() (как и другие функции DirectDraw) требует правильно присвоить значение полю dwSize структуры DDSURFACEDESC.

Третий аргумент используется для настройки параметров Lock(). В него могут входить следующие флаги:

DDLOCK_EVENT

DDLOCK_READONLY

DDLOCK_WRITEONLY

DDLOCK_SURFACEMEMORYPTR

DDLOCK_WAIT

На момент выхода DirectX 5 флаг DDLOCK_EVENT не поддерживался. Возможно, в будущих версиях DirectDraw он будет использоваться совместно с последним аргументом Lock() для реализации альтернативного метода блокировки поверхностей.

Флаги DDLOCK_READONLY и DDLOCK_WRITEONLY следует использовать в том случае, когда доступ к памяти поверхности осуществляется исключительно для чтения или записи. В большинстве ситуаций эти флаги ни на что не действуют, однако в видеорежимах «Mode X» DirectDraw использует их для оптимизации доступа к поверхности.



Флаг DDLOCK_SURFACEMEMORYPTR необязателен, потому что он задает поведение Lock(), которое и так является стандартным. Lock() возвращает указатель на память поверхности как с этим флагом, так и без него, поэтому мы не станем использовать его в своих программах (флаг DDLOCK_SURFACEMEMORYPTR на самом деле определен равным 0, так что я нисколько не преувеличиваю, говоря, что он ни на что не влияет).

Флаг DDLOCK_WAIT показывает, что функция Lock() должна дождаться завершения блокировки в том случае, если в данный момент поверхность используется для другой цели — например, участвует в операции блиттинга или переключения поверхностей. Если этот флаг задан, Lock() работает в цикле до тех пор, пока поверхность не освободится для блокировки или пока не произойдет ошибка. При отсутствии флага DDLOCK_WAIT функция Lock() для занятой поверхности возвратит код DDERR_SURFACEBUSY, и блокировка не состоится. Для упрощения кода мы будем использовать этот флаг.




BMP-файлы


Теперь мы знаем, как получить прямой доступ к поверхности и что делать с ее памятью, чтобы изменить значения отдельных пикселей. Давайте используем полученные знания на практике. В этой главе мы напишем приложение DirectDraw для просмотра графических файлов формата BMP. Но перед тем как браться за такую программу, необходимо научиться загружать BMP-файлы.



Будущее DirectX


Сейчас можно сказать, что разработка приложений на базе DirectX оказалась сложнее, чем хотелось бы. Создание полноценных, переносимых приложений DirectX осложняется целым рядом проблем.

Самая большая проблема заключается в том, что все видеокарты обладают разными возможностями. Немало хлопот вызывает и тот факт, что DirectX продолжает изменяться. Некоторые компоненты DirectX API меняются от версии к версии, и на компьютерах пользователей устанавливаются разные runtime-части. Если этого вам кажется недостаточно, прибавьте проблемы с драйверами различных устройств.

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

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

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

Некоторые недостатки должны исчезнуть в ближайшем будущем, после интеграции DirectX в Windows. Будущие версии Windows 95, подобно Windows NT 4.0, будут содержать встроенную runtime-часть DirectX и драйверы устройств. Несомненно, дальнейшее развитие DirectX облегчит жизнь и пользователям, и разработчикам.



Быстродействие


Наверное, вас интересует, каким образом DirectInput обгоняет традиционные механизмы Windows. DirectInput однозначно приходится выбирать для устройств, не поддерживаемых Win32 API, но зачем использовать его для работы с мышью и клавиатурой?

DirectInput, как и DirectDraw, обходит традиционные механизмы Windows и обеспечивает прямой доступ к устройствам, не утрачивая аппаратной независимости. Поскольку Windows в этой схеме не используется, установленные в системе параметры устройств ввода (например, частота повтора символов для клавиатуры или чувствительность мыши) не влияют на DirectInput.



Частичное обновление экрана


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

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

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

С другой стороны, прямой вывод на первичную поверхность имеет свои недостатки и требует осторожности. Основная потенциальная проблема — расхождение. Переключение страниц выполняется так, чтобы предотвратить возможность расхождения. Следовательно, если вы обходите механизм переключения страниц и обновляете кадр, который в данный момент отображается на экране, то рискуете изменить область экрана, в данный момент обновляемую монитором. Если это произойдет, новое содержимое обновляемой части в течение некоторого времени будет выводиться одновременно со старым — возникнет расхождение.

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



Частота смены кадров


В главе 1 мы узнали, что DirectDraw позволяет приложению задать не только видеорежим, но и частоту смены кадров. Однако перед тем, как писать демонстрационную программу, следует разобраться, что же такое «частота смены кадров».

Частота смены кадров представляет собой скорость обновления экрана. Например, частота в 60 Гц означает, что экран перерисовывается 60 раз в секунду. Данный показатель представляет большой интерес для программистов, работающих с DirectDraw, потому что он управляет скоростью работы приложения. Для полноэкранных приложений DirectDraw с возможностью переключения страниц это происходит автоматически, потому что функция Flip() интерфейса DirectDrawSurface синхронизирует операцию переключения страниц с частотой смены кадров. Такая синхронизация определяет (или, если хотите, ограничивает) скорость, с которой приложение обновляет экран.

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

И все же некоторые частоты смены кадров оставляют желать лучшего. Низкие частоты (особенно ниже 60 Гц) раздражают и обычно плохо подходят для высокопроизводительных приложений. С помощью DirectDraw API ваше приложение может узнать, какие частоты поддерживаются для конкретного видеорежима, и активизировать их по мере необходимости.

Тем не менее подобные «игры» с частотами требуют определенной осторожности. Во-первых, не все видеоустройства позволяют управлять частотой смены кадров. В этом случае вам придется использовать параметры по умолчанию, какими бы они ни были. Кроме того, даже если некоторая частота поддерживается видеокартой, это еще не означает, что она поддерживается монитором. В Windows NT, где DirectDraw не проверяет тип монитора, можно легко включить видеорежим с частотой, при которой на экран выводится сплошной «мусор».
Если ваше приложение управляет частотой смены кадров, позаботьтесь о том, чтобы пользователь всегда мог вернуть исходные параметры.

Кроме аппаратных возможностей следует также учесть быстродействие самого приложения. Даже если вы установите видеорежим с частотой 100 Гц, это еще не значит, что приложение будет выводить 100 кадров в секунду. В какой-то момент оно отстанет от видеорежима, и тогда на каждое обновление экрана будет уходить два цикла. Другими словами, приложение будет пропускать циклы обновления, а FPS упадет до половины частоты смены кадров — для быстрых приложений такая потеря производительности оказывается заметной. По этой причине приложения, изменяющие частоту смены кадров, должны распознавать такие ситуации и выбирать более низкую частоту.

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

Перед тем как заниматься программой SuperSwitch, я хочу снова напомнить, что не все видеокарты позволяют выбирать частоту смены кадров. Если программа SuperSwitch не обнаруживает такой возможности, меню частот будет содержать лишь одну строку — 0 Гц, где ноль обозначает частоту, принятую для данного видеорежима по умолчанию.


Что делать с кнопками мыши?


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

Но стоит ли полностью изолировать приложение от мыши? Если основной поток ничего не знает о том, что происходит с мышью, мы не сможем пользоваться мышью в приложении.

Мы поступили правильно, удалив получение данных о перемещениях мыши из основного потока, но что делать с кнопками мыши? Необходимо придумать, как сообщать основному потоку об изменениях их состояния.

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

Поскольку очередь совместно используется двумя потоками, нам придется позаботиться о синхронизации. В каждом потоке уже присутствует поддержка критических секций, так что добиться синхронизации будет нетрудно.



Что такое DirectDraw?


Весьма интересное определение DirectDraw можно найти у одного из его самых яростных противников — FastGraph. Графический пакет FastGraph появился уже довольно давно. В настоящее время существует версия FastGraph, которая поддерживает DirectDraw, но скрывает DirectDraw API за своим собственным нестандартным API. Тед и Диана Грубер (Ted and Diana Gruber), создатели и поставщики FastGraph, разместили на своем Web-узле файл, в котором доказывается, что FastGraph лучше DirectDraw.

В числе прочих доводов Груберы заявляют, что DirectDraw представляет собой «просто механизм блиттинга». Такая формулировка оказывается довольно точной, но чрезмерно упрощенной. Правильнее было бы сказать, что DirectDraw — аппаратно-независимый механизм блиттинга, наделенный некоторыми возможностями программной эмуляции. Главная задача DirectDraw как раз и заключается в том, чтобы по возможности быстро и надежно копировать графические изображения в память видеоустройств (блиттинг).

DirectDraw можно рассматривать и с другой точки зрения - как менеджер видеопамяти. DirectDraw распределяет блоки памяти и следит за состоянием каждого блока. Программы могут по своему усмотрению создавать, копировать, изменять и уничтожать такие блоки, причем конкретные подробности этих операций остаются скрытыми от программиста. Такое описание тоже оказывается излишне упрощенным. Во-первых, DirectDraw может использовать не только видеопамять, но и обычную память (RAM). Кроме того, при проектировании менеджеров памяти основное внимание обычно уделяется надежности, а не быстродействию. При проектировании же DirectDraw главной целью было именно быстродействие.

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



Что такое DirectInput?


DirectInput представляет собой DirectX API для работы с устройствами ввода — клавиатурой, мышью, джойстиками, рулями, авиационными рукоятками, шлемами виртуальной реальности и даже устройствами с обратной связью. В полном соответствии с идеологией DirectX библиотека DirectInput проектировалась в первую очередь для реализации высокого быстродействия и аппаратной независимости.

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



Данные об осевых смещениях


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

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



Действительно ли C++ медленнее C?


Мы подходим к деликатной теме. Защитники C дружно объединяются для защиты своего любимого языка. Адепты C++ тоже настроены весьма воинственно. К сожалению, преданность такого рода нередко оборачивается неразумным фанатизмом.

Язык C известен своей эффективностью. Именно поэтому он стал первым языком высокого уровня, на котором была написана операционная система (Unix). И все же по скорости C уступает ассемблеру. Он создавался как высокоуровневая альтернатива той чрезмерной детализации, с которой связано программирование на ассемблере.

C++ был создан для того, чтобы обогатить существующий язык C конкретными функциональными возможностями и при этом воспользоваться преимуществами его скорости. Добавлений было несколько, но основным усовершенствованием стало появление объектов, или классов. Более того, первоначально C++ назывался C with classes.

Классы были включены в C++ для поддержки таких объектно-ориентированных принципов, как наследование и полиморфизм. Эти возможности уже присутствовали в других языках (например, в Smalltalk), но C++ был задуман как высокопроизводительный объектно-ориентированный язык. Smalltalk при всей своей мощности работает медленно.

Поддержка классов в C++ реализована с помощью дополнительного уровня косвенной адресации, отсутствующего в C. Доступ ко всем нестатическим переменным и функциям классов осуществляется через неявные указатели. Этот подход обеспечивает полезную возможность — полиморфизм, однако при этом возникают дополнительные накладные расходы, на которых и основано заявление «C++ медленнее C».

Итак, C++ должен быть медленнее C и, вероятно, в большинстве приложений дело обстоит именно так. Но давайте не будем забывать о пропорциях. Стоит ли беспокоиться о нескольких лишних указателях в приложении, которое каждую секунду обрабатывает мегабайты графических данных? Заботиться о быстродействии на этом уровне часто бывает ненужно, да и невыгодно.

С другой стороны, C++ не должен лишать вас здравого смысла. Не стоит пользоваться классами только ради удовольствия. Если ваша программа выиграет от того, что некоторая функциональность будет инкапсулирована внутри объекта, — пожалуйста, но превращать в объекты все, что попадается под руку, — скорее всего, перебор.

Программы в этой книге написаны на C++. Обратите внимание на умеренное использование классов. Например, вполне логично использовать класс для представления окна, потому что в MFC входит заранее написанный и протестированный оконный класс. Тем не менее способ применения этого класса более характерен для «просто C».

Вероятно, споры на тему «C против C++» продлятся еще несколько лет. После этого они утихнут, подобно тому, как утихли споры «C против ассемблера». В наши дни никто не спорит с тем, что ассемблер работает быстрее C, однако писать целые приложения на ассемблере оказывается невыгодно.



DirectDraw API


Один из способов оценить API — посмотреть на его размер. Большой, сложный API может быть результатом неудачного планирования. С другой стороны, большой API иногда свидетельствует и о том, что разработчики учли все возможные ситуации и позаботились о вас. Маленькие API нередко характерны для новых пакетов с ограниченными возможностями. С другой стороны, это может говорить и о том, что API делает только самое необходимое и ничего больше.

DirectDraw API невелик. В сущности, он настолько мал, что все его функции можно рассмотреть в одной главе (так мы и поступим), не превращая ее в справочное руководство. DirectDraw обладает некоторыми удобными средствами и подчиняется нескольким ограничениям.

Библиотека DirectDraw оформлена в виде четырех COM-объектов. Доступ к каждому объекту осуществляется через один или несколько интерфейсов. Вот их полный список:

DirectDraw

DirectDraw2

DirectDrawSurface

DirectDrawSurface2

DirectDrawSurface3

DirectDrawPalette

DirectDrawClipper

Мы рассмотрим все интерфейсы вместе с входящими в них функциями. Тем не менее этот раздел не претендует на то, чтобы заменить собой справочное руководство. Help-файл, входящий в состав DirectX SDK, несмотря на все ограничения, содержит достаточно справочной информации, так что мы не станем подробно рассматривать все функции, а поговорим вместо этого о том, что делает каждая функция, для чего и с какой вероятностью она вам может понадобиться.



DirectDraw AppWizard


Если вы работали с Visual C++, то вам почти наверняка приходилось пользоваться AppWizard. AppWizard представляет собой специальное приложение, работающее в среде Visual C++. По вашим указаниям он генерирует целый проект вместе со всеми исходными текстами. Хотя сам термин подразумевает, что AppWizard генерируют только приложения, некоторые из них создают библиотеки, управляющие элементы OLE и даже другие AppWizard. Важно понять, что AppWizard позволяет быстро и легко заложить основу для работы над проектом. Несколько AppWizard поставляются вместе с Visual C++, самый известный из них предназначен для создания MFC-приложений.

Наш нестандартный DirectDraw AppWizard создает приложения DirectDraw. Для установки DirectDraw AppWizard можно воспользоваться инсталляционной программой с CD-ROM или просто скопировать с диска файл AppWizard (с расширением AWX) в каталог с шаблонами Visual C++.



DirectInput API


До выхода DirectX 3 библиотека DirectInput была построена на существующих функциях Win32 и не поддерживала ввода с клавиатуры или от мыши. В DirectX 3 появились COM-интерфейсы для клавиатуры и мыши, но все остальные устройства продолжали зависеть от функций Win32 (и особенно от функции joyGetPosEx()). В DirectX 5 зависимость DirectInput от Win32 полностью устранена, а все устройства ввода переведены на использование COM-интерфейсов. Работа с устройствами ввода реализована через три интерфейса:

DirectInput

DirectInputDevice

DirectInputEffect

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

Интерфейс DirectInputDevice представляет устройство ввода. Его функции выполняют инициализацию, настройку, захват и отпускание устройств. Что еще важнее, DirectInputDevice содержит функции для получения данных от устройства.

Интерфейс DirectInputEffect применяется для обслуживания устройств с обратной связью. В этой книге он не используется.



DirectX SDK


В DirectX SDK входят все файлы, необходимые для разработки приложений на основе DirectX. В SDK также входит ряд демонстрационных программ и справочных файлов, но эти ресурсы являются необязательными. Необходимо лишь наличие заголовочных (.h) и библиотечных (.lib) файлов.

Найти DirectX SDK несколько сложнее, чем runtime-часть библиотеки. Этот пакет не входит в поставку Windows NT или Windows 95; его нет и на CD-ROM, прилагаемом к книге. Существуют три способа раздобыть SDK:

Купить Visual C++ 5.0 (в комплект которого входит DirectX 3 SDK).

Посетить на Web-узле Microsoft страницу для перекачки DirectX.

Подписаться на MSDN (Microsoft Development Network).

Если у вас есть Visual C++, то есть и SDK. Хотя это не самая последняя версия, ее вполне хватит для большинства материалов из этой книги.

SDK также можно получить на Web-узле Microsoft (адрес есть в предисловии). Объем пересылки оказывается довольно большим (из-за программ-примеров), поэтому приготовьтесь посвятить этому занятию целую ночь, особенно при модемном соединении.

Третий способ вполне хорош, однако кое-кто считает нелогичным платить Microsoft за право разработки приложений, отчасти способствующих продвижению их операционных систем. SDK входит в MSDN уровня 2 и выше.



Для чего написана эта книга


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

Эта книга начинается с того, на чем другие книги обычно заканчивались. Мы поговорим об основах DirectDraw, но лишь в общих чертах. Читатель— опытный программист, но незнакомый с DirectDraw — сможет с ходу войти в курс дела. Затем мы перейдем к другим темам, столь же интересным, сколь и полезным.

Цель этой книги — научить вас работать с DirectDraw, а не предоставить некоторую «структурную основу» или нестандартный API, который бы выполнял за вас всю работу. Демонстрационные программы написаны на C++ и используют MFC, но совсем не для того, чтобы скрыть все технические подробности. С++ и MFC — превосходные инструменты, потому что с их помощью любое приложение можно написать несколькими разными способами. Примеры для этой книги были написаны так, чтобы при этом получались структурированные и удобные для чтения проекты, которые наглядно показывают, что и почему происходит в программе.

Помимо DirectDraw, во многих примерах используется библиотека DirectInput. Строго говоря, при программировании графики для Windows можно обойтись и без DirectInput, но ей все же стоит воспользоваться. Она работает быстрее традиционных средств ввода Windows и к тому же входит в DirectX, так что для работы с ней не потребуется никаких дополнительных SDK.



Долой аппаратную зависимость!


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

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

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



Дополнительные интерфейсы DirectDraw


Строго говоря, DirectDraw содержит еще три интерфейса, не рассмотренных нами:

DDVideoPortContainer

DirectDrawColorControl

DirectDrawVideoPort

Эти интерфейсы, появившиеся в DirectX 5, предназначены для низкоуровневого управления видеопортами. Точнее, они предоставляют средства для потоковой пересылки «живого видео» на поверхности DirectDraw. Хотя с их помощью можно организовать в приложениях DirectDraw поддержку работы с видео, это не рекомендуется, за исключением случаев, когда высокоуровневые видео-API не отвечают вашим потребностям. В книге эти интерфейсы не рассматриваются.



Другие типы поверхностей


Внеэкранные и первичные поверхности (со вторичными буферами) образуют основу всех приложений DirectDraw. Тем не менее существуют и другие разновидности поверхностей. В их число входят оверлейные поверхности, альфа-поверхности, Z-буферы и поверхности 3D-устройств.

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

Альфа-поверхности (alpha channel surface) используются для выполнения альфа-наложения (alpha blending). Альфа-наложение является более сложной формой прозрачности и позволяет осуществлять «полупрозрачное» копирование поверхностей. Альфа-поверхность может использоваться для управления прозрачностью отдельных пикселей. Такие поверхности имеют глубину в 1, 2, 4 или 8 бит. Альфа-поверхность с глубиной 1 бит поддерживает лишь два уровня прозрачности: нулевой (непрозрачный пиксель) и стопроцентный (полностью прозрачный пиксель). С другой стороны, 8-битные альфа-поверхности позволяют задавать до 256 различных степеней прозрачности. Альфа-наложение относится к числу возможностей, не эмулируемых в DirectDraw. Следовательно, для использования альфа-наложения необходимо иметь видеокарту, обладающую соответствующими аппаратными средствами, или же написать собственную функцию для программной эмуляции альфа-наложения.

Z-буферы и поверхности 3D-устройств используются в трехмерных приложениях. Эти типы поверхностей были включены в DirectDraw специально для поддержки Direct3D. Z-буферы используются при визуализации трехмерных сцен; они определяют, какие объекты сцены находятся ближе к зрителю и, следовательно, отображаются перед другими объектами. Поверхности 3D-устройств могут использоваться для синтеза трехмерных изображений в DirectDraw. Z-буферы и поверхности 3D-устройств в этой книге не рассматриваются.



Файлы DirectX SDK


Начиная с версии 4.2, Visual C++ содержит DirectX SDK. К сожалению, обычно в Visual C++ входит относительно старая версия SDK (Visual C++ 4.2 поставляется с DirectX 2 SDK, а Visual C++ 5.0 — с DirectX 3 SDK). Скорее всего, из-за этого вы будете работать с версией DirectX SDK, полученной в другом месте (например, на Web-узле Microsoft).

Тем не менее при установке новой версии SDK часто возникают проблемы, потому что по умолчанию старые файлы DirectX не замещаются новыми. Это происходит из-за того, что Microsoft помещает файлы DirectX в стандартные каталоги include и lib (если файлы MFC хранятся в отдельных каталогах, почему Microsoft не могла сделать того же с файлами DirectX?).

Если вы столкнетесь с этой проблемой (это произойдет, когда программа, использующая новые возможности DirectX, откажется компилироваться), у вас есть два варианта:

скопировать новые файлы DirectX SDK поверх старых;

сделать так, чтобы новые каталоги SDK рассматривались раньше старых.

Первый вариант нежелателен, потому что стандартные файлы Visual C++ обычно не стоит изменять. Кроме того, вам придется заново копировать файлы с выходом каждой новой версии Visual C++ или DirectX SDK.

Лучше воспользоваться вторым вариантом. Для этого выполните команду Tools | Options и перейдите на вкладку Directories. Затем выберите каталог с файлами DirectX SDK и поместите его в списке над другими каталогами (с помощью кнопки ­). На рис. А.15 показано, как может выглядеть список каталогов после внесения необходимых изменений.

На рисунке изображены каталоги, которые Visual C++ просматривает в поисках включаемых файлов (список каталогов просматриваются сверху вниз). Кроме того, не забудьте аналогичным образом изменить порядок каталогов для библиотечных (LIB) файлов.


Рис. А.15. Вкладка Directories окна Options



Фокусы ClassView


ClassView — древовидный элемент окна рабочей области, в котором отображается список классов, входящих в проект (он достаточно удобен для перемещения по проекту). Узлы дерева соответствуют классам; раскрывая их, вы можете просмотреть члены класса. Хотя на первый взгляд ClassView VisualC++ 5.0 ничем не отличается от предыдущих версий, на самом деле он ведет себя несколько иначе.

В Visual C++ 5.0 ClassView отображает лишь те классы, чьи заголовочные файлы были явно включены в проект (командой Project|Add to Project|Files). Если в проект не включено ни одного H-файла, в ClassView не будет ни одного класса. Этим он отличается от Visual C++ 4.x, где выводились все классы из файлов проекта и

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

При импортировании старых проектов в Visual C++ 5.0 по умолчанию отображаются все классы. Класс отсутствует в ClassView лишь в том случае, если CPP-файл был включен в проект без соответствующего H-файла.



Формат BMP-файлов


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

BMP-файлы состоят из трех основных частей:

заголовок;

палитра;

графические данные (значения пикселей).

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

Палитра присутствует только в BMP-файлах, содержащих палитровые изображения (с глубиной пикселей 8 бит и менее). К 8-битным изображениям прикладывается палитра, состоящая из не более чем 256 элементов.

Графические данные — это и есть само изображение. Формат этих данных зависит от глубины пикселей. Хотя BMP-файлы делятся на несколько типов, мы ограничимся 8-битными и 24-битными изображениями. 8-битные BMP-файлы будут использоваться для работы с 8-битными поверхностями, а 24-битные — для беспалитровых поверхностей. Хотя, по слухам, в природе существуют 16-битные и 32-битные BMP-файлы, они встречаются очень редко — например, мне таковые ни разу не попадались. Впрочем, это не имеет особого значения, так как 24-битную графику можно легко преобразовать в 16- или 32-битный формат.



Форматы пикселей


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

В 16-битных пикселях хранится непосредственно цветовая информация. 16-битные форматы пикселей делятся на две основные разновидности: в первой каждая RGB-составляющая представлена 5 битами (оставшийся бит не используется), а во второй 5 битами представлены только красная и синяя составляющие, а для зеленой используется 6 бит. Для этих форматов часто применяются условные обозначения 5-5-5 и 5-6-5 соответственно.

В формате 5-5-5 каждая цветовая составляющая может принимать значения из интервала от 0 до 31. Чем больше значение составляющей, тем интенсивнее она проявляется в результирующем цвете. Формат 5-6-5 работает аналогично, за исключением того, что зеленая составляющая может принимать значения из интервала 0–63. Эти два вида 16-битных пикселей изображены на рис. 5.5.

Дело осложняется тем, что форматы 5-5-5 и 5-6-5 тоже делятся на два типа. На рис. 5.5 изображен RGB -формат, в котором цветовые составляющие хранятся в порядке «красный, зеленый, синий». Также существует BGR-формат, в котором красная и синяя составляющая меняются местами. Вряд ли в ближайшем будущем появятся еще какие-нибудь варианты.


Рис. 5.5. Два распространенных 16-битных формата пикселей

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



С пикселями формата True Color работать проще, потому что каждая из RGB-составляющих представлена одним байтом. В этом случае значение каждой составляющей принадлежит интервалу 0–255; ноль означает, что составляющая вообще не влияет на результирующий цвет, а 255 - что ее влияние максимально. Форматы пикселей для 24- и 32-битных вариантов изображены на
рис. 5.6.

24- и 32-битные пиксели, как и 16-битные, делятся на две разновидности: RGB и BGR. Следовательно, код для работы с пикселями True Color должен использовать сведения о формате, полученные от DirectDraw, и не делать никаких безусловных предположений.

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



Рис. 5.6. Типичные форматы пикселей True Color




FPS - еще не все


В наши дни часто приходится слышать о частоте вывода кадров, или FPS (Frames Per Second, количество кадров в секунду). Этот показатель стал мерой для сравнения графических Windows-приложений. Конечно, DirectDraw позволяет создавать приложения с высоким FPS, однако не стоит переоценивать важность этой характеристики.

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

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



Функции блиттинга


Интерфейс DirectDrawSurface поддерживает три функции, предназначенные для выполнения блиттинга:

Blt()

BltBatch()

BltFast()

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

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

Функция BltFast() является оптимизированным вариантом функции Blt(). Повышение эффективности достигается за счет сокращения возможностей, поэтому BltFast() не умеет выполнять специальные операции блиттинга, поддерживаемые функцией Blt(). Кроме того, BltFast() не выполняет отсечения. Тем не менее функция BltFast() поддерживает блиттинг с использованием цветовых ключей источника и приемника. В сочетании с нестандартными алгоритмами отсечения функция BltFast() обеспечивает выполнение самого быстрого и универсального блиттинга, которого можно добиться от DirectDraw. Нестандартный алгоритм отсечения будет реализован в главе 3.

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



Функции для работы с частотой смены кадров


Интерфейс DirectDraw содержит четыре функции, относящихся не к видеокарте, а к устройству отображения (монитору):

GetMonitorFrequency()

GetScanLine()

GetVerticalBlankStatus()

WaitForVerticalBlank()

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

Функция GetMonitorFrequency() возвращает текущую частоту смены кадров монитора. Эта частота обычно измеряется в герцах (Гц). Например, частота в 60 Гц означает, что состояние экрана обновляется 60 раз в секунду.

Функция GetScanLine() возвращает номер строки развертки (горизонтального ряда пикселей), обновляемой в данный момент. Она не поддерживается некоторыми комбинациями видеокарт и мониторов. Если данная способность не поддерживается, функция возвращает код ошибки DDERR_UNSUPPORTED.

В высокопроизводительных графических приложениях обновление экрана обычно синхронизируется с процессом вертикальной развертки. Другими словами, первичную поверхность желательно обновлять в тот момент, когда монитор закончил очередное обновление экрана. В противном случае в одной части экрана будут отображаться новые данные, а в другой — старые. Подобный эффект называется расхождением (tearing). По умолчанию DirectDraw автоматически синхронизирует обновление экрана с завершением вертикальной развертки. В нестандартных ситуациях можно добиться синхронизации с помощью функций GetVerticalBlankStatus() и WaitForVerticalBlank().



Функции для работы с цветовыми ключами


Интерфейс DirectDraw содержит две функции для назначения и просмотра цветового ключа (или интервала цветовых ключей) поверхности:

GetColorKey()

SetColorKey()

По умолчанию поверхность не имеет цветового ключа. Чаще всего цветовой ключ представляет собой один цвет, однако на некоторых видеокартах возможна работа с интервалами цветовых ключей. Цветовые ключи и их интервалы описываются структурой DDCOLORKEY. Указатели на эту структуру передаются функциям GetColorKey() и SetColorKey() в качестве аргументов. Функции GetColorKey() и SetColorKey() используются при частичном копировании поверхностей, а также при выполнении операций с цветовыми ключами приемника.



Функции для работы с объектами отсечения


DirectDraw позволяет присоединить к поверхности экземпляр интерфейса DirectDrawClipper (который мы еще не рассматривали). После того как такое присоединение состоится, операция блиттинга на данную поверхность будет регулироваться объектом отсечения. Для работы с объектами отсечения в интерфейсе DirectDrawSurface имеются две функции:

GetClipper()

SetClipper()

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



Функции для работы с поверхностями


Помимо функции CreateSurface() интерфейс DirectDraw содержит следующие функции для работы с поверхностями:

DuplicateSurface()

EnumSurfaces()

FlipToGDISurface()

GetGDISurface()

GetAvailableVidMem()

Compact()

Функция DuplicateSurface() создает копию существующей поверхности. Она копирует только интерфейс поверхности, но не ее содержимое. Копия поверхности использует ту же область памяти, поэтому модификация содержимого памяти приведет к изменению изображения, представленного обеими поверхностями.

Функция EnumSurfaces() используется для перебора всех поверхностей, удовлетворяющих заданному критерию. Если критерий не указан, составляется список всех существующих поверхностей.

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

Функция GetGDISurface() возвращает указатель на единственную поверхность, с которой работает GDI. Весь графический вывод Windows осуществляется именно на поверхность GDI. Примером ситуации, когда эта функция может оказаться полезной, является программа копирования экрана, в которой DirectDraw используется для копирования произвольной части рабочего стола.

Функция GetAvailableVidMem() возвращает объем текущей доступной видеопамяти. Эта функция присутствует в интерфейсе DirectDraw2, но отсутствует в DirectDraw. С ее помощью приложение может определить, сколько поверхностей ваше приложение сможет создать в видеопамяти.

Функция Compact() не реализована в DirectX, однако в будущем она обеспечит механизм дефрагментации видеопамяти. Если ваше приложение постоянно создает и уничтожает поверхности, находящиеся в видеопамяти, дефрагментация может высвободить немало места.


br>

Функция ClearSurface() получает три аргумента: указатель на заполняемую поверхность; величину, присваиваемую каждому пикселю; и необязательную структуру RECT, которая определяет заполняемую область поверхности.

После проверки указателя мы подготавливаем экземпляр структуры DDBLTFX. Полю dwFillColor присваивается величина, используемая для заполнения, а сама операция осуществляется функцией Blt() интерфейса DirectDrawSurface. Флаг DDBLT_COLORFILL сообщает Blt() о том, что вместо блиттинга выполняется цветовое заполнение.

Получившаяся функция удобна, но ей не хватает универсальности. Дело в том, что величина, применяемая для заполнения поверхности, может иметь различный смысл. Например, для палитровых поверхностей она представляет собой индекс в палитре. Без предварительной проверки палитры невозможно предсказать, какой цвет будет использоваться для заполнения. Аналогичные проблемы возникают и для беспалитровых поверхностей, поскольку конкретное значение пикселя зависит от глубины и формата пикселей. Форматы пикселей особенно часто различаются в режимах High Color, поэтому заполнение поверхности конкретным цветом превращается в нетривиальную задачу.

Вторая версия ClearSurface() получает в качестве аргументов RGB-составляющие и рассчитывает по ним конкретную величину, присваиваемую пикселям поверхности. В таком варианте функция становится более универсальной, но и работает медленнее; быстродействие особенно сильно снижается для палитровых поверхностей, потому что нужный цвет приходится искать в палитре. Код этой функции будет рассмотрен в главе 5.

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

BOOL DirectDrawWin::GetSurfaceDimensions( LPDIRECTDRAWSURFACE surf, DWORD& w, DWORD& h ) { if (surf==0) return FALSE;

DDSURFACEDESC desc; ZeroMemory( &desc, sizeof(desc) ); desc.dwSize=sizeof(desc); desc.dwFlags=DDSD_WIDTH | DDSD_HEIGHT;

if (surf->GetSurfaceDesc( &desc )!=DD_OK) return FALSE; w=desc.dwWidth; h=desc.dwHeight;

return TRUE; }

После проверки указателя мы подготавливаем экземпляр структуры DDSURFACEDESC. Нас интересуют ширина и высота поверхности, поэтому в поле dwFlags заносятся флаги DDSD_WIDTH и DDSD_HEIGHT.

Затем мы вызываем функцию GetSurfaceDesc() интерфейса DirectDrawSurface и передаем ей указатель на структуру с описанием поверхности. Функция GetSurfaceDesc() сохраняет размеры поверхности в полях dwWidth и dwHeight. Они присваиваются переданным по ссылке переменным w и h типа DWORD, после чего функция завершается.

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


Функции для работы с видеорежимами


Интерфейс DirectDraw содержит четыре функции для работы с видеорежимами:

EnumDisplayModes()

GetDisplayMode()

RestoreDisplayMode()

SetDisplayMode()

С помощью функции EnumDisplayModes() можно получить от DirectDraw список доступных видеорежимов. По умолчанию EnumDisplayModes() перечисляет все видеорежимы, но по описаниям можно исключить из списка режимы, не представляющие для вас интереса. Функция EnumDisplayModes() не обязана присутствовать в программе, однако это желательно, если вы собираетесь организовать переключение видеорежимов. На рынке существует огромное количество видеоустройств, каждое из которых обладает своими возможностями и ограничениями. Не стоит полагаться на автоматическую поддержку любого конкретного видеорежима, за исключением принятого по умолчанию в Windows режима 640x480x8.

Функция SetDisplayMode() активизирует заданный видеорежим. Версия SetDisplay Mode() из интерфейса DirectDraw2 позволяет дополнительно задать частоту смены кадров. Этим она отличается от функции из интерфейса DirectDraw, в которой можно задать только горизонтальное и вертикальное разрешения и глубину пикселей. Функция SetDisplayMode() присутствует в любой программе, осуществляющей переключение видеорежимов.

Функция RestoreDisplayMode() восстанавливает видеорежим, действовавший до вызова SetDisplayMode(). Перед вызовом функций SetDisplayMode() и RestoreDisplayMode() необходимо предварительно установить монопольный уровень кооперации вызовом функции SetCooperativeLevel().



Функции GetDC() и ReleaseDC()


Прямой доступ к памяти поверхности — замечательная возможность, но иногда бывает удобнее рассматривать поверхность как обычное графическое устройство Windows. Для этой цели в интерфейсе DirectDrawSurface предусмотрены две функции:

GetDC()

ReleaseDC()

Функция GetDC() предоставляет в ваше распоряжение DC (контекст устройства, Device Context), через который можно осуществлять вывод на поверхность стандартными функциями Win32. Например, передавая его функции Win32 TextOut(), можно вывести на поверхность текст. Функция ReleaseDC() должна быть вызвана сразу же после завершения работы с DC.

Как и в случае с Lock() и Unlock(), функцию ReleaseDC() необходимо вызывать после GetDC() как можно быстрее. Это связано с тем, что внутри функции GetDC() вызывается Lock(), а внутри ReleaseDC() - Unlock().



Функции IsLost() и Restore()


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

IsLost()

Restore()

Рассмотрим следующую ситуацию: ваше приложение начинает работу. Вы размещаете как можно больше поверхностей в видеопамяти, а все остальные — в системной памяти. В течение некоторого времени приложение работает, но затем пользователь запускает другое приложение или переключается на него. Другое приложение может быть чем угодно — скажем, стандартной Windows-программой (например, Windows Explorer или Notepad). Оно также может оказаться другим приложением, которое тоже работает с DirectDraw и стремится разместить как можно больше поверхностей в видеопамяти. Если DirectDraw откажется выделить новому приложению видеопамять, оно будет работать плохо (а то и вообще не будет). Возможна и обратная ситуация — видеопамять окажется недоступной для вашего приложения.

По этой причине DirectDraw может забрать у неактивного приложения видеопамять, занятую некоторыми (или всеми) поверхностями. Такие поверхности называются потерянными (lost). Вообще говоря, такие поверхности остаются у вашей программы, но они перестают быть связанными с какой-либо областью памяти. Любая попытка использовать потерянную поверхность приводит к ошибке DDERR_SURFACELOST. Функция IsLost() позволяет узнать, была ли потеряна память данной поверхности.

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

При этом существует одна загвоздка. Функция Restore() восстанавливает лишь память, закрепленную за поверхностью, но не ее содержимое. Следовательно, после восстановления поверхности ваше приложение само должно восстановить ее содержимое.

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



Функции Lock() и Unlock()


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

Unlock()

Lock()

Функция Lock() возвращает указатель на область памяти, занятую поверхностью, независимо от того, где поверхность находится — в системной памяти или в видеопамяти. Память поверхности всегда организована линейно, что позволяет максимально упростить обращение к графическим данным. Функция Unlock() сообщает DirectDraw о том, что прямой доступ к памяти поверхности завершен.

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

Заблокированную поверхность невозможно заблокировать снова. Попытка вызова функции Lock() для уже заблокированной поверхности закончится неудачей.



Функции описания поверхностей


Мы начнем с четырех функций, с помощью которых можно получить информацию о самой поверхности:

GetCaps()

GetPixelFormat()

GetSurfaceDesc()

SetSurfaceDesc()

Функция GetCaps() по аналогии с одноименной функцией интерфейса DirectDraw заполняет структуру информацией о том, какие возможности поддерживаются данной поверхностью. В частности, в нее заносятся сведения о том, является ли данная поверхность первичной или внеэкранной, и где она находится — в системной или видеопамяти.

Функция GetPixelFormat() особенно важна при работе с поверхностями форматов High и True Color, поскольку формат пикселей может зависеть от видеокарты. Функция возвращает маски, которые определяют способ хранения отдельных цветовых составляющих.

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

Функция SetSurfaceDesc() (появилась только в DirectX 5 и поддерживается только интерфейсом DirectDrawSurface3) позволяет задать значения некоторых атрибутов поверхности. Например, с ее помощью можно выбрать тип памяти, в которой должна находиться поверхность. Данная функция помогает реализовать нестандартную схему управления поверхностями.



Функции определения состояния поверхностей


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

GetBltStatus()

GetFlipStatus()

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

Аналогично, функция GetFlipStatus() показывает, происходит ли в данный момент переключение страниц. Для получения информации о переключении страниц, вызванном функцией Flip(), вместо GetBltStatus() следует пользоваться именно этой функцией даже в том случае, если DirectDraw имитирует переключение страниц посредством блиттинга.



Функции PageLock() и PageUnlock()


Перейдем к двум функциям, внешне похожим на Lock() и Unlock():

PageLock()

PageUnlock()

Вероятно, имена этих функций были выбраны неудачно, потому что они предназначены совсем для других целей. С помощью PageLock() и PageUnlock() можно управлять тем, как Windows обходится с поверхностями в системной памяти. Для работы с ними используется интерфейс DirectDrawSurface2, в DirectDrawSurface они отсутствуют.

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

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

Следует помнить, что частое использование PageLock() приведет к замедлению работы Windows из-за сокращения общего объема переносимой памяти. Когда именно это произойдет, зависит от объема памяти, для которой запрещен перенос на диск, и общего объема системной памяти.

Функции PageLock() и PageUnlock() используются в первую очередь самой библиотекой DirectDraw, а не приложениями. Например, DirectDraw автоматически вызывает PageLock() и PageUnlock(), чтобы поверхности, находящиеся в системной памяти, не переносились на диск в ходе блиттинга.

Функцию PageLock() можно вызывать для одной поверхности многократно. DirectDraw следит за количеством вызовов PageLock(), используя механизм подсчета ссылок, поэтому для отмены многократных вызовов PageLock() потребуется несколько вызовов PageUnlock().

Функции PageLock() и PageUnlock() не влияют на поверхности, находящиеся в видеопамяти.



Функции палитры


Палитры, как и объекты отсечения, можно присоединять к поверхностям. Для этой цели в интерфейсе DirectDrawSurface предусмотрены две функции:

GetPalette()

SetPalette()

Функция SetPalette() присоединяет к поверхности экземпляр интерфейса DirectDrawPalette (о нем речь пойдет ниже). Функция GetPalette() применяется для получения указателя на палитру, присоединенную ранее.

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



Функции присоединения поверхностей


Интерфейс DirectDrawSurface содержит четыре функции для управления взаимосвязанными поверхностями:

AddAttachedSurface()

DeleteAttachedSurface()

EnumAttachedSurface()

GetAttachedSurface()

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



Функции проверки столкновений


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

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


for (int i=0;i<nsprites;i++) for (int j=0;j<nsprites;j++)

if (SpritesCollide( sprite[i], sprite[j] )) { sprite[i]->Hit( sprite[j] ); sprite[j]->Hit( sprite[i] ); }


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


for (int i=0;i<nsprites;i++) for (int j=i+1;j<nsprites;j++)

if (SpritesCollide( sprite[i], sprite[j] )) { sprite[i]->Hit( sprite[j] ); sprite[j]->Hit( sprite[i] ); }


Этот фрагмент гарантирует, что каждая пара спрайтов будет передаваться функции SpritesCollide() ровно один раз, и спрайты не будут проверяться на столкновения с собой.



Теперь давайте рассмотрим функцию SpritesCollide(). Как видно из кода, аргументами этой функции являются два спрайта. Функция SpritesCollide() возвращает TRUE, если спрайты сталкиваются, и FALSE в противном случае.

Реализация функции SpritesCollide()

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

BOOL SpritesCollide(Sprite* sprite1, Sprite* sprite2) { ASSERT(sprite1 && sprite2);

if (SpritesCollideRect( sprite1, sprite2 )) if (SpritesCollidePixel( sprite1, sprite2 )) return TRUE;

return FALSE; }


Обратите внимание на то, что функция SpritesCollide() должна получать два аргумента — два указателя на объекты Sprite (класс Sprite рассматривается ниже). Сначала функция проверяет, что оба указателя отличны от нуля, с помощью макроса ASSERT().

СОВЕТ

ASSERT() в DirectDraw

Хотя в библиотеку MFC входит макрос ASSERT(), он плохо подходит для полноэкранных приложений DirectDraw. В приложении А описана нестандартная версия ASSERT(), использованная в программах этой книги.

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

Если ограничивающие прямоугольники пересекаются, необходимо продолжить проверку. Мы вызываем функцию SpritesCollidePixel() и также передаем ей два указателя на объекты Sprite. Если эта проверка окажется неудачной, SpritesCollide() возвращает FALSE; в противном случае она возвращает TRUE, что говорит о столкновении спрайтов.



Перед тем как рассматривать процедуру проверки на уровне пикселей, давайте рассмотрим функцию SpritesCollideRect(), в которой проверяется пересечение ограничивающих прямоугольников:

BOOL SpritesCollideRect(Sprite* sprite1, Sprite* sprite2) { CRect rect1 = sprite1->GetRect(); CRect rect2 = sprite2->GetRect(); CRect r = rect1 & rect2;

// Если все поля равны нулю, прямоугольники не пересекаются return !(r.left==0 && r.top==0 && r.right==0 && r.bottom==0); }


Пересечение ограничивающих прямоугольников проверяется в функции SpritesCollideRect() с помощью класса MFC CRect. Сначала для каждого спрайта вызывается функция Sprite::GetRect(). Она возвращает объект CRect, определяющий текущее положение и размеры каждого спрайта. Затем третий объект CRect инициализируется оператором пересечения класса CRect (& ), который вычисляет область пересечения двух своих операндов. Если пересечения не существует (два прямоугольника не перекрываются), все четыре поля CRect обнуляются. Этот признак используется для возврата TRUE в случае пересечения прямоугольников, и FALSE — в противном случае.

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

Листинг 9.1. Функция SpritesCollidePixel()

BOOL SpritesCollidePixel(Sprite* sprite1, Sprite* sprite2) { CRect rect1=sprite1->GetRect(); CRect rect2=sprite2->GetRect();

CRect irect = rect1 & rect2;

ASSERT(!(irect.left==0 && irect.top==0 && irect.right==0 && irect.bottom==0)); CRect r1target = rect1 & irect; r1target.OffsetRect( -rect1.left, -rect1.top ); r1target.right--; r1target.bottom--;

CRect r2target = rect2 & irect; r2target.OffsetRect( -rect2.left, -rect2.top ); r2target.right--; r2target.bottom--;

int width=irect.Width(); int height=irect.Height();

DDSURFACEDESC desc1, desc2; ZeroMemory( &desc1, sizeof(desc1) ); ZeroMemory( &desc2, sizeof(desc2) ); desc1.dwSize = sizeof(desc1); desc2.dwSize = sizeof(desc2);

BYTE* surfptr1; // Указывает на начало памяти поверхности BYTE* surfptr2; BYTE* pixel1; // Указывает на конкретные пиксели BYTE* pixel2; // в памяти поверхности BOOL ret=FALSE;

LPDIRECTDRAWSURFACE surf1=sprite1->GetSurf(); LPDIRECTDRAWSURFACE surf2=sprite2->GetSurf();

if (surf1==surf2) { surf1->Lock( 0, &desc1, DDLOCK_WAIT, 0 ); surfptr1=(BYTE*)desc1.lpSurface;

for (int yy=0;yy<height;yy++) { for (int xx=0;xx<width;xx++) { pixel1=surfptr1+(yy+r1target.top)*desc1.lPitch +(xx+r1target.left); pixel2=surfptr1+(yy+r2target.top)*desc1.lPitch +(xx+r2target.left); if (*pixel1 && *pixel2) { ret=TRUE; goto done_same_surf; } } }

done_same_surf: surf1->Unlock( surfptr1 ); return ret; }

surf1->Lock( 0, &desc1, DDLOCK_WAIT, 0 ); surfptr1=(BYTE*)desc1.lpSurface;

surf2->Lock( 0, &desc2, DDLOCK_WAIT, 0 ); surfptr2=(BYTE*)desc2.lpSurface;

for (int yy=0;yy<height;yy++) { for (int xx=0;xx<width;xx++) { pixel1=surfptr1+(yy+r1target.top)*desc1.lPitch +(xx+r1target.left); pixel2=surfptr2+(yy+r2target.top)*desc2.lPitch +(xx+r2target.left); if (*pixel1 && *pixel2) { ret=TRUE; goto done; } } }

done: surf2->Unlock( surfptr2 ); surf1->Unlock( surfptr1 );

return ret; }

<


br>Функция SpritesCollidePixel() состоит из четырех этапов. Она делает следующее:

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

Вычисляет области спрайтов, для которых потребуется проверка на уровне пикселей.

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

Снимает блокировку с обеих поверхностей и возвращает TRUE или FALSE.


На этапе 1 мы инициализируем два объекта CRect функцией Sprite::GetRect(). Функция GetRect() возвращает прямоугольник CRect, представляющий положение и размеры спрайта. Затем оператор & (оператор пересечения класса CRect) определяет область пересечения двух прямоугольников. Ниже снова приведен соответствующий фрагмент листинга 9.1:

CRect rect1=sprite1->GetRect(); CRect rect2=sprite2->GetRect();

CRect irect = rect1 & rect2;

ASSERT(!(irect.left==0 && irect.top==0 && irect.right==0 && irect.bottom==0));


Как мы узнали из функции SpritesCollideRect(), оператор пересечения класса CRect обнуляет все четыре поля CRect, если операнды не пересекаются. В этом случае функцию SpritesCollidePixel() вызывать не следует, поэтому о такой ситуации сообщает макрос ASSERT().

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

CRect r1target = rect1 & irect; r1target.OffsetRect( -rect1.left, -rect1.top ); r1target.right--; r1target.bottom--;

CRect r2target = rect2 & irect; r2target.OffsetRect( -rect2.left, -rect2.top ); r2target.right--; r2target.bottom--;


В прямоугольниках r1target и r2target

хранятся области спрайтов, для которых потребуется проверка на уровне пикселей. После того как пересечение будет найдено, оба прямоугольника сдвигаются функцией CRect::OffsetRect() так, чтобы левый верхний угол имел координаты (0, 0).


Это объясняется тем, что поля right и bottom объектов CRect

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

Также обратите внимание на то, что правый и нижний края каждого прямоугольника обрезаются на один пиксель. Это связано с особенностями реализации CRect.

СОВЕТ

Кое-что о классе CRect

Класс MFC CRect реализован так, чтобы при вычитании поля left из поля right получалась ширина прямоугольника. Такой подход удобен, но смысл поля right несколько изменяется. Например, рассмотрим прямоугольник, у которого поле left равно 0, а полю right присвоено значение 4. В соответствии с реализацией класса CRect

такой прямоугольник имеет ширину в 4 пикселя, но если использовать эти же значения для обращений к пикселям, ширина прямоугольника окажется равной 5 пикселям (поскольку в нее будут включены пиксели с номерами от 0 до 4). Такие же расхождения возникают и для полей top и bottom. Следовательно, чтобы использовать поля CRect для работы с пикселями, необходимо уменьшить на 1 значения полей right и bottom.

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

LPDIRECTDRAWSURFACE surf1=sprite1->GetSurf(); LPDIRECTDRAWSURFACE surf2=sprite2->GetSurf();


Если поверхности совпадают, проверка выполняется следующим фрагментом:

if (surf1==surf2) { surf1->Lock( 0, &desc1, DDLOCK_WAIT, 0 ); surfptr1=(BYTE*)desc1.lpSurface;

for (int yy=0;yy<height;yy++) { for (int xx=0;xx<width;xx++) { pixel1=surfptr1+(yy+r1target.top)*desc1.lPitch +(xx+r1target.left); pixel2=surfptr1+(yy+r2target.top)*desc1.lPitch +(xx+r2target.left); if (*pixel1 && *pixel2) { ret=TRUE; goto done_same_surf; } } }

done_same_surf: surf1->Unlock( surfptr1 ); return ret; }


Сначала мы блокируем поверхность, чтобы получить доступ к ее памяти.


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

Если два спрайта находятся на разных поверхностях, проверка столкновений выполняется другим фрагментом функции SpritesCollidePixel(). Ниже снова приведен соответствующий фрагмент листинга 9.1:

surf1->Lock( 0, &desc1, DDLOCK_WAIT, 0 ); surfptr1=(BYTE*)desc1.lpSurface;

surf2->Lock( 0, &desc2, DDLOCK_WAIT, 0 ); surfptr2=(BYTE*)desc2.lpSurface;

for (int yy=0;yy<height;yy++) { for (int xx=0;xx<width;xx++) { pixel1=surfptr1+(yy+r1target.top)*desc1.lPitch +(xx+r1target.left); pixel2=surfptr2+(yy+r2target.top)*desc2.lPitch +(xx+r2target.left); if (*pixel1 && *pixel2) { ret=TRUE; goto done; } } }

done: surf2->Unlock( surfptr2 ); surf1->Unlock( surfptr1 );

return ret;


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


Функции создания интерфейсов


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

CreateClipper()

CreatePalette()

CreateSurface()

Функция CreateClipper() создает экземпляры интерфейса DirectDrawClipper. Объекты отсечения (clipper) используются не всеми приложениями DirectDraw, так что в некоторых программах эта функция может отсутствовать. Вскоре мы рассмотрим интерфейс DirectDrawClipper подробнее.

Функция CreatePalette() создает экземпляры интерфейса DirectDrawPalette. Палитры, как и интерфейс DirectDrawClipper, используются не всеми приложениями DirectDraw. Например, приложению, работающему только с 16-битными видеорежимами, палитра не понадобится. Тем не менее приложение, работающее в 8-битном видеорежиме, должно создать хотя бы один экземпляр DirectDrawPalette.

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

Экземпляры самого интерфейса DirectDraw создаются функцией DirectDraw Create(). DirectDrawCreate() - одна из немногих самостоятельных функций DirectDraw, не принадлежащих никакому COM-интерфейсу.



Функция ActivateDisplayMode()


Как было сказано в начале главы, смена видеорежима не сводится к вызову функции SetDisplayMode(). Функция SetDisplayMode() активизирует нужный режим, но при этом необходимо уничтожить существующие поверхности и создать их заново. Класс DirectDrawWin решает эту задачу за вас. В него входит функция ActivateDisplayMode(), выполняющая все действия, необходимые для активизации видеорежима и восстановления поверхностей приложения. Для удобства давайте снова посмотрим, как выглядит функция ActivateDisplayMode() (см. листинг 4.1).

Листинг 4.1. Функция DirectDrawWin::ActivateDisplayMode()


BOOL DirectDrawWin::ActivateDisplayMode( int mode ) { if ( mode<0 || mode>=totaldisplaymodes ) return FALSE;

DWORD width = displaymode[mode].width; DWORD height = displaymode[mode].height; DWORD depth = displaymode[mode].depth;

displayrect.left=0; displayrect.top=0; displayrect.right=width; displayrect.bottom=height; displaydepth=depth;

ddraw2->SetDisplayMode( width, height, depth, rate, 0 ); curdisplaymode = mode;

TRACE("------------------- %dx%dx%d (%dhz) ---------------\n", width, height, depth, rate);

if (CreateFlippingSurfaces()==FALSE) { FatalError("CreateFlippingSurfaces() failed"); return FALSE; }

StorePixelFormatData();

if (CreateCustomSurfaces()==FALSE) { FatalError("CreateCustomSurfaces() failed"); return FALSE; }

return TRUE; }


Функция ActivateDisplayMode() получает один аргумент — индекс в отсортированном списке обнаруженных видеорежимов. Сначала индекс проверяется на правильность. Если он соответствует допустимому элементу массива displaymode, высота, ширина и глубина заданного режима извлекаются из массива и используются для инициализации переменных displayrect и displaydepth. Затем атрибуты видеорежима используются при вызове функции SetDisplayMode(), активизирующей новый видеорежим.

Далее функция CreateFlipingSurfaces() создает первичную поверхность со вторичным буфером, а функция StorePixelFormatData() проверяет, не устарел ли формат пикселей DirectDrawWin (форматы пикселей подробно рассматриваются в главе 5). Наконец, мы вызываем функцию CreateCustomSurfaces(), отвечающую за создание вспомогательных поверхностей приложения.

Итак, функция ActivateDisplayMode() автоматизирует процесс переключения видеорежимов. Сначала она проверяет, будут ли при вызове функции SetDisplayMode() использоваться правильные аргументы, а затем восстанавливает поверхности приложения. Настало время применить ее на практике.



Функция BltSurface()


Функция BltSurface() класса DirectDrawWin оказывается более гибкой и удобной по сравнению с функциями DirectDrawSurface::Blt() и BltFast(). Мы уже видели, как BltSurface() используется внутри функции BounceWin::DrawScene(), а сейчас рассмотрим саму функцию.

Функция BltSurface() требует передачи четырех аргументов, а пятый аргумент необязателен. Первые два аргумента представляют собой указатели на поверхности — источник и приемник. Следующие два аргумента — координаты x и y, определяющие положение копируемой области на приемнике. По умолчанию блиттинг выполняется без цветовых ключей, однако их можно активизировать с помощью необязательного пятого параметра. Код функции BltSurface() приведен в листинге 3.3.

Листинг 3.3. Функция BltSurface()


BOOL DirectDrawWin::BltSurface( LPDIRECTDRAWSURFACE destsurf, LPDIRECTDRAWSURFACE srcsurf, int x, int y, BOOL srccolorkey ) { if (destsurf==0 || srcsurf==0) return FALSE;

BOOL use_fastblt=TRUE;

DDSURFACEDESC destsurfdesc; ZeroMemory( &destsurfdesc, sizeof(destsurfdesc) ); destsurfdesc.dwSize = sizeof(destsurfdesc); destsurf->GetSurfaceDesc( &destsurfdesc ); CRect destrect; destrect.left=0; destrect.top=0; destrect.right=destsurfdesc.dwWidth; destrect.bottom=destsurfdesc.dwHeight;

DDSURFACEDESC srcsurfdesc; ZeroMemory( &srcsurfdesc, sizeof(srcsurfdesc) ); srcsurfdesc.dwSize = sizeof(srcsurfdesc); srcsurf->GetSurfaceDesc( &srcsurfdesc );

CRect srcrect; srcrect.left=0; srcrect.top=0; srcrect.right=srcsurfdesc.dwWidth; srcrect.bottom=srcsurfdesc.dwHeight;

// Проверить, нужно ли что-нибудь делать... if (x+srcrect.left>=destrect.right) return FALSE; if (y+srcrect.top>=destrect.bottom) return FALSE; if (x+srcrect.right<=destrect.left) return FALSE; if (y+srcrect.bottom<=destrect.top) return FALSE;

// При необходимости выполнить отсечение // для прямоугольной области источника if (x+srcrect.right>destrect.right) srcrect.right-=x+srcrect.right-destrect.right; if (y+srcrect.bottom>destrect.bottom) srcrect.bottom-=y+srcrect.bottom-destrect.bottom;

CRect dr; if (x<0) { srcrect.left=-x; x=0; dr.left=x; dr.top=y; dr.right=x+srcrect.Width(); dr.bottom=y+srcrect.Height(); use_fastblt=FALSE; }

if (y<0) { srcrect.top=-y; y=0; dr.left=x; dr.top=y; dr.right=x+srcrect.Width(); dr.bottom=y+srcrect.Height(); use_fastblt=FALSE; }

DWORD flags; if (use_fastblt) { flags=DDBLTFAST_WAIT; if (srccolorkey) flags |= DDBLTFAST_SRCCOLORKEY; destsurf->BltFast( x, y, srcsurf, &srcrect, flags ); } else { flags=DDBLT_WAIT; if (srccolorkey) flags |= DDBLT_KEYSRC; destsurf->Blt( &dr, srcsurf, &srcrect, flags, 0 ); } return TRUE; }

<
br>

Сначала функция BltSurface() проверяет указатели на поверхности. Если хотя бы один из них равен нулю, функция возвращает FALSE, тем самым сообщая о неудаче. Если проверка прошла успешно, два объекта CRect инициализируются в соответствии с размерами поверхностей, полученными с помощью функции DirectDrawSurface::GetSurfaceDesc().

Затем BltSurface() проверяет, что попадает ли точка назначения в границы приемника. Если координаты x и y таковы, что копия не пересекается с поверхностью приемника, блиттинг не нужен, поэтому мы просто выходим из функции.

Если же с точкой назначения все в порядке, функция проверяет, нужно ли выполнять отсечение. Если отсечение не требуется, блит-операция для достижения максимального быстродействия выполняется функцией BltFast(). Если отсечение все же необходимо, возможно, придется пользоваться функцией Blt().

Если отсечение выполняется по правому или нижнему краю источника, функция BltFast() справится с задачей и обрежет выступающую часть копируемой области. Если же отсечение происходит по верхнему или левому краю, приходится работать с функцией Blt(), потому что BltFast() не позволяет задать прямоугольную область приемника. После выполнения блиттинга BltSurface() возвращает TRUE как признак успешного завершения.