Logo

Linux, OpenGL и FreePascal: Часть Третья.
25.07.08

В предыдущих частях я не затронул несколько вопросов, которые были бы очень полезны при разработке OpenGL-приложений в Linux. Постараюсь "добить" все мелочи в этой части.

Ограничение размеров окна

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

Для того чтобы этого избежать нужно несколько модифицировать функцию CreateWindow из примеров, воспользовавшись структурой TXSizeHints и функцией XSetWMNormalHints. Собственно сама функция XSetWMNormalHints ничего интересного из себя не представляет, и лишь применяет "sizehint's" к выбранному окну. Внимание стоит обратить на саму структуру TXSizeHints, где для реализации ограничения размеров потребуется указать значения для параметров min/max_width и min/max_height. Для того чтобы указанные значения применились, следует присвоить flags такие флаги как PMinSize и PMaxSize. Выполнение этих операций выглядит следующим образом:

  sizehints.flags      := PMinSize or PMaxSize;
  sizehints.min_width  := Width;
  sizehints.max_width  := Width;
  sizehints.min_height := Height;
  sizehints.max_height := Height;
  XSetWMNormalHints( scr_Display, wnd_Handle, @sizehints );

Глядя на код может возникнут вопрос - зачем минимальный и максимальный размеры задаются одинаковыми значениями? Это нехитрый способ фиксировать размер окна и убрать кнопку "максимизации", спертый из сырцов движка Quake III :)

Смена настроек экрана

Именно этот не рассмотренный ранее вопрос является весьма важным при разработке игр, т.к. в оконном режиме мало кто играет :) Как и в Windows, в Linux для смены настроек экрана потребуется предварительно получить список возможных режимов, и при условии наличия нужного режима сменить настройки. Также чтобы перейти в FullScreen-режим потребуется несколько изменить параметры окна.

Итак, первым делом нужно узнать поддерживает ли система расширения XF86VidMode. Делается это вызовом функции XF86VidModeQueryExtension:

  if not XF86VidModeQueryExtension( scr_Display, @i, @j ) Then
    begin
      // Ошибка
    end;

Функция должна возвратить true, если все ок, и в переменные i и j сохранит значения версии установленной библиотеки libxxf86vm.

Далее следует определить текущие настройки, дабы потом их восстанавливать при завершении работы приложения, или смены FullScreen-режима. Делается это не очень прямым и понятным методом, но приведу хак из SDL, который переписал на pascal:

  XF86VidModeGetModeLine( scr_Display, scr_Default, @scr_Desktop.dotclock,
                          PXF86VidModeModeLine( @scr_Desktop + SizeOf( scr_Desktop.dotclock ) ) );

Переменная scr_Desktop имеет тип TXF86VidModeModeInfo, в котором есть описание разрешения экрана. Узнать его можно воспользовавшись параметрами hdisplay(ширина) и vdisplay(высота). Хотя для получения только разрешения экрана, можно воспользоваться "макросами" DisplayWidth и DisplayHeight из заголовка xf86vmode.

Получив текущие настройки экрана, можно узнать все возможные режимы используя функцию XF86VidModeGetAllModeLines:

  XF86VidModeGetAllModeLines( scr_Display, scr_Default, @scr_ModeCount, @scr_ModeList );

В переменной scr_ModeCount сохранится количество режимов, а в массив указателей scr_ModeList(тип PXF86VidModeModeInfo) будут сохранены сами настройки.

Теперь после всех "подготовительных работ" можно перейти непосредственно к смене настроек экрана. Делается это посредством функции XF86VidModeSwitchToMode, которая в качестве аргумента, помимо display и screen, принимает указатель на структуру типа TXF86VidModeModeInfo. В общем вся "сложность" смены настроек экрана сводится к поиску настроек в массиве, и вызову этой функции. В примере функция по смене настроек экрана оформлена следующим образом:

procedure SetScreenOptions( Width, Height : Integer; FullScreen : Boolean );
  var
    modeToSet : Integer;
begin
  scr_FullScreen := FullScreen;
  scr_Width      := Width;
  scr_Height     := Height;
  if FullScreen Then
    begin
      wnd_Width  := Width;
      wnd_Height := Height;
    end;

  if ( Width >= scr_Desktop.hDisplay ) and ( Height >= scr_Desktop.vDisplay ) Then
    scr_FullScreen := TRUE;

  // Перебираем все настройки и ищем есть ли указанные Width/Height
  for modeToSet := 0 to scr_ModeCount - 1 do
    begin
      scr_Settings := scr_ModeList[ modeToSet ]^;
      if ( scr_Settings.hDisplay = scr_Width ) and ( scr_Settings.vdisplay = scr_Height ) Then
        break;
    end;
  if ( scr_Settings.hDisplay <> scr_Width ) or ( scr_Settings.vdisplay <> scr_Height ) Then
    begin
      // Ошибка, т.к. не найден подходящий режим
      exit;
    end;

  if ( scr_FullScreen ) and
    ( scr_Settings.hDisplay <> scr_Desktop.hDisplay ) and
    ( scr_Settings.vDisplay <> scr_Desktop.vDisplay ) Then
    begin
      // Меняем настройки
      XF86VidModeSwitchToMode( scr_Display, scr_Default, @scr_Settings );
      XF86VidModeSetViewPort( scr_Display, scr_Default, 0, 0 );
    end else
      begin
        // Сбрасываем
        ResetScreenOptions;
        XMapRaised( scr_Display, wnd_Handle );
      end;

  UpdateWindow;
