Автор: Азиз (JINX) Специально для Королевства
Delphi
Как обычно, начну с оговорок. Первое – для меня большая
проблема перевести некоторые термины. Поэтому я думаю может и не
стоит их переводить. :-) Вот список терминов, которые вызывают у
меня трудности с переводом:
- blitting - blit сокращение от "bit block transfer"
пересылка блоков данных из одной области видеопамяти в другую.
- flip – переключение между буферами видеопамяти
- Surface – "поверхность" – область видеопамяти
Второе – разговор идет о использовании DirectDraw в
Delphi. Для того, чтобы воспользоваться DirectX вообще и DirectDraw
в частности, нужно, чтобы во-первых DirectX был установлен на
компьютере (скачать его можно у Microsoft например, впрочем я не
думаю, что для читателя будет проблемой его найти), во-вторых нужны
файлы заголовков DirectX – их существует немало, я по-прежднему
считаю компонент DelphiX от Hiroyuki Hori – превосходным , кроме
того, существует официально поддерживаемые Borland'ом заголовки
DirectX, составленные в рамках проекта "JEDI" – скачать их можно с
(http://www.delphi-jedi.org/DelphiGraphics/).
Третье – неплохо если Вы имеете некоторое общее представление о
работе видеоадаптера (ну очень общее – тонкости не нужны) и еще
более общее о COM-технологии (всего-то нужно знать что такое COM-
Interface, впрочем и это не обязательно).
DirectDraw – интерфейс DirectX, предназначенный, по существу,
для управления видеопамятью.
Прелесть однако заключается в том, что с DirectDraw доступ к
видеопамяти становится не зависимым от типа используемой видеоплаты
(ну или почти не зависимым). DirectDraw обращается к апаратуре
посредством hardware abstraction layer (HAL) – (уровня
абстагирования от аппаратуры). Кроме того с помощью hardware
emulation layer (HEL) (уровня эмуляции аппаратуры) те возможности,
которые не реализованы в данной видеокарте эмулируются программно (к
сожалению тут есть пара исключений). Благодаря такому подходу жизнь
программиста становится легче и веселее – если, например, видеокарта
поддерживает hardware blitting – DirectDraw использует эту
возможность через HAL – если нет – эмулирует через HEL (естественно
эмуляция всегда медленнее). На рисунке из SDK показаны отношения
между DirectDraw, GDI, HAL и HEL.
Как видно из рисунка DirectDraw находится вне GUI. DirectDraw
может предоставлять области памяти, с которыми он работает в виде
контекста устройства (device context, привычный для
Windows-программиста), что позволяет использовать функции GDI для
работы с ним (например, выводить текст с помощью функции TextOut)
DirectDraw можно использовать как при рисовании в окне
Windows так и при работе в полноэкранном режиме. Я пока буду
говорить только о полноэкранном режиме (с эксклюзивным уровнем
кооперации).
Видео режимы. Режим определяет размер видимой области
экрана в пикселах и число бит, требуемых для представления одного
пиксела (“глубина цвета ”) (практически все мониторы поддерживают
например режим 640ґ480ґ8). Чем больше ширина и высота экрана в
пикселах, и чем больше бит требуется для представления одного
пиксела, тем больше видеопамяти требуется для режима.
Кроме того видеорежимы бывают палитровыми (palettized) и
безпалитровыми (non-palettized). В палитровых режимах “глубина
цвета” означает число элементов палитры для данного режима, например
8-битовый палитровый режим означает, что используется палитра,
размером 256 элементов. В безпалитровом режиме “глубина цвета”
означает число бит для представления цвета (8 бит – 256 цветов, 16
бит – 65535 цветов и т.д.) Чтобы выяснить какие режимы
поддерживает ваша видеокарта можно использовать интефейс
IDirectDraw4::EnumDisplayModes.
Пример:
выясним все поддерживаемые видеорежимы {используем DirectX
headers от JEDI}
function MyEnumFunction(const lpDDSurfaceDesc: TDDSurfaceDesc; lpContext:
Pointer): HResult; stdcall
var
SMode: string;
begin
SMode := IntToStr(lpDDSurfaceDesc.dwWidth) + ' X ';
SMode := SMode + IntToStr(lpDDSurfaceDesc.dwHeight) + ' X ';
SMode := SMode + IntToStr(lpDDSurfaceDesc.ddpfPixelFormat.dwRGBBitCount);
Form1.ListBox1.Items.Append(SMode);
end;
procedure TForm1.Button2Click(Sender: TObject);
var
DD: IDirectDraw;
hr: HRESULT;
begin
hr := DirectDrawCreate(nil, DD, nil);
if (hr = DD_OK) then
begin
ListBox1.Clear;
DD.EnumDisplayModes(0, nil, nil, MyEnumFunction);
end;
end;
|
{то же используя компонент DelphiX}
procedure TForm1.Button1Click(Sender: TObject);
var
i: integer;
SMode: string;
begin
ListBox1.Clear;
for i := 0 to DXDraw1.Display.Count - 1 do
begin
SMode := IntToStr(DXDraw1.Display.Modes[i].Width) + ' X ';
SMode := SMode + IntToStr(DXDraw1.Display.Modes[i].Height) + ' X ';
SMode := SMode + IntToStr(DXDraw1.Display.Modes[i].BitCount);
ListBox1.Items.Append(SMode);
end;
end;
|
Чувствуете почему я так люблю Hiroyuki Hori с его компонентом
DelphiX? :-) Действительно проще – но, увы, документация у DelphiX
очень скудная (и по большей части на японском). Вообще говоря,
наверное полезно изучить “классический” способ работы с DirectDraw
от JEDI – потом легче пользоваться и DelphiX.
Установить видеорежим можно методом IDirectDraw4::SetDisplayMode.
Установим видеорежим 640x480x8 {используем DirectX headers от
JEDI}
procedure TForm1.Button1Click(Sender: TObject);
var
DD: IDirectDraw;
DD4: IDirectDraw4;
hr: HRESULT;
begin
hr := DirectDrawCreate(nil, DD, nil);
if (hr = DD_OK) then
begin
DD.QueryInterface(IID_IDirectDraw4, DD4);
DD4.SetCooperativeLevel(Self.Handle, DDSCL_EXCLUSIVE or DDSCL_FULLSCREEN);
DD4.SetDisplayMode(640, 480, 8, 0, 0);
//DD4.RestoreDisplayMode;
end;
end;
|
{то же используя компонент DelphiX}
procedure TForm1.Button1Click(Sender: TObject);
begin
DXDraw1.Display.Width := 640;
DXDraw1.Display.Height := 480;
DXDraw1.Display.BitCount := 8;
DXDraw1.Options := DXDraw1.Options + [doFullScreen];
DXDraw1.Initialize;
end;
|
Восстановить тот видеорежим, что был установлен до вызова
SetDisplayMode можно функцией IDirectDraw4::RestoreDisplayMode.
Впрочем, для программ использующих полноэкранный режим это не так уж
важно – прежний режим будет восстановлен автоматически.
Кстати пример с JEDI-заголовками хорош тем, что демонстрирует
создание объекта IDirectDraw получение ссылки на интерфейс
IDirectDraw4 вызовом метода QueryInterface из IDirectDraw
(IDirectDraw без номера – базовый (и самый старый) интерфейс
DirectDraw; IDirectDraw4 – интерфейс из DirectX 6). Вообще объект
IDirectDraw – это самая, что ни на есть, сердцевина DirectDraw – он
представляет собой некую абстракцию над видеоадаптером – с помощью
его методов создаются все остальные объекты DirectDraw (Surface'ы,
палитры и т.д.). В принципе можно создавать больше одного объекта
IDirectDraw – если у Вас больше одного видеоадаптера и несколько
мониторов – в таком случае Вы ровно во столько раз круче меня, на
сколько число Ваших видеоадаптеров больше 1-го :-) (для знатоков
COM-технологии – для этого при создании объекта DirectDraw нужно
передать GUID другого дисплея). Если же монитор у Вас один Вы можете
создавать несколько объектов DirectDraw – все они будут управлять
одним и тем же видеоадаптером – но мы этот случай рассматривать не
будем.
В случае же если Вы используете Hori'вский компонент DelphiX –
мучения с инициализацией и деинициализацией сводятся к нулю –
достаточно просто разместить на форме компонент DXDraw – он сам
позаботится о мелочах жизни, вроде create и release. :-)
Итак, переключаться между видеорежимами мы научились.
Поговорим теперь о Surface'ах. (моя попытка найти хороший русский
эквивалент этому слову, не увенчалась успехом). Surface (объект
DirectDrawSurface) – в буквальном переводе поверхность, представляет
собой линейный участок в видеопамяти. (впрочем можно создавать
Surface'ы и в системной памяти – но мы на этом не станем
задерживаться) По умолчанию Surface создается так, чтобы получить
максимальное быстродействие – в видеопамяти, если ее не хватает – в
нелокальной видеопамяти (для плат AGP) а если и ее не хватает то в
системной памяти (этот случай самый медленный). Объект
DirectDrawSurface кроме указателя на область видеопамяти содержит
несколько очень полезных методов (и зверски скоростных) для быстрого
переноса квадратика видеоизображения из одного участка Surface'а в
другой (blitting), для быстрой смены одного Surface' а на экране
другим – fliping, для работы с палитрами и спрайтами и др.
Ну как удалось мне вас заинтересовать? Ну тогда давайте
разберемся – как эти самые замечательные Surface'ы создавать.
Перво-наперво скажем что у каждого Surface'а должен быть размер -
ширина и высота. Кроме того Surface'ы устроены так, что между
началом одной строчки и другой расстояние не всегда равное ширине.
Скажем мы создали Surface 640X480X8 – казалось бы между первой
строчкой и второй ровно 640 байт – ан нет. Может 640 байт а может и
больше (это завист от того парня, который писал драйвер Вашего
видеоадаптера). Расстояние между строчками в байтах называется Pitch
– переводится как шаг. Почему этот самый Pitch не всегда равен
ширине видно из рисунка:
Видите – справа от нашего Front-bufera может быть какой-то кэш,
если Вы вздумаете писать напрямую в видеопамять – писать туда (в
кэш) строго не рекомендуется (за последствия никто не ручается).
Кроме того Pitch, в отличие от ширины измеряется в байтах а не в
пикселах.
Раз уж заговорили, про Front-bufer'ы – скажем уж и про то,
что один Surface, называемый PrimarySurface, является главным - это
тот самый Surface, который был виден на экране в момент когда мы
начали создавать эти самые Surface'ы.
Surface'ы могут быть обьединены в так называемые
flip-цепочки. Когда происходит flip между Surface'ами – тот Surface,
что сейчас на экране, заменяется следующим в цепочке, на следующем
flip'е – этот – следующим и т.д. – если дошли до последнего в
цепочке – то он заменяется на первый. Ну в обычной жизни цепочка
может состоять из всего двух Surface' ов – при каждом они просто
flip'е сменяют друг друга. Обратите внимание – при flip'е смена
Surface'ов происходит не в результате пересылки всего их
содержимого, а просто в результате изменения указателей на области
видеопамяти в видеоадаптере – поэтому flip выполняется очень быстро.
(Исключение может быть только в случае если Вы создали столько
Surface'ов, что они не поместились в видеопамяти – тогда за дело
возьмется HEL – бедняге придется все это эмулировать и скорость
будет – не ахти). C помощью flip можно создавать анимацию, выводим
какую-то картинку, затем в BackBuffer'e – чуть-чуть меняем эту
картинку, вызываем flip, чуть-чуть меняем картинку в BackBuffer'e,
вызываем flip и т.д. в цикле.
Вот пример создания Flip-цепочки из двух Surface'ов, обьектов
IDirectDrawSurface4.
(Ссылки на два созданных Surface'а сохраняются в переменных
FPrimarySurface и FbackSurface) (этот пример взят из моей
демо-программульки, которую Вы может скачать здесь 169K) {используются JEDI –
заголовки DirectX}
uses ... DDraw;
var
hr: HRESULT;
SurfaceDesc: TDDSurfaceDesc2;
DDSCaps: TDDSCAPS2;
DD: IDirectDraw;
begin
/// ...начнем, помолясь
hr := DirectDrawCreate(nil, DD, nil); ///создали DirectDraw
if (hr = DD_OK) then
begin
// Получим интерфейс IDirectDraw4
DD.QueryInterface(IID_IDirectDraw4, FDirectDraw);
// интерфейс DirecDraw1 нам больше не нужен
DD := nil;
// Установим эксклюзивный уровень кооперации и полноэкранный режим
hr := FDirectDraw.SetCooperativeLevel(Handle, DDSCL_EXCLUSIVE or
DDSCL_FULLSCREEN);
if (hr = DD_OK) then
begin
hr := FDirectDraw.SetDisplayMode(640, 480, 8, 0, 0);
///переключаем видеорежим на 640X480X8
if (hr = DD_OK) then
begin
// Создаем главный surface с одним back buffer'ом
FillChar(SurfaceDesc, SizeOf(SurfaceDesc), 0);
SurfaceDesc.dwSize := SizeOf(SurfaceDesc);
///говорим что нам нужны back buffer'ы
SurfaceDesc.dwFlags := DDSD_CAPS or DDSD_BACKBUFFERCOUNT;
////говорим что создаем первый Surface
SurfaceDesc.ddsCaps.dwCaps := DDSCAPS_PRIMARYSURFACE
or DDSCAPS_FLIP //// во Flip-цепочке
or DDSCAPS_COMPLEX; //// а вообще будут и дополнительные Surface'ы
//// число этих самых дополнительных - 1
SurfaceDesc.dwBackBufferCount := 1;
///все готово, создаем Surface'ы и запоминаем главный в FPrimarySurface
hr := FDirectDraw.CreateSurface(SurfaceDesc, FPrimarySurface, nil);
if (hr = DD_OK) then
begin
// А теперь получим указатель на back buffer (создали-то два Surface'a сразу)
ddscaps.dwCaps := DDSCAPS_BACKBUFFER;
///получили и запомнили в FBackSurface
hr := FPrimarySurface.GetAttachedSurface(ddscaps, FBackSurface);
if (hr = DD_OK) then
begin
{Все нормально - Surface'ы созданны - выходим}
exit;
end;
end;
end;
end;
end;
{где-то была ошибка - сообщаем об этом неприятном факте}
MessageBox(Self.Handle, PChar('Не удалось инициализировать DirectDraw! ' +
ErrorString(Hr)), 'ERROR', MB_OK);
Close();
end;
|
Создали Surface'ы. Теперь было бы интересно что-нибудь на них
нарисовать. Интересно также попробовать писать прямо в видеопамять.
Получить указатель на область видеопамяти Surface'а можно вызвав
метод Lock – он вернет указатель в структуре типа TDDSURFACEDESC2,
которую получает в качестве параметра.
С фантазией у меня всегда было не очень – поэтому просто заполню
всю область Surface'ов одним цветом, записав в видеопамять одно и
тоже значение.
var
i, j: integer;
AdresVideo: PByteArray;
SurfaceDesc: TDDSURFACEDESC2;
HR: HResult;
begin
// Пишем прямо в видеопамять
FillChar(SurfaceDesc, SizeOf(SurfaceDesc), 0);
SurfaceDesc.dwSize := SizeOf(SurfaceDesc);
HR := FPrimarySurface.Lock(nil, SurfaceDesc, {DDLOCK_WAIT or}
DDLOCK_SURFACEMEMORYPTR, 0);
if (HR = DD_OK) then
begin
AdresVideo := SurfaceDesc.lpSurface;
for i := 0 to SurfaceDesc.dwHeight - 1 do
begin
for j := 0 to SurfaceDesc.dwWidth - 1 do
begin
AdresVideo[j + i * SurfaceDesc.lPitch] := $FF;
end;
end;
FPrimarySurface.Unlock(nil);
end;
end;
|
Обратите внимание - какой я аккуратный – перехожу между
строчками, учитывая Pitch. Да кстати – я просто демонстрирую как
обратится к каждому байту видеопамяти Surface'a на самом деле если
нужно закрасить весь Surface одним цветом то заносить значения в
каждый байт слишком медленно – для этого можно воспользоваться
методом IDirectDrawSurface4.Blt, передав ему флаг DDBLT_COLORFILL.
Кроме того можно выводить на Surface и привычными функциями GDI –
TextOut'ом например:
var
DC: HDC;
begin
if (FPrimarySurface.GetDC(DC) = DD_OK) then
begin
{Выводим текст на 1-й surface, используя GDI-фуекцию TextOut}
SetBkColor(DC, RGB(255, 255, 255));
SetTextColor(DC, RGB(255, 0, 0));
TextOut(DC, 10, 20, ' Проба пера', Length(' Проба пера'));
FPrimarySurface.ReleaseDC(DC);
end;
end;
|
Небольшое лирическое отступление – между вызовами LOCK и UNLOCK,
а также между GetDC и ReleaseDC выполнение всех других программ
останавливается (в том числе и отладчика). Отсюда выводы – первое –
не стоит делать что-то слишком долго между этими вызовами, второе,
отладить программу пошагово между этими вызовами – невозможно (если
только Вы не вооружились Kernel-debuger'ом).
Теперь попробуем flip'ануть наши Surface'ы. Переключимся на
другой Surface hr := FPrimarySurface.Flip(nil, 0);
Метод Flip может отказаться flip'овать
и вернуть, среди прочих, такие коды ошибок:
- DDERR_NOEXCLUSIVEMODE – этот код возврата означает, что наша
программа потеряла эксклюзивный режим. Произойти такое может, если
мы flip'уем в цикле по таймеру, а пользователь зачем-то ушел из
нашей программы, свернув ее или нажав Alt-TAB. В таком случае,
чтобы зря не тратить процессорные циклы, лучше подождать его
возвращения, вызывая функцию Sleep(0) или WaitMessage.
- DDERR_SURFACELOST – потеря Surface'ов пользователь уходил, но
вернулся, Surface'ы нужно забрать назад, вызвав
IDirectDraw4.RestoreAllSurfaces, содержимое их придется
восстановить.
Все вышесказанное касается классического
стиля использования DirectDraw в стиле С от JEDI. Поклонники же
Hori'вского набора DelphiX могут поэкспериментировать c Surface'ами
используя TDXDraw.DDraw.SurfaceCount, TDXDraw.DDraw.Surfaces,
TDXDraw.Flip – вместе с набором компонент распространяются отличные
примеры.
Я очень рад, что Вы дочитали до этого места (если Вы просто
пролистали в конец, не читая, сделайте вид, что это не так,
порадуйте меня, старика) :-). На этом пока все. Если Вы
заинтересовались – скачайте демо-программку и поэкспериментируйте.
Пишите мне – aziz@telebot.com или error@softhome.net – с удовольствием приму Ваши
пожелания и предложения (особенно если предложите какую-нибудь
работу) :-). |