Данная публикация адресуется новичкам в области программирования компьютерной графики, желающим использовать графическую библиотеку Microsoft DirectX. Сразу оговорюсь:
— затронутая тема, наверняка, относится и к OpenGL, но я это не проверял опытным путём (созданием приложений под OpenGL), поэтому в заголовке упоминаю только Direct3D;
— приводимые здесь примеры кода относятся к языкам Delphi/FreePascal, но перечисленные «рецепты» по большому счету универсальны в пределах целевой ОС (Windows) — их можно применять к любому языку программирования и, с высокой вероятностью — к любой высокоуровневой библиотеке компонентов, помимо VCL (Delphi) и LCL (Lazarus);
— данная публикация не затрагивает тему создания каркасного приложения Direct3D и методов работы с графическими библиотеками DirectX и OpenGL; все эти вещи хорошо освещены в других источниках, и мне практически нечего к этому добавлять.

Итак, ближе к теме. При разработке приложений с трёхмерной графикой для построения каркаса учебного (а тем более — рабочего) приложения обычно рекомендуется использовать чистый Win32 API… Но если очень хочется использовать в своих приложениях ещё и преимущества высокоуровневых библиотек компонентов, тогда добро пожаловать под кат.

Введение в проблему


При использовании чистого Win32 API цикл обработки поступающих оконных сообщений приходится прописывать «вручную», и обычно это выглядит примерно так:

repeat
  if ( PeekMessage(msg, 0, 0, 0, PM_REMOVE) ) then
  // если есть какое-то сообщение в очереди - получаем его и обрабатываем
  begin
    TranslateMessage(msg);
    DispatchMessage(msg);
  end
  // иначе немедленно выполняем очередную отрисовку 3D-сцены
  else
    RenderScene();

  // и вот так повторять до завершения работы приложения
until ( msg.message = WM_QUIT );

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

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

Отступление по поводу маскировки исключений


Я был удивлен, когда не смог нормально запустить откомпилированный в Lazarus проект с использованием Direct3D, стабильно получая при запуске программы исключения, «кивающие» на вычисления с плавающей запятой. Потратив некоторое время на изучение проблемы, так и не нашел в интернете прямых сведений об этой проблеме, но обратил внимание, что если компилировать проект в Delphi для 64-разрядной архитектуры, то при выполнении получаю весьма похожую по сути ошибку. Изучение содержимого окон Debug-режима в Delphi показало, что для FPU-расширения процессора регистр маскировки исключений MXCSR имеет различные значения во всех рассмотренных случаях. Даже после этого нагуглить ничего стоящего тоже не удалось, кроме упоминания о том, что модуль OpenGL из стандартной поставки Delphi содержит в секции «initialization» строку, которая устанавливает маскировку исключений на все возможные случаи.

Маскировка исключений FPU не относится к теме этой публикации, поэтому не буду сильно заострять на ней внимание. Приведу только самый простой пример: когда умножение очень больших чисел с плавающей запятой приводит к переполнению, то в этом случае происходит одно из двух: результат умножения становится равным INFINITY (или -INFINITY), если включена маскировка соответствующего исключения; либо процессор генерирует исключительную ситуацию «floating point overflow» (которая должна быть обработана программой в блоке «try except»), если маскировка соответствующего исключения отключена.

В итоге, попробовав установить в своих проектах маскировку исключений так, как это сделано в стандартном модуле OpenGL, я добился того, чтобы мои Direct3D-приложения работали как в Lazarus, так и в Delphi (включая 64-битную платформу) без проблем.

К сожалению, мне не удалось найти в MSDN или других источниках (может, плохо искал?) указаний на то, что нужно делать именно так и никак иначе, но тем не менее, рекомендую читателям в своих Direct3D-проектах прописывать следующий код:

uses Math;
...
INITIALIZATION
  Math.SetExceptionMask([exInvalidOp..exPrecision]);
END.

При этом должен заметить, что маскировка исключений будет иметь определённые побочные эффекты, которые обязательно следует учитывать. Например, становится «возможным» деление на ноль (с такой проблемой люди столкнулись, например, здесь), поэтому при выполнении вычислений с плавающей запятой нужно обязательно проверять промежуточные результаты.

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

var
  mask: TArithmeticExceptionMask;
begin
  mask := SetExceptionMask([]);  // отключаем всю (или как Вам нужно) маскировку исключений
  try
    // все необходимые вычисления с плавающей запятой
  finally
    SetExceptionMask(mask);  // возвращаем обратно предыдущие флаги маскировки исключений
  end;
end;

На этом я закругляюсь с вопросом маскировки исключений.

Ещё одно отступление — а зачем нам это нужно?


Целей для создания Direct3D-приложений с использованием высокоуровневых библиотек компонентов может быть много. Например, отладка каких-то моментов, таких как шейдеры и эффекты. А может быть, вы создаёте собственный 3D-движок и нуждаетесь в редакторе файлов определений, на основе которых движок будет выполнять загрузку ресурсов и отрисовку сцен? В таких случаях хотелось бы иметь возможность видеть сразу результат, а при необходимости — редактировать что-то и «на лету» при помощи «вменяемого» пользовательского интерфейса со строками меню, модальными диалогами и т.д. и т.п.

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

На этом отступления заканчиваются, и я перехожу к основной теме.

Способ тривиальный — событие TForm.OnPaint, функция Windows.InvalidateRect()


Программисты, знакомые не только с высокоуровневыми библиотеками компонентов, но и с чистым Win32 API, наверняка уже сложили в голове простую схему: «надо отрисовывать Direct3D-сцену в обработчике события формы (или другого компонента), именуемом OnPaint, и там же, по окончанию отрисовки, вызывать функцию InvalidateRect() из Win32 API, чтобы спровоцировать систему на отправку нового сообщения WM_PAINT, которое приведёт к повторному вызову обработчика OnPaint, и так мы далее пойдём по кругу в бесконечном цикле отрисовки, не забывая по ходу дела реагировать и на остальные оконные сообщения».

В общем-то, всё верно.

Вот примерный план кода для обработчика OnPaint:

procedure TFormMain.FormPaint(Sender: TObject);
begin
  // отрисовка Direct3D-сцены
  // ...

  // вывод результатов на экран с помощью интерфейса IDXGISwapChain
  pSwapChain.Present ( 0, 0 );

  // генерация следующего события WM_PAINT для бесконечного цикла отрисовки
  InvalidateRect ( Self.Handle, nil, FALSE );
end;

Но, как говорится, «гладко было на бумаге».

Давайте посмотрим, что получится (напоминаю, что в конце текста будет ссылка на исходные коды — скачав их, читатель может найти подкаталог «01 — OnPaint + InvalidateRect», скомпилировать и запустить программы и убедиться в не очень корректной работе примера).

Проблема 1: при компиляции приложения в Delphi и последующем запуске, Direct3D-сцена отрисовывается как ожидается, но контролы пользовательского интерфейса нормально отображаться не хотят. Пока не изменишь расположение или размер окна программы, не хотят нормально отображаться ни надписи, ни содержимое многострочного поля редактирования, ни статус-бар, ни кнопка… Ну, положим, многострочное поле редактирования более-менее нормально перерисовывается, когда мы начинаем его прокручивать и редактировать содержимое, но в целом результат неудовлетворительный. А если программа в процессе работы открывает диалоговые окна (или хотя бы примитивный MessageBox), то они либо закрываться нормально не хотят, либо отображаться на экране (MessageBox можно закрыть и вслепую кнопкой «пробел», но диалоговое окно, унаследованное от TForm, закрыть у меня уже никак не получается). Для наглядной демонстрации этой проблемы я добавил в главное меню программы-примера пункты «Дополнительно -> О программе (MessageBox)» и «Дополнительно -> О программе (TForm)».

Проблема 2: при компиляции приложения в Lazarus и последующем запуске, впридачу к описанным выше проблемам (как будто их недостаточно), добавляется невозможность завершить работу программы — она не реагирует ни на стандартную кнопку закрытия в заголовке (“X”), ни на пункт меню «Выход»… Чтобы программа завершилась сама, без «помощи» диспетчера задач или комбинации «Ctrl+F2» в IDE, необходимо свернуть программу в таскбар (интересно, почему так?) после нажатия на кнопку закрытия окна.

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

  if ( not ( Application.Terminated ) ) then
    InvalidateRect(Self.Handle, nil, FALSE);

Но вот решить первую проблему так легко, увы, не получится.

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

Примечание: в MSDN можно найти описание функции GetMessage, где упоминается, что сообщение WM_PAINT имеет низкий приоритет по сравнению с другими оконными сообщениями (кроме WM_TIMER — его приоритет ещё ниже), и обрабатывается после всех остальных оконных сообщений.

Итого: факт, как говорится, налицо. Если и не во всех версия ОС, то как минимум в популярной ныне операционной системе Windows 7 (в которой я запускал все приложенные к публикации программы-примеры), ситуация с приоритетом в обработке сообщения WM_PAINT будет несколько посложнее, чем хотелось бы, особенно если приложение использует высокоуровневую библиотеку компонентов, и поэтому полагаться на указанный в MSDN приоритет нельзя.

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

