что такое двойная буферизация в играх
Какую проблему решает двойная или тройная буферизация в современных играх?
Я хочу проверить, правильно ли я понимаю причины использования двойной (или тройной) буферизации:
Монитор с частотой обновления 60 Гц отображает монитор 60 раз в секунду. Если монитор обновляет монитор-дисплей, он обновляет пиксель для пикселя и строку для строки. Монитор запрашивает значения цвета для пикселей из видеопамяти.
Если я сейчас запускаю игру, то эта игра постоянно манипулирует этой видеопамятью.
Если в этой игре не используется буферная стратегия (двойная буферизация и т. Д.), Может возникнуть следующая проблема:
Верно ли мое понимание случаев использования буферной стратегии? Есть ли другие причины?
По сути, основная цель рендеринга состоит в том, чтобы каждый кадр, отображаемый на мониторе, представлял одно связное изображение. Существует несколько различных стратегий, которые использовались или использовались для достижения этой цели.
Zero-Buffer
На самом старом оборудовании часто не хватало памяти для размещения полноэкранного изображения, и поэтому вместо рисования изображения на экране вам нужно было указывать цвета для каждой линии сканирования отдельно, пока монитор находился в процессе рисования этой линии. Например, на Atari 2600 у вас было всего 76 циклов машинных инструкций, чтобы указать, какой цвет вошел в каждый пиксель линии развертки, прежде чем телевизор начал фактически рисовать эту линию развертки. И затем у вас было 76 циклов инструкций для предоставления содержимого для следующей строки сканирования и так далее.
Single-Buffer
Рисуя в контексте «одного буфера», вы рисуете прямо в VRAM, которая читается монитором. При таком подходе вы «гоняете по сканлайну». Общая идея заключается в том, что когда сканлайн начинает рисовать содержимое предыдущего кадра в верхней части экрана, вы рисуете в VRAM за ним. Таким образом, пока линия сканирования рисует изображение экрана для последнего кадра, вы рисуете следующий кадр за линией сканирования.
В общем, вы пытаетесь закончить рисование изображения следующего кадра до того, как отсканирует вас линия сканирования, снова обойдя и обогнав пиксели, которые вы рисуете, а также чтобы никогда не опередить линию развертки или ваш новый кадр может нарисовать то, что должно было быть в предыдущем кадре.
По этой причине рендеринг в одном буфере, как правило, выполнялся путем рисования линий развертки сверху вниз и слева направо. Если вы нарисовали в каком-то другом порядке, вполне вероятно, что линия сканирования снова обернется и обнаружит фрагменты «следующего» изображения, которое вы еще не успели нарисовать.
Дважды Buffer
Это намного, намного проще, чем любая из ранее разработанных стратегий.
После того, как мы закончили рисовать изображение экрана в заднем буфере, мы ждем до vsync, а затем поменяем местами два буфера. Таким образом, задний буфер становится передним буфером, и наоборот, и весь обмен произошел, когда монитор ничего не рисовал.
Triple-буфер
Одна проблема, часто возникающая при подходах с двойным буфером, заключается в том, что после того, как мы завершим рисование в задний буфер, нам нужно просто сидеть и ждать vsync, прежде чем мы сможем поменять местами буферы и продолжить работу; мы могли бы делать вычисления в течение этого времени! Более того, все время, пока мы ожидаем переключения между буферами, изображение в этом заднем буфере стареет и стареет, что увеличивает задержку, воспринимаемую пользователем.
Монитор отображает передний буфер, и мы рисуем в задний буфер # 1. Если мы закончим рисование в заднем буфере # 1 до того, как монитор завершит рисование переднего буфера, то вместо ожидания vsync мы немедленно начнем рисовать следующий кадр в задний буфер # 2. Если мы закончим, а vsync все еще не пришел, мы начнем рисовать обратно в задний буфер # 1 и так далее. Идея состоит в том, что когда vsync в конечном итоге произойдет, один или другой из наших обратных буферов будет завершен, и этот можно заменить на передний буфер.
Преимущество тройной буферизации состоит в том, что мы не теряем время, потраченное на ожидание vsync в подходе с двойной буферизацией, и изображение, помещенное в передний буфер, может быть «свежее», чем то, которое ожидало vsync для 8ms. Недостатком тройной буферизации является то, что нам нужна дополнительная память для хранения дополнительного изображения экрана, и что наше использование процессора / графического процессора будет выше (опять же, поскольку мы не замедляем ожидание vsync).
Как правило, современные драйверы часто выполняют тройную буферизацию прозрачно, за кадром. Вы пишете свой код для двойной буферизации, и драйвер фактически рано возвращает вам управление, и просто выполняет внутреннюю обработку переключения между любым количеством обратных буферов, которые он хочет использовать, даже если ваш код никогда не узнает об этом.
Для чего в настройках игр есть такая настройка тройная буферизация и вертикальная синхронизация? Вообще она нужна?
Вертика́льная синхрониза́ция (англ. V-Sync) — синхронизация кадровой частоты в компьютерной игре с частотой вертикальной развёртки монитора. При этом максимальный FPS с вертикальной синхронизанией приравнивается к частоте обновления монитора. Если FPS ниже частоты обновления монитора, то во избежание ещё большей потери производительности следует включить тройную буферизацию.
Тройная буферизация в компьютерной графике — разновидность двойной буферизации; метод вывода изображения, позволяющий избежать или уменьшить количество артефактов.
Другой метод тройной буферизации включает в себя синхронизацию с частотой обновления экрана, используя третий буфер просто как способ предоставить свободное пространство для запросов на изменения в общем объёме выводимой графики. Здесь буфер используется в истинном смысле, когда он действует как хранилище. Такой метод предъявляет повышенные минимальные требования к аппаратному обеспечению, но обеспечивает согласованную (по сравнению с переменной) частоту кадров.
Тройная буферизация предполагает использование трёх буферов, но метод может быть расширен на любое количество буферов, нужное приложению. Обычно использование четырёх и более буферов не даёт каких-либо преимуществ.
Недостатки двойной буферизации
Если в системе есть три буфера: А, Б и В, ей не нужно ждать смены буферов. Она может отображать буфер Б, формируя изображение в буфере А. Когда изображение в буфере А готово, она немедленно начинает построение изображения в буфере В. При наступлении паузы в вертикальной развёртке отображается буфер А, а буфер Б освобождается для повторного использования.
Ограничения тройной буферизации
Если система всегда заполняет буферы за меньшее время, чем требуется для отображения буфера на экране, компьютер будет всегда ожидать сигнала монитора независимо от количества буферов. В этом случае тройная буферизация не имеет преимуществ перед двойной буферизацией.
В рамках данного обзора, я расскажу вам что такое тройная буферизация, а так же про связанные с этим особенности.
Суть проблемы. При формировании изображения, оптимальным считается, что вначале вся область заменяется фоном (например, белым цветом или некой картинкой), а уже затем на нее наносятся отдельные фрагменты. Если же используется один буфер, с которого считывает и в который записываются данные, то вполне возможно возникновение таких проблем, как мерцание экрана или его отдельных элементов, появление разрывов (верхняя часть картинки из текущей, нижняя часть из старой) и прочих дефектов.
Одним из решений подобной проблемы, является двойная и тройная буферизация. Что это такое и зачем нужно, а так же как связано с вертикальной синхронизацией V-Sync, рассмотрим далее.
Тройная и двойная буферизация
Двойная буферизация
Как устроена двойная буферизация в компьютерной графике? Стоит отметить, что существует два варианта, оба из которых решают проблему мерцания и некоторых иных дефектов, но не решают проблему разрыва картинки. Первый, это когда изображение вначале формируется в оперативной памяти компьютера, а затем копируется в буфер монитора (из которого последний считывает и отображает картинку на экране). Второй, это когда видеокарта исходно поддерживает два буфера, которые она меняет без копирования данных, что существенно быстрее. В этом случаем, реже возникают разрывы.
Стоит знать, что под первичным буфером подразумевают тот, в котором хранится картинка, отображаемая в экране монитора. Под вторичным буфером подразумевается тот, в котором генерируется изображение (происходит рендер).
Тройная буферизация
Зачем это нужно? Дело в том, что в момент копирования данных видеокарта простаивает. Соответственно, дополнительный вторичный буфер решает эту проблему, так как в момент копирования данных, может формироваться следующее изображение. Это позволяет повысить fps.
Однако, обе этих технологии обычно связывают с V-Sync и не просто так. Далее рассмотрим почему.
Двойная и тройная буферизация с вертикальной синхронизацией
Вертикальная синхронизация V-Sync применяется совместно с двойной или тройной буферизацией и позволяет решать проблему разрывов изображений. Отличием от обычного применения является лишь то, что копирование данных синхронизировано с частотой монитора. Простыми словами, в моменты, когда монитор считывает и отображает данные, смены картинки не происходит.
Примечание: Читателям стоит знать, что V-Sync повышает Input Lag.
В чем плюсы и минусы двойной буферизации с V-Sync?
Плюсы. На экране не видны разрывы. Если видеокарта мощная и fps у нее выше частоты монитора, то снижение fps может не чувствоваться, так как каждый кадр анимации будет срендерен (сгенерирован) до момента отображения на экране монитора с учетом задержки копирования.
Минусы. Суть в том, что, кроме проблемы простоя видеокарты при копировании данных, добавляется задержка ожидания отрисовки монитором. Это означает, что может очень сильно снижаться fps, если видеокарта генерирует меньшее число кадров, чем частота монитора. Например, 40-45 fps могут снизиться до 30 реальных fps, так как часть кадров будет отображаться за 1 такт монитора, а часть кадров за 2 такта монитора. Если же fps меньше 30, то снижение может быть вплоть до 15 кадров.
В чем плюсы и минусы тройной буферизации с V-Sync?
Плюсы. Те же, что и у двойной, но с некоторым отличием. Дело в том, что тройная буферизация позволяет избавиться от проблемы простоя, так как в моменты ожидания монитора или копирования данных, видеокарта формирует следующее изображение, что особенно полезно, если видеокарта формирует изображения то быстро, то медленно (однако, возможен минус в виде периодических пропусков изображений из-за V-Sync).
Минусы. Первый минус в том, что тройная буферизация требует больше вычислительных ресурсов. Второй минус в том, что если видеокарта всегда генерирует картинки быстро с учетом всех задержек, то толк от тройной буферизации теряется. Третий. Если компьютер «слабый», то включение этого метода может снизить реальный fps. Происходит это из-за первого минуса, так как требуется больше вычислительных ресурсов. В этом случае, лучше отключить не только тройную буферизацию, но и V-Sync.
Примечание: Третий минус редко встречается, так как нынешние «слабые» компьютеры достаточно мощные для этого метода.
Теперь, вы знаете что такое тройная буферизация, зачем она нужна и некоторые ее особенности.
Двойная буферизация или Назад в прошлое. Часть вторая
Доброго времени суток!
Введение
На дворе уже четвёртое января, а моя душа всё не успокаивается. Поэтому я решил продолжить тему написания J2ME приложений. Плюс ко всему, несколько человек проявили нешуточный интерес к данной теме. Причём это были не только рядовые пользователи хабра, но и read-only аккаунты. Ну да ладно, ближе к теме.
Буквально сразу же после публикации топика, были получены очень дельные комментарии от хабраюзера barker, а именно замечание, по сути являющееся прописной истиной и второй комментарий — поправка, не менее дельная.
О чём мы поговорим сегодня
Сегодня мы с вами поговорим о самом процессе двойной буферизации в javax.microedition.lcdui.Canvas и о том, почему же был создан javax.microedition.lcdui.game.GameCanvas.
Что такое «двойная буферизация»
Двойная буферизация является ничем иным, как техникой, которой предусматривается использование второго (внеэкранного) буфера для отрисовки фигур, спрайтов и так далее в него, с последующим копированием его содержания в экранный. Проблема в том, что при рисовании напрямую, т.е. рисование непосредственно в экранный буфер по времени не укладывается в промежуток времени перерисовки экрана (в Canvas это осуществляется функцией repaint()) и экран попросту начинает «мигать», т.е. пользователь видит перед собой промежуточный результат этого самого рисования. Использование этой самой технике позволяет разработчику избегать этих «миганий». Тем не менее, в Canvas использование этой техники является процессом велосипедостроения, т.к. разработчики стандарта и платформы J2ME не позаботились об этом.
«Двойная буферизация» в Canvas
Процесс «двойной буферизации» в Canvas проходит с помощью использования изображения (объекта Image пакета javax.microedition.lcdui), в качестве внеэкранного буфера. Вот так:
Вот и всё. Код содержит более чем наглядные комментарии, так что разбор кода не должен вызвать у вас проблем. Теперь рассмотрим «двойную буферизацию» в GameCanvas.
«Двойная буферизация» в GameCanvas
Прошло некоторое время и J2ME консорциумом был разработан пакет javax.microedition.lcdui.game, в котором содержался GameCanvas, который представлял собой всё тот же Canvas, но уже с решённой проблемой «двойной буферизации». Программистам теперь не нужно о ней заботиться. Код же будет выглядеть следующим образом:
Тут нам не нужно заботиться о буфере — всё сразу рисуется в него, а затем при вызове flushGraphics всё содержимое внеэкранного буфера копируется в экранный.
На этом всё
Несмотря на то, что данная задача отрисовки решается в несколько строк — это достаточно важная тема, в которой разработчикам нельзя «плавать». Надеюсь, сегодняшний урок был не менее поучительным, чем прошлый. На этом всё, разрешите откланяться.
Двойная буферизация (Double Buffering)
Задача
Дать возможность ряду последовательных операций выполняться мгновенно или одновременно.
Мотивация
В своем сердце компьютер отсчитывает последовательность ударов. Его мощь заключается в способности разбивать громадные задания на мелкие шаги, которые можно выполнять один за другим. Однако пользователю зачастую нужно видеть как вещи выполняются за один шаг или несколько задач выполняются одновременно.
В потоковой и многоядерной архитектуре это уже не совсем верно, но даже при наличии нескольких ядер, всего только несколько операций могут выполняться в конкурентном режиме.
Типичный пример, встречающийся в любом игровом движке — это рендеринг. Когда игра отрисовывает мир, видимый пользователем, она делает это отдельными кусочками: горы вдали, крутые холмы, деревья, все по очереди. Если пользователь увидит этот процесс отрисовки в таком инкрементном режиме, иллюзия когерентности мира теряется. Сцена должна обновляться плавно и быстро, образуя последовательность законченных кадров, появляющихся мгновенно.
Двойная буферизация решает эту проблему, но чтобы понять как, нам нужно для начала вспомнить как компьютер показывает графику.
Как работает компьютерная графика (коротко)
Видео дисплей, как и компьютерный монитор, рисует пиксель за пикселем. Они обновляются один за другим, слева направо в каждом ряду, а затем происходит переход вниз к следующему ряду. Когда нижний правый угол достигнут, происходит переход к левому верхнему углу и процесс начинается заново. Происходит это так быстро — по крайней мере 60 раз в секунду, что наш глаз этого пробегания по рядам не замечает. Для нас все выглядит, так как будто сменяются статичные картинки на весь экран.
Можно думать об этом как о крошечном шланге, из которого мы поливаем экран пикселями. Отдельные цвета подводятся к этому шлангу и распыляются на экран по одному биту цвета за раз. Но каким образом этот шланг знает куда какой цвет направлять?
Такое объяснение конечно несколько. упрощено. Если вы хорошо разбираетесь в работе железа — можете спокойно пропустить следующий раздел. У вас уже есть все необходимые знания для понимания оставшейся части главы. А если вам это незнакомо, я дам вам необходимый минимум знания для понимания шаблона.
В большинстве компьютеров ответ кроется в применение буфера кадра (framebuffer). Буфер кадра — это массив пикселей в памяти, участок RAM, где каждые несколько байтов представляют собой отдельный пиксель. И в то время когда шланг распыляет пиксели по экрану, он считывает значения цветов из массива по одному байту за раз.
Для того чтобы наша игра появилась на экране, все что мы на самом деле предпринимаем — это просто записываем значения в этот массив. Все самые изощренные графические алгоритмы сводятся к этому: установке значения байтов в буфере кадра. Но есть тут одна проблема.
Как я сказал ранее, компьютеры работают последовательно. Если машина выполняет кусок нашего кода рендеринга, мы не ожидаем что делаем в тот же самый момент что-то еще. В целом это верно, но некоторые вещи все-таки происходят во время выполнения нашей программы. Один из таких процессов — это постоянное считывание информации из буфера кадра. И это уже может быть проблемой.
Соответствие между значениями байтов и цветами описывается форматом пикселей (pixel format) и глубиной цвета (color depth) системы. В большинстве современных игр используется 32 битный цвет: по восемь бит на красный, зеленый и синий канал и еще восемь для специального дополнительного канала.
Предположим что мы хотим отобразить на экране смайлик. Наша программа начинает в цикле двигаться по буферу кадра, окрашивая пиксели. Что мы до сих пор не уяснили — так это то, что видео драйвер производит считывание из буфера кадра в то же самое время, когда мы ведем в него запись. И когда он проходит по записанным пикселям, на экране начинает появляться смайлик. А потом он нас обгоняет и считывает данные из тех пикселей, куда мы еще ничего не записали. Результат паршивый: результатом будет баг, когда картинка отрисовывается только в верхней части экрана.
Мы начинаем отрисовывать пиксели из буфера кадра также как видео драйвер (Рис. 1). Видеодрайвер настигает рендер и попадает туда, куда пиксели еще не записывались (Рис. 2). Дале мы заканчиваем отрисовку (Рис. 3), но драйвер этого уже не видит.
Вот результат, который пользователь увидит на экране (Рис. 4). Название «разрыв» возник потому что нижняя часть картинки как будто оторвана.
Вот здесь нам и пригодится наш шаблон. Наша программа рендерит пиксели по одному за раз, но драйверу нам нужно передавать всю картинку целиком — один кадр без смайлика и один кадр со смайликом. Именно для этого и нужна двойная буферизация. Попробую подобрать понятную аналогию.
Акт первый. Сцена первая
Представьте, что мы смотрим нашу собственную пьесу. Как только заканчивается первая сцена и начинается вторая, нам нужно сменить декорации. Если мы просто начнем перетаскивать реквизит, иллюзия когерентности пространства пропадет. Мы конечно можем просто приглушить свет на этот период (так в театре тоже делают), но аудитория по прежнему будет понимать что что-то происходит. Мы же хотим чтобы между сценами не было провалов.
В реальности мы можем прибегнуть к оригинальному решению: Построим две декорации таким образом, что они обе будут видны публике. У каждой сцены свое освещение. Назовем их A и B. Первая сцена демонстрируется в декорации A. В это время декорация B затемнена и работники сцены готовят ее к показу сцены два. Как только сцена первая завершается, мы выключаем свет в декорации A и включаем его в декорации B. Внимание публики сразу переключается к декорации B, где уже начинается сцена вторая.
С помощью полупрозрачного зеркала и очень маленького макета, можно добиться того чтобы зрители видели обе сцены одновременно в одном и том же месте. Как только освещение поменяется, они будут смотреть уже на другую сцену не меняя при этом направление взгляда. Предлагаю вам провести такой эксперимент самостоятельно.
В это время наши работники сцены занимаются затемненной декорацией A, подготавливая ее для сцены три. Как только сцена два закончится, мы снова переключим свет на декорацию A. Этот процесс будет повторяться на протяжении всей пьесы, используя затемненную декорацию для подготовки следующей сцены. Для перехода между сценами мы просто затемняем одну и освещаем другую. В результате наша публика получает возможность видеть спектакль без задержек между сценами. И никогда не видит работников сцены.
Вернемся к графике
Точно также работает и двойная буферизация и именно такой процесс скрывается за системой рендеринга практически любой современной игры. Вместо единственного буфера кадра у нас есть два. Один из них представляет текущий кадр — аналогию декорации A. Это то место, откуда считывает данные видеодрайвер. GPU может производить из него считывание сколько угодно и когда угодно.
Хочу заметить, что не все игры и консоли пользуются таким методом. Старые и самые простые из консолей были настолько ограничены в плане памяти, что вынуждены были синхронизировать отрисовку с обновлением картинки. Довольно хитрая задача.
В это время наш код рендеринга пишет в другой буфер. Это наша затемненная декорация B. Когда код рендеринга заканчивает отрисовку сцены, мы переключаем свет подменяя (swapping) буфера. Этим самым мы говорим видеобуферу, чтобы он теперь считывал данные из второго буфера вместо первого. И пока мы будем выполнять переключение в конце обновления экрана, никаких разрывов мы не увидим и сцена будет отображаться целиком.
А в это самое время наш старый буфер кадра становится готовым к использованию. Мы начинаем рендерить в него новый кадр. Вуаля!
Шаблон
Класс буфера инкапсулирует буфер — часть состояния, которое можно изменить. Буфер изменяется постепенно, но мы хотим чтобы внешний код увидел изменение как единый атомарный процесс. Чтобы это стало возможным, класс хранит два буфера: следующий и текущий.
Когда требуется считать информацию из буфера — всегда используется текущий. А когда информация записывается — используется следующий буфер. Когда изменения закончены, операция обмена (swap) мгновенно меняет местами следующий и текущий буферы, так что новый буфер становится видным публично. Старый текущий буфер теперь доступен для повторного использования в качестве следующего буфера.
Когда его использовать
Это шаблон из тех, про который вы сами поймете, когда его нужно будет использовать. Если у вас есть система, в которой не хватает двойной буферизации, это обычно заметно (как в случае с разрывом) или приводит к некорректной работе. Но просто сказать «Вы поймете когда он вам пригодится» — недостаточно. Если говорить конкретнее, этот шаблон стоит применять если справедливо одно из следующих утверждений:
У нас есть состояние, изменяющееся постепенно.
К состоянию есть доступ посередине процесса его изменения.
Мы хотим предотвратить код, считывающий состояние от чтения незаконченного изменения.
Мы хотим иметь возможность считывать состояние, не дожидаясь когда оно будет изменено.
Имейте в виду
В отличие от больших архитектурных шаблонов, двойная буферизация существует на низкоуровневом слое реализации. Поэтому последствия на всю кодовую базу в целом не слишком велики — большая часть игры даже не заметит разницы. Но и здесь не обошлось без подводных камней.
Переключение само по себе требует времени
Двойная буферизация требует этапа переключения (swap), как только изменение будет закончено. Само это переключение должно быть атомарным — остальной код не должен иметь доступ во время этой операции ни к одному из состояний. Чаще всего переключение выполняется также быстро как переназначение указателя. А вот если переключение требует больше времени чем собственно изменение состояния, то толку от шаблона не будет никакого.
Нам нужно иметь два буфера
Втрое следствие применения шаблона — увеличение потребления памяти. Как явственно следует из названия нам нужно постоянно держать именно две копии состояния в памяти. На устройствах с ограниченным объемом памяти это довольно дорогая цена за применение шаблона. Если вы не можете позволить себе иметь два буфера, вам стоит присмотреться к другим способам обеспечения недоступности состояния для чтения во время изменения.
Пример кода
Теперь, когда мы разобрались с теорией, давайте перейдем к практике. Мы напишем очень приблизительную графическую систему вывода пикселей в буфер кадра. В большинстве консолей и PC всю эту низкоуровневую графическую работу делает видеодрайвер, однако ручная реализация позволит нам лучше разобраться в происходящем. Для начала сам буфер:
Конкретно этот код рисует вот такой замечательный шедевр:
Выглядит достаточно прямолинейно, но если оставить все как есть, у нас будут серьезные проблемы. Проблема в томб что видеодрайвер может вызвать getPixels() у буфера в любое время, даже здесь:
Когда такое происходит, пользователь увидит глаза на лице, а рот на один кадр пропадет. На следующем кадре отрисовка прервется в какой-либо еще точке. В результате у нас получится ужасно моргающая графика. Исправить это можно добавлением второго буфера.
Не только графикой единой
Суть проблемы, решаемой двойной буферизацией заключается в доступе к состоянию во время его модификации. На это есть две причины. Первую мы упоминали в примере с графикой: код из другого потока или прерывания напрямую получает доступ к состоянию.
Искусственный интеллект
Давайте представим себеб что мы разрабатываем поведенческую систему для игры по мотивам гротескной буффонады (http://ru.wikipedia.org/wiki/Буффонада или http://en.wikipedia.org/wiki/Slapstick). В игре имеется сцена, в которой участвует куча актеров, творящих всякие шутки и трюки. Вот базовый актер:
На каждом кадре игра отвечает за то чтобы вызвать update() актера. Таким образом он может что-либо сделать. С точки зрения игрока критически важно, чтобы обновления всех актеров выглядели одновременными.
Актерам потребуется декорация, в которой они будут взаимодействовать:
Единственное, о чем нужно упомянуть — это то, что каждое состояние «получил пощечину» очищается сразу после обновления. Это сделано для того, чтобы каждый актер реагировал на пощечину только один раз.
Чтобы все заработало, давайте определим конкретный подкласс актера. Наш комедиант довольно прост. Он находится напротив другого актера. Когда он получает пощечину (от кого угодно) — он реагирует на пощечину актера, который находится перед ним.
Теперь запустим в декорацию несколько комедиантов и посмотрим, что получится. Добавим трех комедиантов, каждый из которых смотрит на следующего. Последний смотрит на первого, замыкая получившийся круг:
Получившаяся декорация выглядит следующим образом. Стрелки показывают кто на кого смотрит, а номера обозначают индекс в массиве декорации.
Дадим пощечину Гарри и посмотрим что из этого получится, когда мы запустим обновление.
Помните, что функция декорации update() обновляет актеров по-очереди, так, что если мы проследим, что происходит в коде, мы обнаружим следующее:
Итак, в единственном кадре наша начальная пощечина Гарри прошлась по всем комедиантам. Теперь, чтобы немного разнообразить ситуацию, мы поменяем актеров в массиве декорации местами, но смотреть они будут друг на друга по-прежнему.
Не будем трогать остальную часть декорации, а просто заменим код с добавлением актеров на следующий:
Давайте посмотрим что произойдет, когда мы запустим наш эксперимент снова:
Ух ты! Совсем другое дело. Проблема очевидна. Когда мы обновляем актеров, мы изменяем состояние «получил пощечину», т.е. то же самое состояние, которое мы читаем во время обновления. Из—за этого сделанные вначале процесса обновления изменения начинают влиять на то, что происходит дальше в процессе того же самого шага обновления.
В конце концов получается так, что актеры начинают реагировать на пощечину либо на том же самом кадре, либо на следующем только на основании того, в какой очередности они находятся в декорации. Это нарушает наше главное требование — обеспечение видимости одновременного действия всех актеров: порядок, в котором расположены актеры, не должен влиять на результаты обновления в каждом кадре.
Если вы и дальше продолжите наблюдение за происходящим — вы заметите что пощечины распространяются каскадно, одна пощечина за кадр. На первом кадре Гарри бьет Балди. На следующем Балди бьет Чампа и т.д.
Буферизация пощечин
К счастью, нам может помочь наш шаблон Двойной буферизации. На этот раз, вместо того, чтобы заводить две монолитные копии «буферизуемого» объекта, мы буферизуем гораздо более мелкую сущность — состояние «получил пощечину» у каждого из актеров:
Функция update() теперь обновляет всех актеров и только после этого подменяет все их состояния.
В конце концов, у нас получится такая система, в которой актер видит пощечину только на следующем кадре после того, как она была нанесена. И в этом случае актеры будут действовать одинаково, вне зависимости от порядка нахождения в массиве. Для пользователя или внешнего кода обновление актеров будет выглядеть одновременным.
Архитектурные решения
Двойная буферизация — шаблон очень прямолинейный, поэтому рассмотренные нами примеры покрывают большую часть вариантов использования. Самое главное в реализации шаблона — это принять решение по двум следующим вопросам.
Как мы будем переключать буферы?
Операция переключения — наиболее критичная часть процесса, потому что во время ее выполнения оба буфера блокируются как для чтения так и для записи. Чтобы добиться наилучшей производительности, эта операция должна происходить настолько быстро, насколько это только возможно.
Переключение указателей, ссылающихся на буферы:
Именно так работает пример с графикой и это самое распространенное решение двойной буферизации графики.
Это быстро. Неважно какого размера сам буфер, потому что все переключение представляет собой простое переключение указателей. Сложно придумать что-то более быстрое и более простое.
Внешний код не может хранить постоянный указатель на буфер. Это основное ограничение. Так как мы не переносим непосредственно сами данные, все что мы делаем — это просто указываем кодовой базе искать буфер в новом месте, как в нашем первом примере с декорациями. Это значит, что оставшаяся кодовая база не может хранить указатели на данные внутри буфера, потому что через некоторое время он будет указывать на неправильный адрес.
В системах, где видеобуфер должен находиться в строго определенном месте в памяти, это может быть большой проблемой. В этом случае у нас есть несколько вариантов.
Находящиеся в буфере данные будут отставать на два кадра, а не относиться к последнему кадру. Удачные кадры рисуются в альтернативный буфер без копирования данных между ними следующим образом:
Вы можете заметить, что когда мы отрисовываем третий кадр, данные, уже находящиеся в буфере, относятся к первому кадру, а не к более свежему второму. В большинстве случаев это не проблема — мы просто очищаем весь буфер перед отрисовкой. Но если мы хотим использовать часть данных повторно, важно учитывать то, что данные могут быть на кадр старше, чем мы рассчитываем.
Одно из классических применений старого буфера кадра — это эффект размытия движения. Текущий кадр смешивается с частью ранее отрендеренного кадра таким образом чтобы получилось подобие того, что фиксирует реальная камера.
Копирование данных между буферами:
Если мы не можем перенаправить пользователей на другой буфер, единственным выходом остается полное копирование данных из следующего кадра в текущий. Именно так работают наши драчливые комедианты. В этом случае мы выбрали такой вариант потому, что состояние — это всего лишь булевский флаг и его копирование занимает не больше времени, чем указателя на сам буфер.
Данные в следующем буфере отстают только на один кадр. Это преимущество копирования данных над простым перебрасыванием двух буферов. Если нам понадобится доступ к предыдущим данным из буфера, они будут более свежими.
Обмен может занять больше времени. Это конечно основной недостаток. Наша операция обмена теперь означает копирование в памяти всего буфера. Если буфер достаточно большой, как например весь буфер кадра целиком, на это может потребоваться слишком много времени. Так как во время обмена никто не может ни читать буфер, ни писать в него — это серьезное ограничение.
Какова дробность самого буфера?
Еще один вопрос — это организация самого буфера: является ли он единым монолитным куском данных или разрозненным набором объектов. Наш графический пример — это первое, а пример с актерами — второе.
Чаще всего ответ кроется в природе того, что вы буферизуете, но пространство для маневра все-равно остается. Например, наши актеры могут хранить все свои сообщения в едином блоке сообщений и обращаться к ним по индексу.
Если буфер монолитный:
Если у многих объектов есть свой кусочек данных:
Обмен медленнее. Чтобы его выполнить, нам нужно обойти всю коллекцию объектов и выполнить обмен для каждого.
В нашем примере с комедиантами, это нормально, потому что нам все равно нужно очищать состояние следующей пощечины — каждый кусочек буферизованного состояния нужно трогать на каждом кадре. Если же нам не нужно затрагивать старый буфер, мы можем применить простую оптимизацию, чтобы получить для разделенного на множество кусочков состояния такую же производительность, как и для монолитного буфера.
Идея заключается в том, чтобы взять концепцию «текущего» и «следующего» указателей и применить ее к каждому объекту, превратив их в смещение относительно объекта. Примерно таким образом: