По сути, основная цель рендеринга состоит в том, чтобы каждый кадр, отображаемый на мониторе, представлял одно связное изображение. Существует несколько различных стратегий, которые использовались или использовались для достижения этой цели.
Далее я упоминаю «vsync». Vsync - это момент, когда монитор начинает рисовать новое изображение на экране; это точка, в которой «vblank» начинается на традиционном экране ЭЛТ, где линия сканирования на мгновение прекращает рисовать и возвращается к верхней части монитора. Этот момент очень важен для многих подходов к согласованности кадров.
«Разрыв» - это то, что мы называем этим, когда экран визуализируется из двух разных изображений в одном кадре. Если, например, я нарисовал два изображения экрана, которые должны отображаться одно за другим, но монитор вместо этого отобразил верхнюю половину первого кадра и нижнюю половину второго кадра, это «разрыв». Это происходит из-за изменения данных, которые считывает монитор во время рисования, а не во время vblank. (В современных программах это обычно происходит потому, что пользователь отключил ожидание vsync в своих настройках драйвера)
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).
Как правило, современные драйверы часто выполняют тройную буферизацию прозрачно, за кадром. Вы пишете свой код для двойной буферизации, и драйвер фактически рано возвращает вам управление, и просто выполняет внутреннюю обработку переключения между любым количеством обратных буферов, которые он хочет использовать, даже если ваш код никогда не узнает об этом.
В настоящее время производители графических процессоров рекомендуют вам не реализовывать тройную буферизацию самостоятельно - драйвер сделает это автоматически.