Библиотеки VCL и LCL предлагают программисту в классах, унаследованных от TWinControl, метод Invalidate(). В библиотеке VCL его вызов сводится к вызову вышеозначенной функции InvalidateRect() чистого Win32 API, но в общем случае поведение этого метода зависит от реализации в конкретной библиотеке. Так, в LCL этот метод приводит к вызову другой функции Win32 API, имеющей имя RedrawWindow() — эта функция даёт примерно тот же результат (будет выполнена новая отрисовка окна), но кое-какие нюансы отличаются. Поэтому, чтобы не акцентировать внимание на нюансах, я сразу предложил обратиться к функции InvalidateRect() из Win32 API.

Способ более удачный — задействуем событие Application.OnIdle


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

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

Теперь мы можем убрать из обработчика OnPaint вызов API-функции InvalidateRect() и перенести его в обработчик события Application.OnIdle.

В итоге получается код по примерно такой схеме:

procedure TFormMain.FormCreate(Sender: TObject);
begin
  Application.OnIdle := OnApplicationIdle;
  // прочий код инициализации
  // ...
end;

procedure TFormMain.FormPaint(Sender: TObject);
begin
  // отрисовка Direct3D-сцены
  // ...

  // вывод результатов на экран с помощью интерфейса IDXGISwapChain
  pSwapChain.Present ( 0, 0 );

  // генерации следующего события WM_PAINT здесь больше нет — она перенесена в OnApplicationIdle()
end;

procedure TFormMain.OnApplicationIdle(Sender: TObject; var Done: Boolean);
begin
  if ( Application.Terminated )  // выполняется завершение работы приложения
     or ( { другие условия, при которых не нужно продолжать бесконечный цикл отрисовки } ) then
  begin
    // перерисовка не нужна, завершаем цикл обработки OnIdle()
    Done := TRUE;
    Exit;
  end;

  // будем обрабатывать OnIdle() повторно для обеспечения бесконечного цикла отрисовки
  Done := FALSE;

  // обеспечить сообщение WM_PAINT для последующей отрисовки
  InvalidateRect ( Self.Handle, nil, FALSE );
end;

В приложенных к публикации исходниках можно найти подкаталог «02 — OnPaint + OnApplicationIdle» и убедиться, что программа работает намного лучше, обновляя содержимое всех контролов своевременно и корректно отображая все модальные диалоговые окна.

К вышесказанному хочу добавить ещё вот что: если свернуть окно программы в таскбар и открыть диспетчер задач, то можно увидеть, что программа «кушает» как минимум одно ядро процессора полностью, и это несмотря на то, что рисовать программе по большому счёту нечего и незачем. Если вы хотите, чтобы ваша программа уступала ресурсы CPU другим приложениям в подобных случаях, а также не вызывала глюков в открытых модальных окнах (я такое видел только в Lazarus), то можно модифицировать обработчик события Application.OnIdle следующим способом:

procedure TFormMain.OnApplicationIdle(Sender: TObject; var Done: Boolean);
begin
  if ( Application.Terminated )  // выполняется завершение работы приложения
     or ( Application.ModalLevel > 0 )  // открыты модальные окна
     or ( Self.WindowState = wsMinimized )  // окно программы свернуто
     or ( { другие условия, при которых не нужно продолжать бесконечный цикл отрисовки } ) then
  begin
    // перерисовка не нужна, завершаем цикл обработки OnIdle()
    Done := TRUE;
    Exit;
  end;

  // будем обрабатывать OnIdle() повторно
  Done := FALSE;

  // обеспечить сообщение WM_PAINT для последующей отрисовки
  InvalidateRect ( Self.Handle, nil, FALSE );
end;

Однако, даже в случае обработки события Application.OnIdle невозможно добиться идеального бесконечного цикла отрисовки. Например, когда открыто главное меню окна, то в процессе навигации по нему событие Application.OnIdle не будет вызываться и, соответственно, анимация Direct3D-сцены «остановится». То же самое произойдёт и в случае открытия программой модального диалога или окна MessageBox.

Конечно, с такими проблемками тоже можно побороться. Например, положить на форму объект TTimer, настроить его на срабатывание каждые 50 миллисекунд, и вызывать в его обработчике события всё ту же функцию InvalidateRect() — тогда можно будет надеяться, что и при навигации по главному меню, и при работе с модальными диалогами цикл отрисовки будет продолжать свою работу, но в эти моменты уже не будет возможности адекватно оценивать FPS и производительность отрисовки 3D-сцены в целом. Впрочем, это вряд ли будет интересовать пользователя в те моменты, когда он открывает главное меню и диалоговые окна, поэтому я не акцентирую внимание на непрерывности бесконечного цикла отрисовки — главное чтобы он был и работал в те моменты, когда внимание пользователя сосредоточено на окне с Direct3D-сценой, а остальное уже не столь важно и отдается на откуп читателю — желающие могут реализовать момент с TTimer самостоятельно и убедиться, что это работает вполне ожидаемым образом.

