Вадим Бодров
"Открытые Системы"
Мир ПК, #06/2002
Бытует ошибочное мнение, что механизм DirectDraw слишком сложен для изучения. Цель данной статьи - опровергнуть его.
Уже более пяти лет операционные системы семейства Win32 (Windows 95/98/Me/NT/2000/XP) практически безраздельно властвуют на рынке приложений для платформы Intel. Тем не менее до сих пор многие начинающие программисты не торопятся осваивать Windows. Когда дело касается создания графических приложений, требующих прямого доступа к видеопамяти, они отдают свое предпочтение MS-DOS и низкоуровневым функциям стандартов VGA BIOS и VESA/VBE. Так, и на страницах журнала "Мир ПК" часто можно встретить статьи, посвященные такому подходу. Между тем есть достаточно простой способ решения данных задач средствами Win32, а именно использование части технологии Microsoft DirectX - компонента DirectDraw.
Объект DirectDraw представляет собой обычный COM-объект, порожденный непосредственно от базового интерфейса IUnknown. Напомню, что COM-объекты очень похожи на обычные объекты языка Cи++, но отличаются рядом ограничений. Так, COM-объекты не могут иметь открытых переменных (полей), конструкторов и деструкторов. Для создания COM-объектов обычно используются специальные функции, а для их удаления применяется метод Release(), принадлежащий базовому интерфейсу IUnknown. Поскольку COM-интерфейсы довольно легко реализуются средствами Cи++, в данной статье выбран именно этот язык программирования. Разумеется, работать с объектами DirectX позволяют и другие популярные языки, например Паскаль (Borland Delphi [3], TMT Pascal [4]) и Visual Basic. Стандартный язык программирования Си также позволяет работать с COM-объектами, но при этом значительно усложняется синтаксис вызова функций DirectDraw, и потому при выборе транслятора следует ориентироваться на Microsoft Visual C++ 6.x.
Первый шаг к применению компонента DirectDraw - реализация базового DirectDraw-объекта при помощи функции DirectDrawCreate(), обычно находящейся в динамической библиотеке DDRAW.DLL и объявленной в заголовочном файле ddraw.h:
LPDIRECTDRAW lpDDraw; hResult = DirectDrawCreate(NULL, &lpDDraw, NULL);
Первый параметр функции DirectDrawCreate() является указателем на GUID (Globally Unique Identifier), определяющим создаваемый драйвер. Чаще всего в качестве этого параметра применяется константа NULL, идентифицирующая активное графическое устройство (Active Display Device). В случае удачного вызова функция возвращает значение DD_OK и помещает указатель на порожденный объект DirectDraw в переменную lpDDraw. Последний параметр функции DirectDrawCreate() предназначен для COM-агрегирования (COM aggregation), реализующего разделение интерфейсов многократного использования при наследовании одним объектом методов другого. В текущей версии DirectX этот параметр должен иметь значение NULL.
Полученный указатель на объект DirectDraw теперь можно применять для задания прав доступа к экрану (Cooperative Level) и установки графического режима (Display Mode). Права эти задаются с помощью метода SetCooperativeLevel(). Обычно требуется задавать полноэкранный режим с исключительными правами доступа. Делается это следующим образом:
hResult = pDDraw->SetCooperativeLevel (hWnd, DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN);
В качестве первого аргумента SetCooperativeLevel() принимает дескриптор основного окна приложения, которое должно быть создано предварительно (см. листинг). Второй аргумент метода устанавливает флаги, собственно и определяющие уровень доступа.
Теперь настало время заняться установкой графического режима. Эта задача решается при помощи метода SetDisplayMode(), аналога функции 02h прерывания Int 13h VESA/VBE, но в отличие от нее позволяет напрямую задавать горизонтальное и вертикальное разрешение экрана, а также максимальное количество цветов, доступное в данном режиме. Например, установка 256-цветного режима с разрешением 640x480 точек будет иметь вид:
hResult = lpDDraw->SetDisplayMode(640, 480, 8);
Следующая же функция устанавливает режим 800x600 точек, поддерживающий уже 65 536 цветов.
hResult = lpDDraw->SetDisplayMode(800, 600, 16);
Здесь следует отметить, что современные версии пакета DirectX содержат новые компоненты DirectDraw2, DirectDraw4 и DirectDraw7, обладающие более широкими возможностями, чем рассматриваемый компонент DirectDraw. Так, они позволяют устанавливать вертикальную частоту развертки монитора, возвращают объем видеопамяти и т.п. Однако для данной статьи мы возьмем именно DirectDraw, поскольку он обладает всеми свойствами, необходимыми для решения поставленной задачи, в частности для получения прямого доступа к видеопамяти в оконных Win32-приложениях.
Следующий шаг - создание первичной поверхности (Primary Surface), являющейся объектом DirectDrawSurface. Здесь следует отметить, что компонент DirectDraw дает возможность порождать множество объектов DirectDrawSurface. Чтобы упростить пример, мы заведем всего одну поверхность DirectDrawSurface, отображенную непосредственно на область видеопамяти для получения прямого доступа к ней.
Перед тем как приступить к формированию первичной поверхности, нужно объявить и инициализировать специальную структуру типа DDSURFACEDESC. Вообще говоря, программа взаимодействует с объектом DirectDraw посредством множества различных структур данных, в том числе и через структуру типа DDSURFACEDESC, представленную в файле ddraw.h следующим образом:
typedef struct _DDSURFACEDESC { DWORD dwSize; DWORD dwFlags; DWORD dwHeight; DWORD dwWidth; union { LONG lPitch; DWORD dwLinearSize; }; DWORD dwBackBufferCount; union { DWORD dwMipMapCount; DWORD dwZBufferBitDepth; DWORD dwRefreshRate; }; DWORD dwAlphaBitDepth; DWORD dwReserved; LPVOID lpSurface; DDCOLORKEY ddckCKDestOverlay; DDCOLORKEY ddckCKDestBlt; DDCOLORKEY ddckCKSrcOverlay; DDCOLORKEY ddckCKSrcBlt; DDPIXELFORMAT ddpfPixelFormat; DDSCAPS ddsCaps; } DDSURFACEDESC;
Полное описание всех полей этой структуры можно найти в работе [2]. Нам же понадобится инициализировать три поля:
Структура DDCAPS также определена в файле ddraw.h:
typedef struct _DDSCAPS{ DWORD dwCaps; } DDSCAPS,FAR* LPDDSCAPS;
Первичная поверхность создается при помощи метода CreateSurface(), принадлежащего объекту DirectDraw:
DDSURFACEDESC ddsd; DDSCAPS ddsc; ZeroMemory(&ddsd, sizeof(ddsd)); ddsd.dwSize = sizeof(ddsd); ddsd.ddsCaps.dwCaps = DDSCAPS _PRIMARYSURFACE; ddsd.dwFlags = DDSD_CAPS; hResult = (lpDDraw->CreateSurface (&ddsd, &lpPrimarySurface, NULL);
Обратите внимание на то, что перед вызовом метода CreateSurface() все неиспользуемые поля структуры типа DDSCAPS должны быть обнулены, а поле dwSize содержать ее точный размер в байтах.
Мы научились создавать и инициализировать объект DirectDraw, устанавливать требуемый графический режим и формировать первичную графическую поверхность. Теперь нужно позаботиться о получении прямого доступа к видеопамяти. Для этого заблокируем поверхность в памяти при помощи метода Lock():
hResult = lpPrimarySurface->Lock (NULL, &ddsd, DDLOCK_WAIT, NULL);
В качестве первого аргумента Lock() получает указатель на структуру типа RECT, содержащую координаты прямоугольной области, к которой мы хотим иметь прямой доступ. В приведенном выше примере вместо указателя на структуру RECT мы взяли значение NULL. Значит, мы хотим, чтобы у нас появился прямой доступ ко всей поверхности, а не к какой-то отдельной ее части. Указатель на саму поверхность передается вторым аргументом метода Lock().
Флаг DDLOCK_WAIT сообщает методу Lock() о том, что при неудачной попытке блокирования поверхности (например, во время проведения blit-операции) следует повторять эту операцию вплоть до возникновения какой-либо иной ошибки типа DDERR_SURFACEBUSY и т.п.
После успешного выполнения (hResult = DD_OK) метод Lock() заполняет структуру ddsd необходимыми нам параметрами блокируемой поверхности. В данном случае это будут параметры DirectDraw-поверхности, отражаемой на активную страницу видеопамяти. Итак, наиболее важными полями структуры ddsd считаются:
Таким образом, имея указатель на начало активной страницы видеопамяти, мы способны непосредственно манипулировать ее содержимым. Например, функция очистки видеостраницы может выглядеть так:
memset(lpSurface, 0, ddsd.lPitch * ddsd.dwHeight);
Следует подчеркнуть очень важный момент при работе с заблокированной поверхностью: после выполнения операций, которые связаны с прямым доступом к памяти, ассоциированной с поверхностью DirectDraw, требуется немедленно разблокировать эту поверхность при помощи метода Unlock():
lpPrimarySurface->Unlock (ddsd.lpSurface);
В противном случае операционная система может зависнуть.
Перед уничтожением главного окна приложения нужно удалить созданные объекты DirectDraw, чтобы освободить занимаемые ими системные ресурсы. Обычно все делается в обработчике события WM_DESTROY. Главное, о чем всегда следует помнить при работе с COM-объектами, - это то, что недопустимо применять метод delete() непосредственно. Дело в том, что удаляемый объект может использоваться другим приложением, ведь мы работаем в многозадачной среде. С подобной проблемой легко справляется метод Release(), который просто уменьшает счетчик ссылок на объект (reference count) и вызывает метод delete() лишь тогда, когда значение этого счетчика равно нулю. Это свидетельствует о том, что удаляемый объект не используется ни одним приложением.
В данном примере мы создали два COM-объекта, причем указатели на них содержатся в переменных lpDDraw и lpPrimarySurface. Значит, при обработке системного события WM_DESTROY нам требуется применить всего два метода:
lpPrimarySurface->Release(); lpDDraw->Release();
Простейшая программа, демонстрирующая прямой доступ к видеопамяти средствами DirectDraw, приведена в листинге.
Листинг
#include <windows.h> #include <ddraw.h> const PHYSICAL_WIDTH = 800; const PHYSICAL_HEIGHT = 600; LPDIRECTDRAW lpDDraw; LPDIRECTDRAWSURFACE lpPrimarySurface; LRESULT CALLBACK DDrawWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); BOOL DDrawInit(HWND hWnd); void DDrawDone(); void DrawScreen(); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { WNDCLASS wndClass; HWND hWnd; MSG msg; ZeroMemory(&wndClass, sizeof(wndClass)); wndClass.style = CS_HREDRAW | CS_VREDRAW; wndClass.lpfnWndProc = DDrawWndProc; wndClass.hInstance = hInstance; wndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION); wndClass.hCursor = LoadCursor(NULL, IDC_ARROW); wndClass.lpszClassName = <DDrawApp>; RegisterClass(&wndClass); hWnd = CreateWindowEx( WS_EX_TOPMOST, wndClass.lpszClassName, <DirectDraw Application>, WS_POPUP | WS_MAXIMIZE, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), NULL, NULL, hInstance, NULL); ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; } LRESULT CALLBACK DDrawWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch(message) { case WM_CREATE: DDrawInit(hWnd); SetTimer(hWnd, 1, 50, 0); return 0; case WM_TIMER: DrawScreen(); return 0; case WM_KEYDOWN: if (wParam == VK_ESCAPE) SendMessage(hWnd, WM_CLOSE, 0, 0); return 0; case WM_DESTROY: KillTimer(hWnd, 1); DDrawDone(); PostQuitMessage(0); return 0; default: return DefWindowProc(hWnd, message, wParam, lParam); } } BOOL DDrawInit(HWND hWnd) { DDSURFACEDESC ddsd; DDSCAPS ddsc; if (DirectDrawCreate(NULL, &lpDDraw, NULL) != DD_OK) return FALSE; if (lpDDraw->SetCooperativeLevel(hWnd, DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN) != DD_OK) return FALSE; if (lpDDraw->SetDisplayMode(PHYSICAL_WIDTH, PHYSICAL_HEIGHT, 8) != DD_OK) return FALSE; ZeroMemory(&ddsd, sizeof(ddsd)); ddsd.dwSize = sizeof(ddsd); ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE; ddsd.dwFlags = DDSD_CAPS; if (lpDDraw->CreateSurface(&ddsd, &lpPrimarySurface, NULL) != DD_OK) return FALSE; return TRUE; } void DDrawDone() { if (lpPrimarySurface != NULL) lpPrimarySurface->Release(); if (lpDDraw != NULL) lpDDraw->Release(); } void DrawScreen() { DDSURFACEDESC ddsd; static int pos; ZeroMemory(&ddsd, sizeof(ddsd)); ddsd.dwSize = sizeof(ddsd); if (lpPrimarySurface->Lock(NULL, &ddsd, DDLOCK_WAIT, NULL) == DD_OK) { char* buffer = (char*)ddsd.lpSurface; for (int i = 0; i < PHYSICAL_HEIGHT / 2; i++) { memset(buffer + (i * ddsd.lPitch), i + pos, PHYSICAL_WIDTH); memset(buffer + ((PHYSICAL_HEIGHT / 2 + i) * ddsd.lPitch), i - pos, PHYSICAL_WIDTH); } pos++; lpPrimarySurface->Unlock(ddsd.lpSurface); } }
Cтандарт VESA/VBE отображает видеопамять в виде отдельных окон (банков) размерами не более 64 Кбайт, причем только одно из них может быть активным в заданный момент времени. Переключение таких окон значительно усложняет алгоритмы обработки графики, а также замедляет скорость работы всего приложения. Стандарт VESA/VBE, начиная с версии 2.0, позволяет добиться линейного доступа к видеопамяти в режиме LFB (Linear Flat-frame Buffer) аналогично тому, как это делает DirectDraw. К сожалению, приложения MS-DOS, использующие режим LFB, не могут работать под управлением ОС семейства Windows NT (Windows NT/2000/XP). Это значительно ограничивает область применения LFB и практически сводит на нет все его преимущества перед обычными "оконными" режимами VESA/VBE.
В отличие от упомянутых режимов компонент DirectDraw представляет видеопамять в виде непрерывного линейного массива (вектора), напрямую определяющего цвета отображаемых пикселов. Таким образом, программирование графики в режиме прямого доступа к видеопамяти в Win32-приложениях гораздо более простая задача, чем использование средств VESA/VBE в среде MS-DOS.