Добро
пожаловать в «машинное отделение»!
В этой главе мы рассмотрим
интерфейс DirectDraw, который открывает
перед нами низкоуровневый доступ к
видеоадаптеру. Мы узнаем, для чего
нужен данный интерфейс, как он
устроен и что с ним делать.
Приложение для этой главы
находится в каталоге DDEval.
Что такое DirectDraw?
Библиотека
DirectDraw предоставляет единый
программный интерфейс для работы с
различными видеоадаптерами. Но
ведь подобный интерфейс, Microsoft Windows
Graphics Device Interface (GDI), существовал и
ранее? Вы совершенно правы. Главное
отличие между DirectDraw и GDI
заключается в том, что DirectDraw
позволяет работать
непосредственно с видеоадаптером, a
GDI — наоборот, ограждает вас от
этого! Возможно, сказанное не
совсем справедливо по отношению к
GDI — интерфейс проектировался для
создания переносимых приложений, а
о какой переносимости можно
говорить, если кто угодно как
угодно развлекается с
видеоадаптером?
Конечно,
переносимые приложения — вещь
хорошая, но если программные
прослойки, обеспечивающие
переносимость, оказываются слишком
«толстыми» или неэффективными, это
снижает производительность
приложения и делает его
недопустимо медленным. Игры как раз
и составляют такой класс
приложений, для которых
незначительная разница в
производительности может
обернуться разницей между доходами
и потерями для фирмы-разработчика.
Даже
несмотря на то, что Windows GDI
совершенствовался с годами, всегда
хотелось обойти его и напрямую
обращаться к видеоадаптеру —
например, когда приложение
работает в полноэкранном режиме.
Если приложение занимает весь
экран, почему бы не дать ему полную
свободу в работе с видеоадаптером?
Интерфейс DirectDraw позволяет нам
почти напрямую обращаться к
видеоадаптеру в любом
Windows-приложении. Остается лишь
решить, стоит этим пользоваться или
нет?
Чтобы вам
было проще принять решение, мы
кратко рассмотрим наиболее
интересные возможности DirectDraw на
примере несложного приложения. Это
поможет вам в дальнейшем
самостоятельно экспериментировать
с DirectDraw.
Архитектура
DirectDraw
На рис. 12-1
изображена слегка упрощенная
архитектура GDI и DirectDraw с точки
зрения приложения, работающего с
трехмерными объектами.
Как видите,
для рисования трехмерного объекта
у приложения есть четыре
возможности:
• GDI.
• OpenGL.
•
Абстрактный режим Direct3D.
• DirectDraw.
Рис. 12-1.
Архитектуры GDI и DirectDraw
Рисование в GDI
Windows-программисты
хорошо знают путь, изображенный
слева на рис. 12-1. Приложение
вызывает функции GDI, преобразуемые
в вызовы DIB-механизма. DIB-механизм
обращается к драйверу
видеоустройства, который работает
с видеоадаптером. Чтобы создать
трехмерное приложение,
пользующееся услугами GDI, вам
придется самостоятельно выполнять
все преобразования координат,
вычисления глубины, отсечение
невидимых линий и т. д. В результате
всей этой работы должно появиться
приложение, которое компилируется
и работает на разнообразных
платформах.
Рисование в OpenGL
Для полноты
картины я включил в наше
рассмотрение механизм
визуализации OpenGL. OpenGL входит в Win32 и
позволяет создавать переносимые
трехмерные приложения, обходясь
без самостоятельных вычислений
глубины и т. д. Язык OpenGL
поддерживается на большом
количестве платформ, что повышает
переносимость вашего приложения.
Качество реализации OpenGL отличается
на разных платформах, так что
производительность приложения
также может изменяться. Поскольку
реализация OpenGL в Microsoft Windows 95
использует прослойку Direct3D, ее
производительность оказывается
лучше, чем в ряде других систем.
Рисование в
абстрактном режиме Direct3D
До
настоящего времени мы использовали
только абстрактный режим Direct3D.
Механизм абстрактного режима во
многих отношениях напоминает OpenGL.
Главное отличие заключается в том,
что абстрактный режим
оптимизирован для работы с
прослойкой Direct3D и обеспечивает
бесспорно лучшую
производительность из всех
возможных.
Рисование в
DirectDraw
Путь, который
мы рассмотрим сейчас, идет от
приложения прямо к прослойке
DirectDraw/Direct3D. Особый интерес
представляет ветка DirectDraw, которая
идет вниз, к видеоадаптеру, через HAL
(прослойка абстрактной аппаратуры,
Hardware Abstraction Layer) и HEL (прослойка
эмуляции аппаратуры, Hardware Emulation Layer).
Работа только с сервисом DirectDraw в
определенном отношении напоминает
обращение с GDI, поскольку немалую
часть вычислении вам придется
выполнять самостоятельно. Тем не
менее если вы уверены в своих силах,
то этот путь теоретически позволит
добиться наилучшей
производительности, поскольку
перед вами открывается почти
прямой доступ к видеоадаптеру. Если
вы собираетесь перенести в Windows уже
существующий механизм
визуализации, то вам следует идти
именно этим путем.
На рис. 12-1
компоненты DirectDraw и Direct3D изображены
в одной общей рамке. Между ними
существует немало сходства, так что
в условном изображении архитектуры
их можно разместить вместе. Тем не
менее они все же отличаются между
собой, и в этой главе мы будем
рассматривать только DirectDraw. О Direct3D
рассказывается в главе 13.
Прослойка
абстрактной аппаратуры (HAL)
Мне не
нравятся термины «прослойка» или
«абстрактный», поскольку они
наводят на мысли о какой-то жирной и
неповоротливой программе, хотя HAL с
подобным представлением не имеет
ничего общего. HAL существует для
того, чтобы обеспечить возможность
унифицированного взаимодействия с
видеоадаптером. Во многих случаях
ее можно просто считать таблицей,
которая показывает, где хранятся те
или иные данные. Например, при
отображении видеопамяти на
основное адресное пространство HAL
содержит информацию о том, с какого
адреса начинается блок
видеопамяти. Кроме того, HAL знает о
том, через какой порт можно
работать с аппаратной палитрой и т.
д. На самом деле прослойка HAL
оказывается очень тонкой.
Память
большинства видеокарт
организована в виде одного
большого непрерывного блока,
поэтому вы можете получить
указатель на видеопамять и
рисовать непосредственно в ней.
Однако в некоторых старых картах
эпохи VGA используется технология
переключения банков памяти,
которая отображает фрагменты
видеопамяти в небольшое окно,
расположенное в адресном
пространстве процессора. Это
означает, что вы можете получить
указатель лишь на часть
видеопамяти. На выручку приходит HAL.
HAL имитирует большой, непрерывный
буфер видеопамяти и позволяет
осуществлять чтение/запись по
несуществующим адресам. При
попытке чтения/записи по такому
адресу процессор генерирует
исключение, которое HAL
перехватывает и использует для
правильного отображения
адреса на нужный фрагмент
видеопамяти. Благодаря виртуальной
адресации практически все
видеокарты для программы выглядят
так, словно они обладают единым,
непрерывным адресным
пространством.
Прослойка
эмуляции аппаратуры (HEL)
HEL существует
для того, чтобы предоставлять
услуги, не поддерживаемые
конкретной моделью видеоадаптера.
Многие видеокарты содержат
блиттеры — аппаратные средства для
пересылки битовых блоков (bitbit) из
одного участка видеопамяти в
другой. На некоторых картах имеются
блиттеры для пересылки блоков в
основной памяти, а также в
видеопамяти. Пересылая спрайт в
своей программе, вы не
задумываетесь о том, где он
хранится в данный момент. Это
означает, что некоторая внешняя
программа должна следить за
аппаратными и программными
средствами. Такая задача возложена
на HEL, которая осуществляет
программную эмуляцию той или иной
операции в случае, если ваши
аппаратные средства не могут ее
выполнить.
Компоненты DirectDraw
Архитектуру
DirectDraw можно рассмотреть и с
несколько иной точки зрения, как
показано на рис. 12-2.
В этом случае
DirectDraw предоставляет интерфейс для
работы с видеопамятью и палитрой. Я
изобразил на рисунке палитру,
поскольку 256-цветные дисплеи с
палитрой продолжают оставаться
самыми распространенными. Если
ваша видеосистема работает с 16-, 24-
или 32-битной кодировкой пикселей,
то палитра не понадобится, и схема
несколько упрощается. Если
временно не обращать внимания на
палитру, мы увидим, что приложение
должно располагать средствами для
доступа к видеопамяти и
перемещения данных в ней.
В типичном
приложении с анимацией
используется несколько буферов
видеопамяти. Первичный буфер
содержит изображение, которое в
настоящий момент выводится на
экране видеоадаптером. Вторичный
буфер предназначен для построения
следующего кадра. В типичной
плоской анимации для каждого
персонажа имеется несколько
спрайтов. Желательно кэшировать их
в буфере, из которого блиттер мог бы
извлекать их с минимальными
усилиями, — обычно такой буфер
находится в видеопамяти.
Если учесть,
что HEL обеспечивает выполнение
блитовых операций даже в том
случае, если они не поддерживаются
на аппаратном уровне, то становится
понятно, почему на рис. 12-2 нет
аппаратного блиттера. Приложение
должно работать лишь с различными
областями видеопамяти и, возможно,
с палитрой.
Для
приложения с трехмерными объектами
схема выглядит более сложно, однако
мы рассмотрим соответствующие
отличия в следующей главе. А сейчас
давайте обсудим различные
компоненты DirectDraw и способы
управления ими.
Поверхности в
видеопамяти
DirectDraw делит
видеопамять на поверхности.
Поскольку объем памяти каждой
поверхности может достигать
размера наибольшего свободного
блока памяти, вы можете завести
одну поверхность, которая
использует всю память, или
несколько меньших поверхностей.
Поверхности разумно создавать в
основной памяти, что-
Рис. 12-2.
Компоненты DirectDrow
бы при
исчерпании всей свободной
видеопамяти вам не пришлось
вносить исправления в программу.
Разумеется, рисование на
поверхностях в основной памяти
происходит медленнее, чем
обращение к поверхности в
видеопамяти, поэтому за
расположением поверхностей
необходимо следить.
К одним
поверхностям могут присоединяться
другие. Взгляните на рис. 12-2.
Вторичный буфер присоединен к
первичному, в этом случае ими легче
управлять, поскольку вам нужно
хранить указатель лишь на
первичный буфер. Указатель на
вторичный буфер всегда можно
получить из списка присоединенных
поверхностей первичного буфера.
При работе с трехмерными объектами
иногда бывает необходимо отводить
поверхность под Z-буфер, ее тоже
удобно присоединить к первичному
буферу. Кроме того, поверхность
можно выделить под альфа-буфер и
также присоединить ее к другой
поверхности.
Поверхность
описывается структурой DDSURFACEDESC, в
которой содержатся поля для высоты
поверхности, ее ширины и т. д. Кроме
того, в нее входит структура
DDPIXELFORMAT, описывающая формат
отдельных пикселей. Пиксели могут
быть заданы в виде RGB-тройки,
индекса палитры, YUV-значения или в
каком-нибудь другом формате,
который поддерживается вашим
видеоадаптером. Количество бит на
пиксель изменяется от 1 до 32. Для
поверхностей, где число бит на
пиксель составляет 16 и менее,
цветовые составляющие (например,
красная, зеленая и синяя) задаются в
виде масок, для которых следует
выполнить операцию поразрядного AND
со значением конкретного пикселя.
Чтобы
получить доступ к поверхности,
необходимо заблокировать ее и
получить указатель. Завершив
работу с поверхностью, вы
разблокируете ее. Блокировка нужна
для доступа к поверхности в режиме
чтение/запись; она также помогает
организовать взаимодействие с
аппаратным блиттером и т. д.
Следовательно, перед тем как
пытаться получить доступ к
поверхности, необходимо знать о
том, кто ее использует. Пример будет
приведен немного ниже, в разделе
«Тестирование прямого доступа к
пикселям» на стр. 296.
Рабочая
поверхность может быть потеряна,
если в системе присутствует другое
приложение, также использующее
DirectDraw. При попытке выполнить
какую-нибудь операцию с потерянной
поверхностью обычно возвращается
код ошибки DDERR_SURFACELOST. Потерянная
поверхность восстанавливается
функцией IDirectDrawSurface::Restore (ресурсы
Windows используются совместно — со
временем к этому привыкаешь).
Поверхности
также могут содержать отдельный
цвет или диапазон цветов,
используемых в качестве цветового
ключа. Цветовые ключи находят
достаточно разнообразное
применение, в частности с их
помощью задаются области
поверхности-источника или
приемника, не подлежащие
копированию. Функция IDirectDrawSurface::Blt
содержит специальный флаг, который
управляет использованием цветовых
ключей при копировании.
Палитры
Для описания
цветов в поверхностях с 1-, 4- и
8-битной кодировкой пикселей
применяются палитры. В DirectDraw
поддерживаются 4- и 8-битные палитры,
которые содержат 16 и 256 цветов
соответственно. Палитра также
может представлять собой набор
индексов в другой палитре. Если вам
приходилось работать с палитрами в
Windows, то и палитры DirectDraw покажутся
хорошо знакомыми. Если же вы
привыкли «играть» с цветовыми
таблицами видеоадаптера, то вам,
вероятно, понравится гибкость
работы с палитрами DirectDraw. Создание
палитр рассматривается ниже, на
стр. 298.
Большая
часть видеоадаптеров способна
работать всего с одной палитрой, но
DirectDraw позволяет связать с каждой
поверхностью произвольную палитру.
Тем не менее следует учесть, что при
блитовых операциях цвета не
преобразуются; при выполнении
блиттинга между поверхностями,
палитры которых отличаются,
результат окажется довольно
жутким.
При создании
палитры вы указываете, какую ее
часть можно отдать в распоряжение
DirectDraw. При работе в оконном режиме
обычно приходится резервировать 20
системных цветов Windows (по 10 с
каждого конца палитры) и разрешать
DirectDraw пользоваться оставшимися 236
элементами. В полноэкранном режиме
другие приложения все равно не
видны, поэтому резервировать для
них системные цвета незачем, и вы
можете предоставить в распоряжение
DirectDraw всю палитру. DirectDraw определяет
несколько новых флагов,
показывающих, как используется тот
или иной элемент палитры (в
дополнение к существующим флагам
Windows). Флаг D3DRMPALETTE_FREE позволяет
DirectDraw использовать данный элемент
палитры. Флаг D3DRMPALETTE_READONLY позволяет
DirectDraw читать элемент палитры и
работать с ним, не изменяя его
значение (используется для
системных цветов в оконном режиме),
а флаг D3DRMPALETTE_RESERVED резервирует
элемент палитры для ваших
собственных целей. DirectDraw не
изменяет и не использует элементы
палитры, помеченные флагом
D3DRMPALETTE_RESERVED.
Ограничители
DirectDraw
работает в одном из двух режимов.
Первый режим, который больше всего
привлекает бывших DOS-программистов,
— полноэкранный. В этом случае вы
можете полностью распоряжаться
изображением на экране. Обычно в
полноэкранном режиме создаются два
буфера, как показано на рис. 12-3.
Рис. 12-3. Работа
с буферами в полноэкранном режиме
Первичный и
вторичный буфера имеют одинаковый
размер. В полноэкранном режиме
можно переключать буфера, то есть
выводить на экран содержимое
вторичного буфера вместо
первичного. Таким образом
реализуется переключение страниц
видеопамяти с минимальными
накладными расходами. Разумеется,
при желании можно воспользоваться
блит-функциями для переноса
содержимого вторичного буфера в
первичный.
Второй режим
больше соответствует облику
стандартных приложений Windows. В
оконном режиме приходится работать
с привычным окном приложения,
расположенным на рабочем столе, как
показано на рис. 12-4.
Оконный
режим связан с определенными
сложностями, поскольку поверхность
вторичного буфера используется
совместно с GDI. При
непосредственной записи в
видеопамять необходимо соблюдать
осторожность и не выйти за пределы
области вашего окна. Чтобы помочь
вам в этом, DirectDraw предоставляет
объект-ограничитель DirectDrawClipper,
который присоединяется к
первичному буферу. Ограничитель
следит за окном, созданным на
рабочем столе, и определяет границы
памяти первичного буфера. В оконном
режиме вы уже не можете переключать
страницы, как это делается в
полноэкранном режиме, и размер
вторичного буфера обычно равен
лишь размеру окна. Некоторые
видеоадаптеры выполняют сложные
перекрытия, которые позволяют в
оконном режиме переключать
страницы так, как это делается в
полноэкранном; при соответствующей
аппаратной поддержке DirectDraw
позволяет вызвать функцию
переключения Flip для поверхности в
оконном режиме.
Рис. 12-4.
Оконный режим
Ограничитель
не только следит за тем, чтобы вы не
пытались что-нибудь записать за
пределы прямоугольной области
окна. Если другое окно частично
перекроет ваше, то вы также не
сможете рисовать поверх
перекрывающего окна.
Поверхности и GDI
Возможно, вы
уже решили, что отныне пользуетесь
только DirectDraw и можете забыть про
существование GDI. Это неверно сразу
по двум причинам. Во-первых, GDI
никуда не пропадает и пользуется
видеопамятью совместно с вашими
программами. Во-вторых, функциями GDI
можно пользоваться для рисования
на поверхностях DirectDraw. На первый
взгляд это кажется излишним, но что
вы будете делать, если, например,
необходимо вывести текст?
Важно
понимать, что GDI не исчезает, даже
если вы получили прямой доступ к
видеопамяти. GDI продолжает работать
с поверхностью, которая
использовалась перед запуском
вашего приложения. Если приложение
требует монопольного управления и
переключается в полноэкранный
режим, GDI все равно осуществляет
вывод в используемый ранее буфер и
теоретически способен испортить
содержимое ваших буферов.
Вмешательство GDI можно
предотвратить — для этого перед
переходом в полноэкранный режим
следует создать окно, занимающее
всю площадь рабочего стола. GDI
поймет, что ваше окно должно
находиться наверху, и не будет
пытаться рисовать поверх того, что
он принимает за экранную область.
Интерфейс GDI
используется и для работы с
поверхностями. С помощью DirectDraw
получают контекст устройства (DC)
для любой созданной поверхности,
после чего по DC вызываются функции
GDI для рисования на поверхности.
Пример будет
рассмотрен ниже, на стр. 291. С
некоторыми операциями GDI
справляется очень хорошо (например,
вывод текста и цветовая заливка
областей). Не стоит полностью
отвергать этот интерфейс лишь
потому, что основная часть вашего
приложения напрямую работает с
видеопамятью. Если сомневаетесь —
попробуйте сами, измерьте скорость
и оцените результаты.
Интересный
побочный эффект от использования
видеопамяти в GDI прослеживается при
попытке отладить программу,
непосредственно работающую с
пикселями, в графическом отладчике
(например, отладчике Microsoft Visual C++).
Перед тем, как работать с
поверхностью, ее необходимо
заблокировать. При этом DirectDraw
блокирует подсистему Win 16, через
которую осуществляется доступ к
таким компонентам Windows, как USER и GDI.
Когда DirectDraw заблокирует Winl6, GDI
перестанет работать, поэтому ваш
отладчик не сможет ничего вывести
на экран и «повиснет».
Выход
заключается в том, чтобы работать с
отладчиком, не использующим GDI
(например, WDEB386), или запустить Visual C++
в сеансе удаленной отладки,
подключаясь к целевому компьютеру
через кабель или сетевое
соединение TCP/IP. Инструкции по
удаленной отладке приведены в
справочном разделе Books Online среды
Visual C++.
Работа с DirectDraw
После
краткого знакомства с «машинным
отделением» DirectDraw, давайте
посмотрим, как же пользоваться
интерфейсом DirectDraw в наших
приложениях. Код, которым мы будем
заниматься, взят из примера,
находящегося в каталоге DDeval. Это
приложение написано мной
специально для экспериментов с
DirectDraw. Его даже трудно назвать
приложением — скорее, это
инструмент для тестирования и
оценки, поэтому средства его
пользовательского интерфейса
ограничены. DDEval не делает ничего
сверхъестественного, но показывает
вам, как использовать GDI для работы
с поверхностью, как запустить
программу в полноэкранном или
оконном режиме, как напрямую
работать с пикселями поверхности.
Тесты позволяют измерить скорость
работы приложения, выраженную в
количестве кадров в секунду (fps),
чтобы вы могли наглядно
представить себе
производительность.
ПРИМЕЧАНИЕ
Помните, что
скорость работы приложения может
заметно отличаться для различных
моделей видеоадаптеров. Если вы
захотите измерить
производительность своего кода,
обязательно протестируйте его на
различных видеоадаптерах.
Структура
программы DDEval
Я создал
программу DDEval с помощью Visual C++ AppWizard.
Я решил оформить ее в виде окна
диалога, а не в виде приложения с
интерфейсом SDI или MDI. Окно диалога
изображено на рис. 12-5.
DDEval проводит
четыре теста, каждый из которых
может выполняться либо в оконном,
либо в полноэкранном режиме. В
оконном режиме размер окна
выбирается из списка (от 320х200 до
1024х768). Количество цветов всегда
совпадает с
Рис. 12-5.
Приложение DDEval
числом
цветов в системе, на которой
выполняется программа. Чтобы
изменить цветовой режим,
необходимо обратиться к свойствам
экрана в Панели управления'Windows.
При выборе
полноэкранного режима программа
составляет перечень всех возможных
режимов. На моем компьютере Dell он
состоит из 17 режимов, так что на
однообразие жаловаться не
приходится. Полноэкранные режимы
не ограничиваются текущим
количеством цветов. Даже если вы
работаете с 8-битными цветами, то
при желании можете провести
тестирование в полноэкранном
режиме с 16--битными цветами.
Рассматриваемая
нами программа пользуется классами
библиотеки SdPlus, которые
представляют собой очень тонкие
оболочки классов C++ для интерфейсов
DirectDraw. По возможности я буду
приводить в функциях обращения как
к классам C++, так и к базовым
интерфейсам.
Программа DDEval
Начнем с
простейшего: подготовки объекта
CDirectDraw к работе и составления
перечня доступных полноэкранных
режимов. Управляющий объект CDirectDraw
создается следующим образом:
void CDDEvalDIg::SetInitialState
() (
// Создать
объект DirectDraw m_pDD = new CDirectDraw;
BOOL b = m_pDD-»Create
() ;
}
В этом
простейшем фрагменте спрятан
довольно большой объем кода,
создающего первичный и вторичный
буфера, а также связанную с ними
палитру. Мы рассмотрим его чуть
позже.
После того
как создан объект CDirectDraw, можно
составлять перечень доступных
полноэкранных режимов. Приведенная
ниже функция заполняет список в
окне диалога DirectDraw Evaluation:
void CDDEvalDIg::ShowModes(
int iModes =
m_pDD-»GetNumModes () ;
DDSORFACEDESC dds;
for (int i = 0; i « iModes;
i++) ( m_pDD-»GetMode!nfo (i, &dds);
Как видно из
листинга, мы определяем режимы и
получаем описание поверхности для
каждого из них. Затем для каждого
режима конструируется строка,
которая заносится в список. Код
объекта CDirectDraw выглядит несколько
сложнее, поскольку для нумерации
режимов в DirectDraw используются
функции косвенного вызова. Я
реализовал функции CDirectDraw::GetNumModes и
CDirectDraw::GetModelnfo с помощью одной
функции косвенного вызова,
расположенной в файле 3dDirDrw.cpp:
//
Информационная структура для
нумерации режимов typedef struct _EnumModeInfo {
int iModeCount;
int iMode;
DDSURFACEDESC ddSurf;
} EnumModeInfo;
// Функция
косвенного вызова для нумерации //
режимов устройства
static
HRESULT FAR PASCAL EnumModesFn(LPDDSURFACEDESC psd, LPVOID pArg)
1
EnumModeInfo* pinfo =
(EnumModeInfo*) pArg;
ASSERT(pinfo) ;
// Проверить
правильность режима if (p!nfo-»iMode ==
p!nfo-»iModeCount) {
p!nfo-»ddSurf = *psd;
return DDENuMRET_CANCEL;
// Прекратить нумерацию
}
p!nfo-»iModeCount++;
return DDENUMRET_OK;
}
// Определить
количество поддерживаемых
экранных режимов int CDirectDraw::GetNumModes()
// Получить
данные для заданного режима BOOL
CDirectDraw::GetModeInfo(int iMode,
DDSURFACEDESC* pDesc)
{
int iModes =
GetNumModes();
if (iMode »= iModes) return
FALSE;
ASSERT(m_pIDD) ;
EnumModeInfo mi;
mi. iModeCount =0;
mi.iMode = iMode;
m hr = m
pIDD-»EnumDisplayModes (0, NULL, &mi, EnumModesFn);
ASSERT(SUCCEEDED(m_hr)) ;
*pDesc = mi.ddSurf;
return TRUE;
}
Структура
EnumModeInfo управляет функцией
нумерации и служит для возвращения
результата. При подсчете
количества режимов возвращаемые
данные не используются. При
получении информации по
конкретному режиму функция
нумерации вызывается до тех пор,
пока номер режима, возвращаемый
интерфейсом DirectDraw, не совпадет с
заданным.
После выбора
теста пользователем, функция
тестирования заполняет структуру
сведениями о размере экрана и
количестве цветов, а затем создает
окно для выполнения теста. Давайте
поочередно рассмотрим все четыре
теста, их назначение и принципы
работы.
Подготовка
тестового окна
Для каждого
теста мы создаем окно,
устанавливаем соответствующий
режим DirectDraw и производим
тестирование. Каждый тест
выполняется примерно для 100 кадров,
сообщает о результатах и проверяет,
не была ли нажата клавиша,
прекращающая процесс тестирования.
Ниже приведена функция, которая
создает окно, устанавливает режим и
запускает тест:
BOOL
CTestWnd::Create(TESTINFO* pti)
{
// Сохранить
информацию о тесте m_pTI = pti;
ASSERT(m_pTI) ;
// Создать
объект DirectDraw m_pDD = new CDirectDraw;
BOOL b = m_pDD-»Create
() ;
ASSERT(b);~
II
Зарегистрировать класс окна
CString strClass =
AfxRegisterWndClass(CS_HREDRAW
I CS_VREDRAW, ::LoadCursor(NULL, IDC_ARROW),
(HBRUSH)::GetStockObject(GRAY_BRUSH)
// Задать
стиль и размеры окна DWORD dwStyle = WS_VISIBLE
¦ WS_POPUP;
RECT re;
if (m_pTI-»bFullScreen)
{
re.top = 0;
re.left = 0;
re.right =
::GetSystemMetrics(SM_CXSCREEN);
re.bottom =
::GetSystemMetrics(SM_CYSCREEN);
} else { //
Оконный режим
dwStyle ¦= WS_CAPTION
¦ WS_SYSMENU;
re.top = 50;
re.left = 50;
re.bottom = re.top +
m_pT I-»i Height;
re. right = re. left +
m_pTI-»iWidth;
::AdjustWindowRect(&rc,
dwStyle, FALSE);
if (!CreateEx(0,
strClass,
"DirectDraw
Window",
dwStyle,
re.left, re.top,
re.right — re.left,
re.bottom — re.top,
m
pTI-»pParent-»GetSafeHwnd() ,
NULL)) {
return FALSE;
i
// Обеспечить
отображение окна на экране UpdateWindow()
;
// Установить
режим для окна
ASSERT(m_pTI) ;
if (m_pTI-»bFullScreen)
f
b =
m_pDD-»SetFull3creenMode (GetSafeHwndO ,
m_pTI-»iWidth,
m_pTI-»iHeight,
m_pTI-»iBpp)
;
} else (
b =
m_pDD-»SetWindowedMode (GetSafeHwnd () ,
m_pTI-»iWidth,
m_pTI-»iHeight)
;
} ASSERT(b) ;
// Выполнить
тестирование SetTimerfl, 100, NULL);
return TRUE;
»
В первой
половине функции мы создаем объект
CDirectDraw и тестовое окно, которое
появляется на экране. Большей
частью она состоит из стандартного
кода Windows. После того как окно
создано, объект CDirectDraw переводится
в оконный или полноэкранный режим.
Наиболее сложной оказывается
завершающая часть. Функции
SetFullScreenMode и SetWindowedMode равносильны
вызову функции CDirectDraw::_SetMode,
выполняющей всю работу по созданию
первичного и вторичного буферов, а
также связанной с ними палитры.
Установка режима состоит из трех
этапов:
1. Задание
уровня кооперации (cooperative level),
который определяет, какие действия
разрешается выполнять с DirectDraw. Если
приложение должно работать в окне,
выбирается нормальный режим. Для
полного экрана следует затребовать
монопольный (exclusive) режим. Уровень
кооперации помогает распределять
ресурсы между системой и
приложениями DirectDraw.
2. Создание
первичного и вторичного буферов.
Буфера являются поверхностями
DirectDraw. Если мы собираемся работать
в полноэкранном режиме, то создаем
так называемую сложную
переключаемую поверхность,
состоящую из двух одинаковых
буферов. Для работы в окне
создается два отдельных буфера:
первичный, используемый совместно
с GDI, и вторичный, принадлежащий
только приложению.
3. Определить,
нужен ли ограничитель, и если да, то
создать его.
Ниже
приводится функция для установки
режима:
BOOL
CDirectDraw::_SetMode(HWND hWnd, int ex, int cy,
int bpp, BOOL
bFullScreen) t
ASSERT(m_pIDD) ;
// Освободить
все существующие буфера ReleaseAllO ;
// Задать
уровень кооперации if (bFullScreen) {
if
(!SetCooperativeLevel(hWnd, DDSCL_EXCLOSIVE ¦
DDSCL_FULLSCREEN)) (
return FALSE;
}
m_hr =
m_pIDD-»SetDisplayMode (ex, cy, bpp) ;
if (FAILED(m_hr)) {
return FALSE;
} m_bRestore = TRUE;
} else {
if
(!SetCooperativeLevel(hWnd, DDSCL_NORMAL)) ( return FALSE;
} )
// Создать
первичную и вторичную поверхности
m_iWidth = ex;
m_iHeight = су;
DDSURFACEDESC sd;
inemset(&sd, 0,
sizeof(sd));
sd.dwSize == sizeof(sd);
if (bFullScreen) {
// Создать
сложную переключаемую поверхность
// с первичным и вторичным буферами
sd.dwFlags = DDSD_CAPS
I DDSD_BACKBUFFERCOUNT;
sd.ddsCaps.dwCaps =
DDSCAPS_PRIMARYSURFACE
I DDSCAPS_FLIP
I DDSCAPS_COMPLEX
I DDSCAPS_3DDEVICE;
sd.dwBackBufferCount =
1;
// Создать
поверхности для первичного и
вторичного буфера
// Создать две
поверхности для оконного режима —
// первичную, используемую
совместно с GDI, //и вторичный буфер
для вывода изображения.
// Создать
поверхность первичного буфера. //
Примечание: поскольку первичный
буфер является // основной
(существующей) поверхностью, // мы не
указываем его ширину и высоту.
sd.dwFlags = DDSD_CAPS;
sd.ddsCaps.dwCaps
= DDSCAPS_PRIMARYSURFACE;
m_pFrontBuffer = new
CDDSurface;
if (
!m_pFrontBuffer-»Create (this, &sd) ) { return FALSE;
}
// Создать
поверхность вторичного буфера
sd.dwFlags = DDSD_WIDTH
I DDSD_HEIGHT
I DDSD_CAPS;
sd.dwWidth = ex;
sd.dwHeight = cy;
sd.ddsCaps.dwCaps =
DDSCAPS_OFFSCREENPLAIN
I DDSCAPS_3DDEVICE;
m_pBackBuffer = new
CDDSurface;
if (
!m_pBackBuffer-»Create(this, &sd)) (
delete m_pFrontBuffer;
m_pFrontBuffer = NULL;
return FALSE;
}
// Создать
объект-ограничитель для первичного
буфера, // чтобы вывод ограничивался
пределами окна m_pClipper = new CDDClipper;
if (
!m_pClipper-»Create(this, hWnd) ) { return FALSE;
} }
Хотя многие
функции в данном фрагменте
относятся к классам SdPlus, я не стану
приводить обращения к
соответствующим интерфейсам DirectDraw,
поскольку в основном роль функции
сводится к простой передаче
полученных параметров
инкапсулированному интерфейсу.
Приведенный
фрагмент содержит большое
количество аспектов, которые я не
стану подробно объяснять.
Интерфейсы DirectDraw описаны в
документации по DirectX SDK. Вы можете
самостоятельно провести ряд
экспериментов, взяв за основу
данный код.
Тестирование
Для
организации тестовых циклов
используется таймер. Тестирующая
функция вызывается из обработчика
сообщений таймера:
void
CTestWnd::OnTimer(UINT nIDEvent) {
// Выполнить
следующий тест switch (m_pTI-»iTest) { case 1:
TestGDIText() ;
breaks-case 2:
TestGDIGfxf) ;
break;
case 3:
TestDDSprite() ;
break;
case 4:
TestDirectPixels() ;
break;
default:
ASSERT(0) ;
break;
}
Тестирование GDI
при работе с текстом
В этом тесте
я решил прибегнуть к услугам GDI для
того, чтобы вывести в окне
небольшой текст. При этом мне
хотелось оценить, насколько
медленно GDI будет справляться с
данной задачей и будет ли вывод
текста влиять на другие тесты.
Результаты, полученные на моем
компьютере Dell, меня вполне устроили
— при выполнении теста в окне 320х200
скорость превышала 200 кадров в
секунду. Все остальные тесты
построены на основе этого кода,
поэтому мы подробно рассмотрим его,
шаг за шагом:
void
CTestWnd::TestGDIText() (
ASSERT(m_pTI) ;
// Получить
указатели на первичный и вторичный
буфера
CDDSurface* pBB =
m_pDD-»GetBackBuffer () ;
ASSERT(pBB) ;
CDDSurface* pFB =
m_pDD-»GetFrontBuffer();
ASSERT(pFB) ;
// Получить
прямоугольник, описывающий
первичный буфер RECT rcFront;
if (m_pTI-»bFullScreen)
{
pFB-»GetRect (rcFront)
;
} else (
GetClientRect(SrcFront)
;
ClientToScreen(&rcFront)
;
}
RECT rcBack;
pBB-»GetRect (rcBack) ;
DWORD dwStart =
timeGetTime() ;
int nFrames = 100;
for (int
iFrame = 0; iFrame « nFrames; iFrame++) (
// Получить DC
для вторичного буфера CDC* pdc = pBB-»GetDC()
;
ASSERT(pdc) ;
// Заполнить
буфер белым цветом pdc-»PatBlt (rcBack.left,
rcBack.top,
rcBack.right —
rcBack.left,
rcBack.bottom —
rcBack.top,
WHITENESS) ;
// Вывести
текст pdc-»DrawText (buf,
-1,
srcBack,
DT_CENTER ¦ DT_BOTTOM I
DT_SINGLELINE) ;
// Освободить
DC pBB-»ReleaseDC(pdc) ;
//
Переключить буфера
if (m_pTI-»bFullScreen)
{ pFB-»Flip() ;
} else (
pFB-»Blt (SrcFront,
pBB, SrcBack) ;
} } )
Тестирование
начинается с получения указателей
на первичный и вторичный буфера и
подготовки прямоугольника,
описывающего их. Обратите внимание
на то, что размер прямоугольника
первичного буфера зависит от того,
в каком режиме проводится
тестирование — в полноэкранном или
оконном. В оконном режиме
поверхность первичного буфера
используется совместно с другими
приложениями, работающими в
системе.
Каждый цикл
тестирования в функции TestGDIText
состоит из следующих этапов:
1. Определить
текущее время и вычислить текущую
скорость вывода.
2. Создать
выводимый текст.
3. Получить
контекст устройства для вторичного
буфера.
4. Стереть
весь вторичный буфер, заполняя его
белым цветом с помощью функции GDI.
5. Вызвать
другую функцию GDI для вывода текста
во вторичный буфер.
6. Освободить
контекст устройства.
7.
Переключить буфера и вывести
результат на экран.
После
получения DC в программе
встречаются привычные вызовы
функций GDI, так что нетрудно забыть,
что мы работаем с поверхностью
DirectDraw, а не с оконным DC.
Разумеется,
суть последнего этапа не всегда
заключается в том, что первичный
буфер меняется местами со
вторичным. При работе в
полноэкранном режиме функция
CDDSurface::Flip действительно меняет
буфера местами, однако в оконном
режиме содержимое вторичного
буфера копируется в первичный
функцией Bit. В случае смены буферов
не нужно беспокоиться о новой роли
каждого из них, так как за всем
следит DirectDraw, и когда мы требуем
указатель на вторичный буфер, то
всегда получаем именно то, что
нужно.
Производительность
GDI при выводе текста оказалась
хорошей, и потому в следующем тесте
я решил определить, насколько
быстро GDI может рисовать на
поверхности.
Тестирование GDI
при работе с графикой
Чтобы
проверить производительность GDI
при работе с графикой, я внес
небольшие изменения в приведенную
выше функцию и заставил ее рисовать
средствами GDI прямоугольник,
который перемещается внутри окна.
Для этого мне пришлось
добавить в функцию CTestWnd::TestGDIGfx
приведенную ниже строку и в каждом
цикле отслеживать положение
прямоугольника в окне:
Вы можете
самостоятельно протестировать
интерфейс GDI и посмотреть, как он
справляется с рисованием
прямоугольников.
Тестирование
DirectDraw при работе со спрайтами
Этот тест
значительно интереснее предыдущих.
Я хотел воспользоваться
поверхностью в видеопамяти для
создания спрайта, задать
прозрачные области с помощью
цветового ключа и затем посмотреть,
насколько быстро можно будет
двигать спрайт в окне. За его основу
я взял тест по выводу текста и
добавил к нему два дополнительных
фрагмента. Первый из них создает
спрайт, а второй в цикле выводит
спрайт во вторичный буфер. В
течение некоторого времени я
мучительно размышлял над тем, как
же мне создать спрайт, потом махнул
рукой и воспользовался функциями
GDI:
void
CTestWnd::TestDDSprite() {
DDSURFACEDESC sd;
memset(&sd, 0,
sizeof(sd));
sd.dwSize = sizeof(sd);
sd.dwFlags = DDSD_WIDTH
I DDSD_HEIGHT
I DDSD_CAPS;
sd.dwWidth = ex;
sd.dwHeight = cy;
sd.ddsCaps.dwCaps =
DDSCAPS_OFFSCREENPLAIN;
CDDSurface sprite;
BOOL b =
sprite.Create(m_pDD, &sd) ;
ASSERT(b) ;
// Получить DC
для поверхности спрайта.
//
Нарисовать спрайт в виде красного
круга на черном
Я создал
поверхность, размер которой равен
размеру спрайта (сх х су) и затем
воспользовался функциями GDI, чтобы
заполнить поверхность черным
цветом и нарисовать красный круг.
Поскольку цветовым-ключом
поверхности задан черный цвет, при
выводе спрайта рисуется только
красный круг. Признаюсь, здесь я
немного смошенничал и выбрал
черный цвет в качестве цветового
ключа лишь потому, что 0
соответствует черному цвету
независимо от того, как его
рассматривать — как RGB-значение или
индекс палитры.
Создав
простой спрайт, я добавил в функцию
следующий фрагмент, который рисует
спрайт во вторичном буфере:
// Закрасить
буфер белым цветом CDC* pdc = pBB-»GetDC() ;
ASSERT(pdc) ;
pdc-»PatBlt (rcBack. left,
rcBack.top,
rcBack.right —
rcBack.left,
rcBack.bottom —
rcBack.top, i
WHITENESS) ;
// Вывести текст
pdc-»DrawText (buf,
-1,
SrcBack,
DT_CENTER ¦ DT_BOTTOM ¦
DTJ3INGLELINE) ;
pBB-»Relea5eDC (pdc) ;
// Вывести
спрайт RECT rcDst;
rcDst.left = x;
rcDst.top = y;
rcDst.right = rcDst.left +
ex;
rcDst.bottom = rcDst.top +
cy;
RECT rcSrc;
rcSrc.left = 0;
rcSrc.top = 0;
rcSrc.right = rcSrc.left +
ex;
rcSrc.bottom = rcSrc.top +
cy;
//
Вызвать Bit с цветовым ключом pBB-»Blt
(SrcDst, Ssprite, SrcSrc, DDBLT WAITIDDBLT KEYSRC) ;
Перед тем как
копировать спрайт во вторичный
буфер, мы с помощью функции GDI
закрашиваем буфер белым цветом и
выводим строку с количеством
кадров в секунду. Обратите внимание
на то, что среди аргументов функции
Bit имеется флаг DDBLT_KEYSRC, который
указывает ей на необходимость
использования цветового ключа
поверхности-источника. Для того
чтобы прозрачные области
обрабатывались правильно,
необходимо задать цветовой ключ в
поверхности-источнике и указать
этот флаг. Я провел пять или шесть
часов в недоумении, пока не
догадался включить флаг DDBLT_KEYSRC в
вызов функции Bit. Что тут можно
сказать? Видимо, я соображаю
недостаточно быстро.
Поскольку в
окне этого приложения нет ничего,
кроме красного круга, вам придется
самостоятельно запустить тест и
посмотреть на скорость работы.
Думаю, она произведет на вас
впечатление.
Тестирование
прямого доступа к пикселям
Я едва не
отказался от этого теста. Когда в
течение целого дня мой компьютер
«зависал» через каждые пять минут,
я возненавидел DirectDraw и решил, что в
дальнейшем буду работать с
графикой только через GDI. Все это
время я пытался заблокировать
буфер, чтобы получить возможность
писать в него. Вместе с буфером
почему-то блокировался и компьютер,
и мне приходилось перегружаться.
Намучившись, я лег спать, и в лучших
нердовских традициях решение
пришло во сне. Чтобы моя программа
заработала, из нее нужно было
убрать всего один символ. Привожу
старую и новую версию функции
CDDSurface::Unlock из файла SdDirDraw.cpp, которая
причинила мне столько огорчений:
Удалось ли
вам найти отличие? Эти указатели
так похожи друг на друга — до тех
пор, пока вы не попытаетесь их
использовать!
Хорошо
запомнив полученный урок, я дописал
код для тестирования прямого
доступа к пикселям. Он стирает
содержимое вторичного буфера,
выводит в него строку с
количеством кадров в секунду и
рисует горизонтальные цветные
линии посредством прямого доступа
к пикселям.
Перед тем как
что-либо рисовать в буфере,
необходимо выполнить два условия.
Прежде всего следует заблокировать
буфер и получить указатель на
связанную с ним область памяти.
Затем нужно проверить формат
поверхности и определить способ
записи отдельных пикселей.
Вероятно, в реальном приложении
можно выбрать один формат буфера и
пользоваться только им, но в нашем
тесте формат проверяется каждый
раз заново.
После того
как мы будем знать количество бит
на пиксель и маски для красной,
зеленой и синей составляющих (или
индексы палитры), можно приступать
к рисованию. Чтобы определить
смещение в буфере, по которому
нужно записать пиксель, умножьте
ширину буфера на номер строки.
Ширина буфера измеряется в байтах.
Она может отличаться от количества
пикселей, умноженного на
количество байт на пиксель, потому
что строки часто дополняются по
границе ближайших 4 байтов
(32-разрядное выравнивание).
Приведенный
ниже фрагмент определяет формат
пикселей для вторичного буфера:
void
CTestWnd::TestDirectPixels() (
// Получить
информацию о вторичном буфере
int iBpp =
pBB-»GetBitsPerPixel () ;
ASSERT(iBpp »= 8);
int iWidth -
pBB-»GetWidth() ;
int iHeight =
pBB-»GetHeight () ;
int iPitch =
pBB-»GetPitch() ;
// Получить
RGB-маски DWORD dwRMask, dwGMask, dwBMask;
pBB-»GetRGBMasks
(dwRMask, dwGMask, dwBMask) ;
// Для каждой
маски определить ширину в битах // и
количество бит, на которые маска //
смещена от конца младшего байта DWORD
dwRShift, dwGShift, dwBShift;
Обратите
внимание на то, что цветовые маски
нужны лишь в том случае, когда
пиксели поверхности кодируются
более чем 8 битами. Кроме того,
предполагается, что поверхность
имеет формат RGB, а не YUV или
какой-нибудь другой. В случае
8-битной кодировки пикселей
необходимо создать палитру:
// Если буфер
имеет 8-битную кодировку пикселей, //
получить элементы палитры и
присвоить им // нужные цветовые
значения PALETTEENTRY ре [256];
BYTE r, g, b;
CDDPalette* pPal =
pBB-»GetPalette () ;
if (pPal) {
pPal-»GetEntries (0, 256,
ре) ;
//
Задать нужные цветовые значения. //
Мы воспользуемся моделью с 2-битной
кодировкой // R, Си В-составляющих for
(r = 0; r « 4; г++) { for (g = 0; g « 4; g++) ( for (b = 0; b
« 4; b++) {
int index =10+r*16+g*4+b;
pe[index].peRed = r * 85;
ре[index].peGreen = g *
85;
pe[index].peBlue = b * 85;
}
// Заполнить
оставшиеся элементы палитры серым
цветом, // чтобы их можно было
увидеть при отладке for (int i = 10 + 4*4*4; i
« 246; i++) {
Я заполнил
свободные элементы палитры серым
цветом, чтобы проследить за тем, как
механизм визуализации
распределяет используемые цвета.
Рисование линий в буфере
происходит следующим образом:
//
Заблокировать буфер и получить
указатель.
// ВНИМАНИЕ:
Не пытайтесь включать пошаговое
выполнение
// до вызова
Unlock.
BYTE* pBuf = (BYTE*)
pBB-»Lock () ;
if (pBuf) (
for (int у = 0; у «
iHeight; y++) (
// Определить
смещение начала строки int n = iwidth;
DWORD
dwOffset = у * iPitch; // В байтах
// Получить
цвет int ir = GetRValue(cIrLine);
int ig =
GetGValue(clrLine);
int ib =
GetBValue(cIrLine);
// Вывести
пиксели непосредственно в буфер
switch (iBpp) (
case 8: {
// Найти
индекс для цвета
// в
соответствии с принятой нами
моделью int index = 10 + (ir / 85) * 16 + (ig /85) * 4 +
(ib / 85) ;
BYTE* p =
pBuf + dwOffset;
while (n-) {
*p++ = (BYTE) index;
) breaks-case 16: (
// Построить
цветовое значение
DWORD dw = (ir »» (8 -
dwRBits)) ««
dwRShift
I (ig »» (8 -
dwGBits)) «« dwGShift
I (ib »» (8 -
dwBBits)) «« dwBShift;
WORD w = (WORD)dw;
WORD* p = (WORD*)(pBuf +
dwOffset);
while (n—) *p++ = w;
) breaks-case 24:
// Упражнение для
самостоятельной работы:
breaks-case 32: {
DWORD dw = (ir »» (8 - dwRBits))
«« dwRShift
I (ig »» (8 - dwGBits)) ««
dwGShift
I (ib »» (8 - dwBBits)) ««
dwBShift;
DWORD* p = (DWORD*)(pBuf +
dwOffset);
while (n—) *p++ = dw;
) breaks-default:
break;
i
// Перейти к
следующему цвету NextColor(clrLine) ;
} pBB-»Unlock() ;
// Снова можно
работать с отладчиком
> NextColor(cIrStart) ;
Программа
несколько отличается для разных
цветовых кодировок, поскольку
пиксели занимают разное количество
байт и для них требуются различные
RGB-маски. Пожалуйста, соблюдайте
осторожность при выполнении
арифметических операций с
указателями. Вы можете перепутать
указатели на байт с указателями на DWORD
и получить неверный результат,
поскольку компилятор прибавит 2 или
4 вместо 1 или наоборот.
Чтобы тест
генерировал другой набор линий,
следует внести изменения в функцию
NextColor, которая определяет цвет для
вывода следующей линии.
Наверняка вы
обратили внимание, что я пропустил
код для 24-битной кодировки. Мой
видеоадаптер работает только с 8-,
16-и 32-битными цветами, поэтому я не
мог протестировать 24-битный код.
Результаты данного теста приведены
на цветной вкладке.
Веселье
продолжается
Разработка
приложений для DirectDraw в моем
представлении не является
утомительным и занудньм
кодированием. Скорее это сплошное
развлечение. Хотелось бы видеть,
как вы примените на практике
продемонстрированные мной методы.
Если после
этой главы вам нестерпимо
захотелось написать свой
собственный механизм визуализации,
продолжайте читать. В следующей
главе рассмотрена прослойка DirectSD —
перед тем, как нажимать клавиши,
стоит познакомиться с ней поближе.
Emanual.ru – это сайт, посвящённый всем значимым событиям в IT-индустрии: новейшие разработки, уникальные методы и горячие новости! Тонны информации, полезной как для обычных пользователей, так и для самых продвинутых программистов! Интересные обсуждения на актуальные темы и огромная аудитория, которая может быть интересна широкому кругу рекламодателей. У нас вы узнаете всё о компьютерах, базах данных, операционных системах, сетях, инфраструктурах, связях и программированию на популярных языках!