Отрисовка 3D-сцены в отдельный контрол


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

Логичнее будет создать панельку (или иной контрол), которая при необходимости будет менять свои размеры вместе с окном программы (удобно использовать свойство Align для автоматической подгонки размеров контрола) и избавит от «шаманства» с матрицами преобразований при отрисовке Direct3D-сцены.

К сожалению, мне не удалось найти стандартных малофункциональных контролов типа TPanel, которые бы имели «публичный» обработчик события OnPaint, поэтому пришлось реализовать компонент-наследник от TCustomControl (можно и от других классов) и перегрузить его метод Paint().

Такая реализация предельно проста, и приложенные к публикации исходные коды содержат подобный пример в подкаталоге «03 — TCustomControl descendant».

Использование тем оформления Windows и двойная буферизация при отрисовке окон


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

Что же касается проектов Lazarus, то настройка такого манифеста для них отключена, и это, к сожалению, не случайность — я выставил её намеренно, и сейчас объясню почему.

Библиотеки компонентов VCL и LCL умеют использовать двойную буферизацию при отрисовке окон. В проектах-примерах можно увидеть строку в обработчике FormCreate(), отключающую двойную буферизацию.
Почему важно отключать двойную буферизацию, когда мы отрисовываем окно средствами Direct3D? Потому, что отрисовку окон и контролов эти библиотеки выполняют средствами GDI. А поскольку Direct3D в программах-примерах выполняет вывод в окно напрямую, минуя любую «пользовательскую» двойную буферизацию, то и выходит, что библиотека компонентов, при включенной двойной буферизации будет получать в своих заэкранных буферах просто чёрный прямоугольник — ведь в заэкранный буфер мы ничего не рисовали! Таким образом, при каждой отрисовке, при включенной двойной буферизации, будет происходить примерно следующий сценарий: библиотека создает заэкранный буфер, очищает его чёрным цветом, затем наш обработчик OnPaint() рисует сцену средствами Direct3D и выводит её на экран, минуя созданный библиотекой компонентов заэкранный буфер… а после выполнения обработчика OnPaint() библиотека компонентов свой пустой буфер (чёрный прямоугольник) отрисовывает поверх той картинки, которую мы получили средствами Direct3D. В итоге, включив двойную буферизацию, будем иметь весьма заметное (вплоть до чёрного окна с редкими «вспышками») мерцание окна программы. Это можно проверить на программах-примерах, поменяв соответствующую строчку в обработчике FormCreate() в любом из проектов.

Наверное, Вы уже задумались — если во всех программах-примерах двойная буферизация отключена, то какие могут быть проблемы?
Рассказываю — даже при выставлении свойству DoubleBuffered формы (или целевого контрола) значения FALSE, программы, созданные в Lazarus с использованием библиотеки компонентов LCL, всё равно будут использовать двойную буферизацию, когда программе доступны темы оформления для Windows (при помощи упомянутого выше манифест-файла).
Доказательство этого очень даже простое, в заголовочном файле win32callback.inc библиотеки LCL, в коде функции WindowProc(), имеется строка:
useDoubleBuffer := (ControlDC = 0) and (lWinControl.DoubleBuffered or ThemeServices.ThemesEnabled);

обратите внимание на последнюю часть условия — она достаточно красноречиво объясняет бесполезность отключения свойства DoubleBuffered при доступных темах оформления Windows.

Что же касается библиотеки VCL в Delphi, то она умеет обходиться без двойной буферизации при использовании тем оформления, вот аналогичная строчка из недр VCL:
if not FDoubleBuffered or (Message.DC <> 0) then


На этом у меня пока всё, благодарю за внимание.
Полезные дополнения к материалу и конструктивную критику — жду в комментариях.

Исходные коды


Программы-примеры к данной публикации доступны по следующей ссылке:
github.com/yizraor/PubDX_VCL_LCL
спойлер
(гитхабом раньше не пользовался, надеюсь, что получилось правильно)