end;

Из всего выше приведенного неизвестны лишь новые переменные, и три функции - XF86VidModeSetViewPort, ResetScreenOptions и UpdateWindow. XF86VidModeSetViewPort предназначена для того, чтобы указать начало координат. Дело в том, что при смене настроек экрана - вся рабочая область пользователя останется в его "родном" разрешении(если новое разрешение меньше за предыдущее), а "под программу" будет выделена часть экрана указанного разрешения с центром в точке совпадающей с координатами курсора мыши, но т.к. окно приложения создается в координатах (0, 0), то нужно перевести "взгляд" к этим координатам.

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

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

Последним моментом касательно смены настроек экрана является модификация функции CreateWindow, а именно указание нового флага для окна и перехват устройств ввода для их обработки в полноэкранном режиме. Для того, чтобы всё изображение выводилось непосредственно на экран в обход оконному менеджеру(который помимо управления окном еще и "рисует" его рамку), следует установить wnd_Attr.override_redirect в True и к wnd_ValueMask добавить флаг CWOverrideRedirect. Ну а перехват устройств ввода выполняется функциями XGrabKeyboard и XGrabPointer. Думаю из их названий понятно за что каждая из них отвечает :) Их вызов следует организовать следующими образом:

  XGrabKeyboard( scr_Display, wnd_Handle, True, GrabModeAsync, GrabModeAsync, CurrentTime );
  XGrabPointer( scr_Display, wnd_Handle, True, ButtonPressMask, GrabModeAsync, GrabModeAsync,
                wnd_Handle, None, CurrentTime );

Описывать что к чему я не буду, т.к. все весьма детально все расписано в man'е. И еще - не стоит забывать что в случаи возврата к оконному режиму нужно "разблокировать" устройства ввода функциями XUnGrabKeyboard и XUnGrabPointer.

Вертикальная синхронизация

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

procedure WaitVSync( VSync : Boolean );
  var
    sync : LongWord;
begin
  if ( VSync ) and ( ogl_CanVSync ) Then
    begin
      glXGetVideoSyncSGI( sync );
      glXWaitVideoSyncSGI( 2, ( sync + 1 ) mod 2, sync );
    end;
end;
Работа со временем

Для организации таймеров весьма важно знать функцию, котороя возвращает текущее время :) В FreePascal, получить нужные данные можно двумя способами - функцией fpGetTimeOfDay(в каком из хедеров храниться "оригинальная" GetTimeOfDay я не нашол) и структурой данных TimeVal или функцией asyncGetTicks из модуля libasync, которая уже является обёрткой над fpGetTimeOfDay. В общем в любом случаи функция будет выглядеть так:

function GetTicks : Double;
  var
    t_tmr : TimeVal;
begin
  fpGetTimeOfDay( @t_tmr, nil );
  GetTicks := t_tmr.tv_sec * 1000 + t_tmr.tv_usec / 1000 - t_start;
end;

Тут t_start фигурирует как время старта программы. Для того чтобы компилятор "знал" тип TimeVal, следует подключить модуль Unix.

Курсор мыши

Т.к. тема курсора мыши может плавно перейти к описанию довольно таки обширной темы Graphical Context для X Windows, я решил опустить описание некоторых функций в ниже приведенном коде, и расскажу лишь общую суть. Изображение курсора мыши в Linux представляется типом TCursor, которому можно присвоить константные значения либо через функцию XCreatePixmapCursor создать свой курсор из Pixmap'а. Для того чтобы присвоить окну курсор используется функция XDefineCursor.

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

procedure ShowCursor( Show : Boolean );
  var
    mask   : TPixmap;
    xcolor : TXColor;
begin
  if wnd_Handle = 0 Then exit;

  wnd_ShowCursor := Show;
  case Show of
    TRUE:
      if wnd_Cursor <> None Then
        begin
          XFreeCursor( scr_Display, wnd_Cursor );
          wnd_Cursor := None;
          XDefineCursor( scr_Display, wnd_Handle, wnd_Cursor );
        end;
    FALSE:
      begin
        mask := XCreatePixmap( scr_Display, wnd_Handle, 1, 1, 1 );
        FillChar( xcolor, SizeOf( xcolor ), 0 );
        wnd_Cursor := XCreatePixmapCursor( scr_Display, mask, mask, @xcolor, @xcolor, 0, 0 );
        XDefineCursor( scr_Display, wnd_Handle, wnd_Cursor );
      end;
  end;
end;
В заключение

Вроде описал все самое необходимое и с этим всем можно приступать к созданию собственного движка/игры/etc. :) В примере, что прикреплен к статье, изменил структуру - теперь всё разбито на модули, и код теперь будет проще использовать в своих целях. Т.к. никаких мыслей по поводу "как показать системщину визуально" в голову не пришло, решил "рисовать" таймер, тем самым показывая использование написанной функции GetTicks. Еще одним признаком изменений является исчезнувшая кнопка максимизации для окна :)


MIRGames
Valid HTML 4.0.1 Strict Valid CSS 3.0 Strict