пикселей. Windows пытается вывести курсор мыши, но поскольку режимы Mode X не поддерживаются Windows GDI, изображение курсора портится. Кроме того, Windows не умеет выводить курсор мыши на вторичных видеоустройствах (например, на видеокартах с чипами 3Dfx). Вывод продолжает поступать на первичное видеоустройство независимо от того, какое устройство активно в данный момент, поэтому курсор мыши пропадает.
Значит, если полноэкранное приложение захочет вывести курсор мыши, ему придется делать это самостоятельно. На первый взгляд все просто: нужно создать небольшую поверхность с изображением курсора и перемещать ее в соответствии с вводом от мыши. Такое решение работает, но у него есть свои недостатки.
Раз курсор мыши отображается самим приложением, а не Windows, частота его прорисовки зависит от быстродействия приложения. Если приложение постоянно работает на 75 FPS, это будет приемлемо, но что если частота вывода кадров упадет до 30 FPS и ниже? С падением частоты курсор будет все медленнее реагировать на действия пользователя.
Существует множество причин, по которым приложение может иметь низкую частоту вывода кадров. Трехмерные приложения предъявляют особенно жесткие требования к системе и часто работают с частотой 30 FPS и менее. Но «тормозить» могут и обычные, не трехмерные приложения. Сложное приложение, выводящее сотни объектов, будет иметь низкий FPS (конечно, все зависит от компьютера и видеокарты). Кроме того, режимы High и True Color часто оказываются намного медленнее 8-битных режимов.
Более того, простому приложению DirectDraw вряд ли потребуется курсор мыши. Если возникла необходимость в курсоре, значит, пользователь должен выделять определенные области экрана. Сомнительно, чтобы сложное приложение смогло обеспечить высокую частоту вывода, пока пользователь работает с мышью.
Итак, нам придется искать нетривиальное решение. Оно должно обеспечивать быструю реакцию на действия с мышью независимо от FPS, а курсор не должен мерцать. Поскольку нам все равно придется создавать собственный курсор, для него можно выбрать произвольный размер.
Эта глава посвящена решению, которое удовлетворяет всем перечисленным критериям. Сначала мы обсудим частичное обновление экрана (прямой блиттинг на первичную поверхность), а затем поговорим о том, как многопоточность обеспечивает обновление курсора, не зависящее от частоты вывода. Наконец, теория воплотится на практике в виде программы Cursor.
Частичное обновление экрана
Типичное приложение DirectDraw (наподобие тех, что рассматривались в предыдущих главах) заранее строит весь кадр во вторичном буфере и затем переключает страницы. Эта методика работает быстро (переключение страниц обычно происходит почти мгновенно) и не вызывает мерцания (построение каждого кадра завершается до его вывода).
Чтобы обновление курсора не зависело от частоты вывода, нам придется обновлять курсор так, чтобы обойтись без переключения страниц. Поэтому вместо того, чтобы обновлять весь экран, мы будем перерисовывать лишь его часть. Для этого можно непосредственно изменить содержимое первичной поверхности.
Хотя в нашем случае частичное обновление экрана используется для вывода курсора мыши, эта методика полезна и в других случаях. Например, приложение может обновить меню прямо на первичной поверхности вместо того, чтобы заново строить весь кадр (вместе с изменившимся меню) на вторичном буфере и затем переключать страницы.
С другой стороны, прямой вывод на первичную поверхность имеет свои недостатки и требует осторожности. Основная потенциальная проблема — расхождение. Переключение страниц выполняется так, чтобы предотвратить возможность расхождения. Следовательно, если вы обходите механизм переключения страниц и обновляете кадр, который в данный момент отображается на экране, то рискуете изменить область экрана, в данный момент обновляемую монитором. Если это произойдет, новое содержимое обновляемой части в течение некоторого времени будет выводиться одновременно со старым — возникнет расхождение.
Для борьбы с расхождением есть два пути. Во-первых, можно обновлять лишь малую область экрана — это сокращает вероятность расхождения. Во- вторых, обновление можно синхронизировать с циклом вертикальной развертки. Если подождать с обновлением экрана до завершения очередного переключения страниц, вероятность расхождения становится еще меньше.
Итак, курсор мыши должен обновляться прямо на первичной поверхности. При очередном перемещении курсора необходимо выполнить два действия:
1. Стереть курсор в старом месте.
2. Нарисовать курсор в новом месте.
Первую задачу можно решить, восстанавливая ранее сохраненную часть первичной поверхности. Затем мы рисуем курсор мыши на первичной поверхности в новом месте. Тем не менее для восстановления изображения придется добавить дополнительный шаг — перед выводом курсора сохранить часть первичной поверхности, которую он займет. В результате получается следующий алгоритм:
1. Восстановить фоновое изображение в старом месте.
2. Сохранить часть изображения в новом месте.
3. Нарисовать курсор в новом месте.
Эти три шага позволяют переместить курсор без переключения страниц, сохранив при этом содержимое первичной поверхности.
И все же такой подход связан с некоторыми ограничениями. Он хорошо работает, если старая область курсора не накладывается на новую. Но если области перекрываются, курсор мерцает, потому что стирание происходит поблизости от места рисования. Чтобы полностью избавиться от мерцания, мы должны одновременно обновлять старую и новую области расположения курсора, а описанный выше алгоритм можно использовать для неперекрывающихся областей курсора.
Прежде чем продолжать, я хотел бы заметить, что чаще встречаются именно перекрывающиеся области. Старая и новая области курсора перекрываются при любом медленном перемещении мыши, а мышь обычно перемещается быстро лишь из одного края экрана в другой. Во всех остальных случаях при выборе конкретного участка экрана курсор перемещается медленно. Следовательно, борьба с мерцанием становится очень важной задачей.
Чтобы справиться с мерцанием, можно обновлять изображение на внеэкранной поверхности. Мы копируем в нее обе области курсора (старую и новую), обновляем изображение, а затем копируем обе области обратно на первичную поверхность как единое целое. Алгоритм состоит из пяти этапов:
1. Скопировать объединение старой и новой областей курсора на вспомогательную поверхность.
2. Стереть старый курсор на вспомогательной поверхности.
3. Сохранить фоновое изображение, занятое новой областью курсора.
4. Нарисовать новый курсор на вспомогательной поверхности.
5. Скопировать содержимое вспомогательной поверхности на первичную поверхность.
Используя оба алгоритма (из трех и пяти этапов), мы всегда сможем обновить курсор без мерцания и разрушения основного изображения.
Для реализации двух алгоритмов потребуются три внеэкранные поверхности: поверхность с курсором, поверхность для хранения фонового изображения и вспомогательный буфер для перекрывающихся курсорных областей. Размеры первой и второй поверхностей совпадают с размерами курсора. Однако вспомогательный буфер должен быть вдвое выше и вдвое шире поверхности курсора, чтобы в нем могли разместиться области при минимальном перекрытии (на самом деле при таком размере буфер получается на один пиксель выше и шире, чем необходимо, но это непринципиально).
До сих пор мы рассматривали обновление курсора мыши без переключения страниц, но ведь приложение должно переключать страницы для обновления экрана. Что же произойдет с нашим тщательно подготовленным курсором после переключения? Он исчезнет. Мы можем нарисовать его заново, но это вызовет мерцание.
Логичнее будет выводить курсор на вторичном буфере после подготовки очередного кадра. Такое решение оказывается удачным, потому что содержимое фонового буфера все равно приходится обновлять перед переключением страниц (в противном случае при следующем обновлении курсора будет восстановлена устаревшая область первичной поверхности). Алгоритм выглядит так:
1. Построить новый кадр во вторичном буфере.
2. Сохранить область вторичного буфера, где должен находиться курсор.
3. Нарисовать курсор на вторичном буфере.
4. Выполнить переключение страниц.
Теперь курсор можно обновлять при переключении страниц или без него, причем не вызывая мерцания. Однако мы лишь подходим к решению проблемы — нужно придумать, как запрограммировать это решение.
Многопоточность
Когда все внимание сосредоточено на курсоре мыши, нетрудно забыть, что курсор — всего лишь часть нашего приложения. После появления курсора приложение не должно принципиально отличаться от рассмотренных выше, так что было бы нежелательно вставлять код ввода от мыши и обновления курсора в середину приложения. И даже если согласиться на это, как будет выглядеть этот код? Он должен постоянно проверять наличие новых данных от мыши. При обнаружении данных он обновляет курсор мыши; в противном случае продолжает свою нормальную работу. Постоянный опрос мыши замедлит приложение и усложнит его структуру. Более удачное решение — разделить приложение на две подзадачи, использовав многопоточность.
Если вы уже знакомы с концепцией многопоточности, этот раздел вам не понадобится. Однако для новичков в нем рассматриваются основные положения, которые необходимо усвоить перед тем, как переходить к программированию. Ни в коем случае не следует рассматривать его как исчерпывающее