Комментарии (21)


  1. MrShoor
    29.04.2015 22:03
    +1

    Первый вариант (с вызовом Invalidate в обработке WM_PAINT) — неверный. Вы сами объяснили почему (WM_TIMER).
    Второй вариант с OnIdle у вас тоже не верный. Как минимум из-за того, что вам пришлось городить вот это:

      if ( Application.Terminated )  // выполняется завершение работы приложения
         or ( Application.ModalLevel > 0 )  // открыты модальные окна
         or ( Self.WindowState = wsMinimized )  // окно программы свернуто
         or ( { другие условия, при которых не нужно продолжать бесконечный цикл отрисовки } ) then
    

    А еще из-за того, что вы по прежнему не учитываете, что WM_PAINT может случится, и стандартная отрисовка может «мерцать» в вашем рендере.
    А правильно делать так:
    Рисовать сцену только в OnPaint, а в OnIdle делать только InvalidateRect. Тогда вы решите проблему со свернутыми окнами, заблокированными экраном по Win+L, да и вообще во всех потенциально возможных случаях + исчезнет проблема с мелькающей дефолтной отрисовкой на обработку WM_PAINT.
    Но тут надо быть внимательным, ибо как вы дальше заметили — оно не работает с включенными темами в LCL. Очевидно, что это бага LCL.


    1. yizraor Автор
      30.04.2015 17:06

      Спасибо за комментарий!

      Первый вариант (OnPaint + InvalidateRect) я тоже считаю неверным.
      Но я не мог о нём не упомянуть, поскольку он является самым тривиальным…
      Может быть, я недостаточно акцентировал внимание на неудачности этого способа?

      А правильно делать так:
      Рисовать сцену только в OnPaint, а в OnIdle делать только InvalidateRect.

      В общем-то вариант с использованием OnIdle() у меня работает именно так :)

      Тогда вы решите проблему со свернутыми окнами, заблокированными экраном по Win+L, да и вообще во всех потенциально возможных случаях + исчезнет проблема с мелькающей дефолтной отрисовкой на обработку WM_PAINT.

      А Вы пробовали в программе-примере закомментировать то, что «пришлось городить» и запустить под отладчиком?
      Я, когда запускаю под отладчиком, сворачиваю окно программы и ставлю брейкпоинт в обработчике OnPaint() — то он, этот обработчик не вызывается, но программа всё равно «кушает» процессор.
      Почему? Потому что цикл OnIdle() продолжает работать безо всяких пауз и остановок.

      То условие, которое Вы процитировали, отвечает в моём коде за (временное) прекращение обработки цикла OnIdle():
      procedure TFormMain.OnApplicationIdle(Sender: TObject; var Done: Boolean);
      

      Если параметр Done после исполнения обработчика события будет равено TRUE, то выполнение программы будет приостановлено до тех пор, пока не появятся новые оконные сообщения, а если FALSE — то в случае отсутствия новых оконных сообщений будет повторное срабатывание события OnIdle().

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

      P.S.:
      по поводу заблокированного экрана — спасибо за мысль, сам я не догадался это проверить — но попробую сегодня вечером


      1. MrShoor
        30.04.2015 20:01

        Каюсь, я прочитал вскользь. Подумал что вы отрисовку делаете прямо в OnIdle.
        p.s. Я кстати Done := True (на OnIdle) в своих проектах не использую. Подобное делаю только беглонаписанных тестах/семплах. Зачем бесполезно гонять электроэнергию и греть воздух? Если кадр не менялся, то там и нечего перерисовывать. Если есть какая-то анимация и она в кадре — то класс, играющий анимацию сам вызывает Invalidate.


  1. Blackmorsha
    19.05.2015 11:08

    Мне кажется, автор не рассмотрел еще один, очевидный способ реализации: нам нужен «бесконечный цикл»? repeat until false вполне подходит; нужно обрабатывать оконные сообщения — Application.ProcessMessages в цикле с этим справится; корректно завершать программу — на FormClose сбрасываем флаг. Разумеется цикл запускать будем не прямо в FormCreate, я использую таймер 1 раз для «передачи управления». Все эти OnPaint, OnIdle и прочее оставим VCL — они имеют отношение к его главному циклу, но не к нашему.

    Текст
    procedure TForm1.FormCreate(Sender: TObject);
    begin

    BClose:=False;
    Timer1.Enabled:=True;
    end;

    procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
    begin
    BClose:=True;
    end;

    procedure TForm1.Timer1Timer(Sender: TObject);
    begin
    Timer1.Enabled:=False;
    repeat // main loop
    Application.ProcessMessages;
    DoRender;
    until BClose;
    end;


    1. Blackmorsha
      19.05.2015 11:28

      Исходники примера + exe тут: yadi.sk/d/-GSPdSvUgVUM2


      1. Blackmorsha
        19.05.2015 11:57

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


    1. MrShoor
      19.05.2015 23:52

      Жуть. Никогда не используйте Application.ProcessMessages; А если очень надо, то хорошо подумайте, и не используйте.
      Ваш точно код ломает:
      1. Завершение приложения по WM_QUIT
      2. Работу Application.OnIdle
      3. Синхронизацию через TThread.Synchronize
      и бог знает что еще ломает. Не надо так делать.


      1. Blackmorsha
        20.05.2015 05:03

        image

        Скриншот моего примера по теме, ссылка для скачать есть на 2 поста выше.


  1. Blackmorsha
    20.05.2015 04:53

    MrShoor
    Вы мою демку не проверяли в работе?
    Мне тоже, как видимо и автору статьи понадобилось недавно быстренько шейдеры потестить (из за одной «вкусной» статьи на Хабре), тут и возникла идея не бегать от VCL при работе с графикой, все-таки готовые контролы, и такой пример быстрее написать. Поэтому тему заявленную автором я признаю, реально такое нужно бывает.
    Отвечаю по пунктам:
    1. WM_QUIT в моём примере обрабатывается корректно, это проверяемо — сообщение WM_QUIT проходит при закрытии окна «крестиком», выключении PC; также сообщение можно послать из другой программы; все 3 случая — 100% правильно отрабатываются, вообще-то это следует из текста примера;
    2. Application.OnIdle происходит своим чередом. В своём примере я его не использую (а зачем?) и пишу о том, что это OnIdle «главного цикла» VCL, а у нашей графики свой «главный цикл», который впрочем не мешает VCL, что и требуется по условиям задачи автора. Могу на OnIdle повесить что нибудь, например, проверку, не изменились ли текстовые файлы с шейдерами. Это будет работать.
    3. У автора шла речь об одном потоке и простом примере, я тоже не стала преумножать число потоков приложения. Мой способ сводится к тому, как в одном потоке приложения сделать 2 или более «независимых циклов», конечно эти циклы не есть отдельные потоки, но они могут работать параллельно без семафоров. Синхронизацию через TThread.Synchronize мой пример никак не нарушает, проверено — я использую такой способ в своём многопоточном сервере, все ок.
    Ничего мой пример не ломает. Это просто цикл который крутится сам по себе, у него нет магических свойств. Это обычный типовой приём из области практического программирования, не я его придумала.
    Application.ProcessMessages использовать можно, об этом гласит Help и офф. примеры.

    Постарайтесь быть доброжелательнее, во имя Хабра. Хотя бы потому, что Вам будет крайне сложно доказать неработоспособность рабочего примера. В примере-то всего 5 строчек и все работают правильно. Я бы никогда не взялась такое оспаривать.
    Мир?


    1. MrShoor
      20.05.2015 07:04

      Вы мою демку не проверяли в работе?
      Я смотрю на код, который вы привели. Я знаю как работает Application.ProcessMessages и знаю как работает Application.Run. Этого достаточно.

      1. WM_QUIT в моём примере обрабатывается корректно, это проверяемо — сообщение WM_QUIT проходит при закрытии окна «крестиком», выключении PC; также сообщение можно послать из другой программы; все 3 случая — 100% правильно отрабатываются, вообще-то это следует из текста примера;

      WM_QUIT не обрабатывается. Вы в этом легко можете убедиться кинув на форму кнопку, и вызвав PostQuitMessage(0); в обработчкие OnClick.

      2. Application.OnIdle происходит своим чередом. В своём примере я его не использую (а зачем?) и пишу о том, что это OnIdle «главного цикла» VCL, а у нашей графики свой «главный цикл», который впрочем не мешает VCL, что и требуется по условиям задачи автора. Могу на OnIdle повесить что нибудь, например, проверку, не изменились ли текстовые файлы с шейдерами. Это будет работать.
      Это не будет работать. Киньте на форму ApplicationEvents и убедитесь самостоятельно.
      3. У автора шла речь об одном потоке и простом примере, я тоже не стала преумножать число потоков приложения. Мой способ сводится к тому, как в одном потоке приложения сделать 2 или более «независимых циклов», конечно эти циклы не есть отдельные потоки, но они могут работать параллельно без семафоров. Синхронизацию через TThread.Synchronize мой пример никак не нарушает, проверено — я использую такой способ в своём многопоточном сервере, все ок.
      Любой сторонний компонент/библиотека, использующий TThread зависнет при вызове Synchronize. Например компоненты Indy использующие серверные сокеты. Вы можете даже не знать, что эти компоненты создают потоки. Просто компоненты перестанут работать, втихую. А все благодаря вашему «циклу».
      Ничего мой пример не ломает. Это просто цикл который крутится сам по себе, у него нет магических свойств. Это обычный типовой приём из области практического программирования, не я его придумала.
      Вы просто не знаете устройство VCL. VCL под капотом выполняет далеко не
      repeat
      Application.ProcessMessages;
      until IsFinish;
      и когда вы крутите цикл так, то код, который должен был выполнятся в Application.Run — перестает выполнятся.
      Application.ProcessMessages использовать можно, об этом гласит Help и офф. примеры.
      Само собой, если функция в паблике — то использовать можно, но не нужно, потому что это очень плохой тон. Даже так: ОЧЕНЬ плохой тон. Вы пока еще новичек в мире программирования, поэтому просто поверьте моему опыту, хорошо?
      Постарайтесь быть доброжелательнее, во имя Хабра. Хотя бы потому, что Вам будет крайне сложно доказать неработоспособность рабочего примера. В примере-то всего 5 строчек и все работают правильно. Я бы никогда не взялась такое оспаривать.
      Мир?
      Я с вами не воевал. Я просто против, когда учат плохому. Вы можете убедится в том, что ваши 5 строчек ломают чужой код. Как это сделать — я написал. Между прочим автор текущей статьи сделал все как раз таки правильно, и ваш рендер стоило бы переделать именно так.


  1. Blackmorsha
    20.05.2015 11:55

    Кинула на форму кнопку, вызвала в её обработчике OnClick функцию Winapi.Window.PostQuitMessage(0), по нажатию кнопки приложение закрывается. На всякий случай привожу текст из Vcl.Forms.pas: TApplication.ProcessMessages с обработкой события WM_QUIT, TApplication.Run с завершением цикла по флагу Terminated, TApplication.HandleMessage с вызовом OnIdle.

    Тест
    function TApplication.ProcessMessage(var Msg: TMsg): Boolean;
    var
    Handled: Boolean;
    Unicode: Boolean;
    MsgExists: Boolean;
    begin
    Result := False;
    if PeekMessage(Msg, 0, 0, 0, PM_NOREMOVE) then
    begin
    Unicode := (Msg.hwnd = 0) or IsWindowUnicode(Msg.hwnd);
    if Unicode then
    MsgExists := PeekMessageW(Msg, 0, 0, 0, PM_REMOVE)
    else
    MsgExists := PeekMessageA(Msg, 0, 0, 0, PM_REMOVE);

    if MsgExists then
    begin
    Result := True;
    if Msg.Message <> WM_QUIT then
    begin
    Handled := False;
    if Assigned(FOnMessage) then FOnMessage(Msg, Handled);
    if not IsPreProcessMessage(Msg) and not IsHintMsg(Msg) and
    not Handled and not IsMDIMsg(Msg) and
    not IsKeyMsg(Msg) and not IsDlgMsg(Msg) then
    begin
    TranslateMessage(Msg);
    if Unicode then
    DispatchMessageW(Msg)
    else
    DispatchMessageA(Msg);
    end;
    end
    else
    begin
    {$IF DEFINED(CLR)}
    if Assigned(FOnShutDown) then FOnShutDown(self);
    DoneApplication;
    {$IFEND}
    FTerminate := True;
    end;
    end;
    end;
    end;

    procedure TApplication.ProcessMessages;
    var
    Msg: TMsg;
    begin
    while ProcessMessage(Msg) do {loop};
    end;

    procedure TApplication.Run;
    begin
    FRunning := True;
    try
    {$IF NOT DEFINED(CLR)}
    AddExitProc(DoneApplication);
    {$IFEND}
    if FMainForm <> nil then
    begin
    case CmdShow of
    SW_SHOWMINNOACTIVE:
    begin
    FInitialMainFormState := wsMinimized;
    FMainForm.FWindowState := wsMinimized;
    end;
    SW_SHOWMAXIMIZED: MainForm.WindowState := wsMaximized;
    end;
    if FShowMainForm then
    if (FMainForm.FWindowState = wsMinimized) or (FInitialMainFormState = wsMinimized) then
    begin
    Minimize;
    if (FInitialMainFormState = wsMinimized) then
    FMainForm.Show;
    end else
    FMainForm.Visible := True;
    repeat
    try
    HandleMessage;
    except
    HandleException(Self);
    end;
    until Terminated;
    end;
    finally
    FRunning := False;
    end;
    end;

    procedure TApplication.HandleMessage;
    var
    Msg: TMsg;
    begin
    if not ProcessMessage(Msg) then Idle(Msg);
    end;


    1. MrShoor
      20.05.2015 19:02

      Напористая вы. Я даже скачал ваш пример, чтобы убедится. Выводы все те же, все поломано.

      Кинула на форму кнопку, вызвала в её обработчике OnClick функцию Winapi.Window.PostQuitMessage(0), по нажатию кнопки приложение закрывается.

      Я кинул в вашем примере на кнопку с обработчиком:
      procedure TForm1.Button1Click(Sender: TObject);
      begin
        PostQuitMessage(0);
      end;
      
      Приложение не завершается. Но это не удивительно. PostQuitMessage отправляет только WM_QUIT. TApplication.ProcessMessage конечно же обрабатывает WM_QUIT, но он только выставляет флаг FTerminate := True;, который нужен для выхода из главного оконного цикла Application.Run, который вы привели. А кто выставит ваш флаг BClose, чтобы код вышел из вашего цикла? Никто. Это прекрасно видно по коду, который вы привели.
      Вывод: ваш цикл ломает обработку WM_QUIT

      Теперь смотрим на TApplication.HandleMessage. Что он делает? Вызывает Idle если ProcessMessage вернул False. ProcessMessage это не ProcessMessages, не путать. А теперь посмотрим на ProcessMessages который вы так же привели:
      procedure TApplication.ProcessMessages;
      var
        Msg: TMsg;
      begin
        while ProcessMessage(Msg) do
          { loop } ;
      end;
      
      Кто вызывает Idle? Никто. Итак у нас поломаный Idle. Я на всякий случай лично проверил это вот этим обработчиком добавив его в вашем примере:
      procedure TForm1.ApplicationEvents1Idle(Sender: TObject;
        var Done: Boolean);
      begin
        AllocConsole;
        WriteLn(IntToStr(Random(1000)));
        Done := False;
      end;
      


      Давайте посмотрим что у нас происходит в Idle:
      procedure TApplication.Idle(const Msg: TMsg);
      var
        Control: TControl;
        Done: Boolean;
      begin
        Control := DoMouseIdle;
        if FShowHint and (FMouseControl = nil) then
          CancelHint;
        Application.Hint := GetLongHint(GetHint(Control));
        Done := True;
        try
          if Assigned(FOnIdle) then FOnIdle(Self, Done);
          if Done then
            if FActionUpdateDelay <= 0 then
              DoActionIdle
            else
              if IdleTimerHandle = 0 then
              begin
                // Constantly assigning to the IdleTimerDelegate causes a
                // memory allocation, and alot of TFNTimerProc's appear in Gen0 because of this.
                // Only assign the delgate once; that is all that is needed.
                if not Assigned(IdleTimerDelegate) then
                  IdleTimerDelegate := @IdleTimerProc;
                IdleTimerHandle := SetTimer(0, 0, FActionUpdateDelay, IdleTimerDelegate);
                if IdleTimerHandle = 0 then
                  DoActionIdle
              end;
        except
          HandleException(Self);
        end;
      
        if (GetCurrentThreadID = MainThreadID) and CheckSynchronize then
          Done := False;
        if Done then WaitMessage;
      end;
      
      У нас дополнительно отваливается Application.Hint, но это ерунда. Главное, что теперь у нас CheckSynchronize не вызывается. А CheckSynchronize — это обработчик всех TThread.Synchronize.

      Выводы: при использовании графики (DX или OpenGL) в VCL приложениях не обязательно привязывать рендер к событиям VCL-компонентов, вы можете по прежнему использовать свой «главный цикл», также как в WinAPI программах. Только не забывайте «дать подышать» VCL при помощи Application.ProcessMessages, он все таки занимается обработкой сообщений.
      Выводы: автор комментария выше ничего не проверял, или проверял не так. Код по прежнему поломан, что хорошо видно из исходников VCL, которые были приведены, а так же легко проверяется в примере автора.


  1. Blackmorsha
    20.05.2015 20:18

    Я бы сказала так:
    Вариант 1 (традиционный). Для работы графики делаем WinAPI приложение, обрабатываем сообщения самостоятельно. Текста там много, но примеры есть, это работает у всех.
    Вариант 2 (рассматриваем). Создать в VCL приложении OpenGL (или DirectX) контекст на форме. Это можно, но придется внимательно прописать все нюансы. Контексты GDI и OpenGL разные, рисованием занимаются разные драйвера. Задачка решаемая, например GLScene так и делает, реализация всех нюансов помещена в компонент. Автор топика (кстати где он?) пошел по этому пути, это нормально. Мы помним о том, что обработкой сообщений занимается VCL и не мешаем ему, используем его обработчики событий.
    Вариант 3 (мой+Ваш). Вы скачали демку. Пробовали нажать на крестик? Окно закроется. Реакция на клавиатуру и мышь также в норме. Для дальнейших успехов, в тот момент когда вы посылаете сообщение WM_QUIT вы должны дополнительно: либо вызвать Form1.Close, либо напрямую сбросить мой флаг. Напоминаю, в моём примере 2 главных цикла, мой и VCL, оба крутятся в одном потоке, для корректного выхода из обоих циклов необходимо «уважить» каждый. Раз уж моя демка с VCL, я работаю через VCL и все в норме. Если мне потребуется самостоятельная посылка/обработка сообщений, то я не должна забывать о втором цикле и вовремя обеспечивать его информацией.