Снова привет, мир! Я Triang3l, графический программист Xenia, и это мой новый пост. Спустя почти шесть лет с последнего сообщения я расскажу, что случилось интересного в эмуляции Xbox 360!

В 2015 году список игр, которые можно считать «играбельными», был довольно мал, и в основном состоял из не особо технологически сложных и обычно двухмерных игр. В те времена в большинстве игр, геймплей которых хотя бы можно было увидеть, присутствовали серьёзные графические баги, а частота кадров была такой, что её нельзя считать не то что «комфортной», но хотя бы «движущейся», к тому же иногда возникали блокировки дальнейшей работы со стороны CPU.

Сегодня, 27 апреля 2021 года, из 1404 тайтлов трекера совместимости игр 1041 игра, то есть 74% от всей библиотеки протестированных игр, отмечены как играбельные или доходящие до этапа геймплея, а 221 из них играбельны почти без изъянов, а большинство игр может достигать нужной частоты кадров на современном «железе» PC! Xenia постоянно эволюционирует по всем аспектам — воссоздание ОС, CPU, обработка звука и, разумеется, эмуляция graphics processing unit Xenos консоли Xbox 360.

В частности, эмуляция GPU была очень захватывающим квестом на протяжении всего процесса разработки Xenia. GPU — это чрезвычайно сложное устройство, включающее в себя множество различных видов функциональности на всех этапах графического конвейера. А GPU консоли Xbox 360 была настоящей песочницей для экспериментов — его разрабатывали ближе к концу эпохи Direct3D 9, но ещё до Direct3D 10, и он содержит множество функций, нестандартизированных, а то и вовсе недоступных на PC; но когда они добрались до PC, реализация могла сильно отличаться; также он имел совершенно уникальные особенности. На протяжении этих лет мы пробовали разные подходы к эмуляции разных компонентов GPU, в том числе и провели полный редизайн всей архитектуры эмуляции GPU в 2018 году.

На этот раз мы релизим полностью переписанную реализацию самой специализированной части GPU Xbox 360 — обработку вывода цвета и буфера глубин/стенсил-буфера, что значительно повысило скорость традиционной реализации render target (без ROV), добавив опцию, которая может повысить его точность, обеспечивающую истинное MSAA вместо суперсэмплирования, а также добавляющую масштабирование разрешения 3x3 на путях вывода пикселей как с ROV, так и без ROV!

В этом посте мы подробно опишем, что происходило с эмуляцией GPU за последние три года, расскажем обо всех сложностях, с которыми мы столкнулись при эмуляции сверхбыстрой eDRAM консоли, и о предпринятых нами попытках решения. Мы объясним, почему она так сильно «съедает» производительность в эмуляции, в том числе и об обмене данными и проблемах со специализированными пиксельными форматами. Также мы расскажем об эмуляции унифицированной памяти и текстур!

Дисклеймер: информация об интерфейсе и поведении GPU Xbox 360 была получена из общедоступных источников, например, из релиза файла заголовка Code Aurora Forum Qualcomm и неофициального драйвера Freedreno (так как GPU Adreno 200 многое позаимствовал у Xenos), XNA Game Studio 3.1 и его документации на MSDN, презентаций и статей, выпущенных разработчиками игр и Microsoft, а также реверс-инжинирингом игр, консолей и мобильных устройств с Adreno 2xx. Автор этого поста никогда и ни для каких целей не использовал Xbox 360 XDK, и код, созданный на основании информации, полученной из XDK не допускается при создании эмулятора. Поэтому нет гарантии, что в посте представлена совершенно точная информация о поведении консоли.

Давным-давно в далёком-далёком рендерере…


Чтобы понять, почему render targets являются такой важной (и сложной) частью для эмуляции GPU Xbox 360, давайте вернёмся к этой прекрасной сцене из Halo, которую вы, вероятно, видели в 2018 году:


Эмулируем радугу! (Halo 3 компании Bungie, скриншот предоставлен CovertSlinky)

Здесь мы видим G-буфер, содержащий нормали пикселей, некоторые области которого (в формате letterbox катсцены) не были должным образом очищены, а также остатки другого прохода рендеринга, в котором участвовала растительность.

Но почему буфер, заполненный на очень раннем этапе конвейера рендеринга игры и используемый только для освещения, мог быть передан на окончательный вывод?

Давайте посмотрим, как выполняется управление буферами кадров (также называемыми «render targets») в игре для PC, работающей на графическом API PC, например, на Direct3D 9, 11 или OpenGL.

Упрощённый конвейер рендеринга с отложенным освещением имеет два прохода:

  1. Проход заполнения G-буфера — запись нормалей, цвета diffuse, glossiness и глубин.
  2. Проход освещения и композитинга — считывание нормалей, diffuse/glossiness и глубин, запись получившегося в итоге цвета.

На PC с Direct3D 9 подготовка и рендеринг выполняются следующим образом:

  1. Создаём устройство с поверхностью заднего буфера с 8 битами на канал (8.8.8.8) для вывода окончательных данных.
  2. Создаём текстуру нормалей с 10 битами на канал (10.10.10.2).
  3. Создаём текстуру diffuse/glossiness с 8 битами на канал (8.8.8.8).
  4. Создаём текстуру глубин.
  5. При отрисовке кадра:
    1. Привязываем поверхность текстуры нормалей как render target 0.
    2. Привязываем поверхность текстуры diffuse/glossiness как render target 1.
    3. Привязываем поверхность текстуры глубин как render target глубин.
    4. Отрисовываем геометрию для заполнения G-буферов.
    5. Привязываем поверхность заднего буфера как render target 0.
    6. Привязываем текстуру нормалей к пиксельным шейдерам.
    7. Привязываем текстуру diffuse/glossiness к пиксельным шейдерам.
    8. Привязываем текстуру глубин к пиксельным шейдерам.
    9. Отрисовываем проход освещения и вещи наподобие UI.
    10. Передаём задний буфер на экран.

Обычно при создании текстуры на старом API PC-графики с высоким уровнем абстрагирования наподобие Direct3D 9 мы просто указываем такие её свойства, как формат, размер, количество mip-текстур, способ использования, но память под неё выделяется где-то в другом месте драйвером или ОС. Само приложение получает непрозрачный дескриптор, например, указатель IDirect3DTexture9, который можно использовать для ссылки на текстуру в различных операциях API, например, для привязки текстуры к шейдерам для сэмплирования из неё, задания её в качестве текущего render target, копирования данных между этой и другими текстурами (обычно того же формата) или буферами, загрузка нового содержимого. Однако выделение памяти для текстуры скрывается от приложения приложением, и каждая текстура получает собственную отдельную выделенную память, не делящую своё содержимое ни с какой другой текстурой.

Вопреки распространённому заблуждению, Xbox 360 не является просто «коробкой с DirectX 9 внутри». По сути, он содержит GPU, напоминающий мобильные версии, использующий тайловый рендеринг (однако не полностью — об этом мы скажем позже), при этом обладающий гораздо большей «сырой» мощью, чем мобильные GPU со сравнимыми параметрами. Если сравнить регистры Xenos и Qualcomm Adreno 200, то можно увидеть, что большинство из них одинаково, поскольку они и являются практически одинаковыми GPU — до приобретения компанией Qualcomm процессор Adreno 200 назывался AMD Z430, и его даже называли "мини-Xenos"!

В традиционном графическом конвейере PC пиксели, полученные в результате отрисовки, записываются в текстуры в обычной памяти графической карты. Однако растровые операции — тестирование глубин (отбрасывание пикселей поверхностей, перекрытых другой геометрией), смешение, запись пикселей в буферы кадров — выполняются с чрезвычайно высокими частотами. Простая запись изображения 1280x720 с глубиной цвета 32bpp в соответствующий буфер глубин/стенсил-буфер при 60 FPS отнимает от полосы пропускания 422 МБ/с. Учитывая перерисовку и непрозрачных, и просвечивающих пикселей в 3D-сцене, заполнение буфера глубин на предварительном проходе для эффективного отбрасывания скрытых поверхностей, множественные проходы рендеринга в конвейере игры, а в некоторых играх и отложенное освещение с большим количеством попиксельных данных, все эти операции могут отнимать у полосы пропускания гигабайты в секунду.

Унифицированная память GDDR3 консоли Xbox 360 работает с частотой 700 МГц и имеет 128-битный интерфейс, обеспечивая полосу пропускания 22,4 ГБ/с. Эта полоса пропускания является общей для CPU и GPU, то есть если бы Xbox 360 имел обычную архитектуру GPU с непосредственным режимом, то операции с буферами кадров должны были бы конкурировать не только с другими операциями GPU, например, получением текстур (which amounts to bandwidth usage roughly comparable to that of color writing), но и со всей обработкой кадра на CPU. Подобная ситуация стала бы катастрофой для игр консоли в HD.

Именно здесь на помощь приходит eDRAM. eDRAM (сокращение от «embedded DRAM») — это очень важный ресурс. Он расположен непосредственно на чипе, с которым взаимодействует, и имеет с ним очень широкий интерфейс. eDRAM обеспечивает чрезвычайно широкую полосу пропускания. Однако за такую мощь приходится платить — eDRAM имеет гораздо меньшую плотность по сравнению с обычной DRAM и более дорога в производстве, поэтому её объём значительно ограничен по сравнению с основной ОЗУ. Хотя Xbox 360 имеет 512 МБ GDDR3 для CPU, а также для текстур и геометрии в GPU, он содержит всего 10 МБ eDRAM.


Слева — eDRAM Xbox 360, справа — GPU

Стоит заметить, что eDRAM консоли Xbox 360 — это не просто сырая память, к которой имеет доступ GPU. Отдельный чип на левой части изображения, вместе с самой памятью содержит всю логику слияния выходящих данных. Он соединён с GPU интерфейсом со скоростью 32 ГБ/с. Однако между оборудованием для растровых операций и памятью полоса пропускания составляет целых 256 гигабайт в секунду!


Архитектура памяти Xbox 360

Именно благодаря внутренней полосе пропускания Xbox 360 способен обрабатывать просвечивающие поверхности и MSAA без затрат полосы пропускания памяти буферов кадров и даже без таких техник сжатия, как исключение избыточных данных значений цветов и их отвязка от покрытия сэмплами. Не забывайте, что у нас есть полоса пропускания 32 ГБ/с между пиксельным шейдингом и блоком растровых операций, и 256 ГБ/с между растровыми операциями и пиксельной памятью, то есть в 8 раз больше.

Допустим, у нас есть значение цвета из пиксельного шейдера, и мы хотим записать его в буфер кадров. В простейшем случае нам просто нужно записать этот цвет в память — 1 операция с памятью. Но мы рендерим эффект просвечивания, например, стекло или дым, поэтому нам нужно смешать цвет с тем, что было отрисовано ранее. То есть нужно получить текущий цвет из буфера кадров, и получается уже 2 операции чтения/записи. Добавим к этому 4x multisample anti-aliasing. Концепция, лежащая в основе MSAA, заключается в том, что хотя значения цветов хранятся отдельно для каждого из сэмплов внутри пикселя, в отличие от чистого суперсэмплирования шейдер всё равно исполняется один раз для целого пикселя. То есть у нас всё равно есть одно значение на входе, но его нужно записать в 4 места в буфере кадров. А для получения просвечивания нужно смешать цвет с тем, что хранится в каждом из них, то есть при 4x MSAA и блендинге мы получаем 4 операции считывания и 4 операции записи, или 8 операций памяти буфера кадров на шейдер. Это хорошо сопоставляется с соотношением 32 ГБ/с: 256 ГБ/с внешней и внутренней полос пропускания eDRAM и чипа растровых операций — даже сочетание 4x MSAA и блендинга всё равно столь же быстро, как и запись одного непрозрачного сэмпла.

Примечание: в предыдущем параграфе предполагается, что цвет источника (из шейдера) имеет ту же битовую глубину, что и конечный цвет (буфер кадров), обычно 32 бит (чаще всего нормализованные 8.8.8.8 и 10.10.10.2 RGB с плавающей точкой + нормализованный альфа-канал). Обычно вполне нормально снизать точность значения источника, который был вычислен шейдером как полный вектор 32.32.32.32 чисел с плавающей запятой, поскольку погрешность достаточно мала и не вызовет существенных отличий, особенно учитывая то, что в конце значения всё равно будут преобразованы; функциональная спецификация Direct3D 11.3, определяющая поведение современных GPU на PC, также гласит, что «операции блендинга могут выполняться при равной или большей (например, до float32) точности/диапазоне, чем выходной формат». Однако исключением из правила «блендинг без затрат» является альфа-блендинг с форматами 10.10.10.2, когда предварительное преобразование альфа-канала источника до 2 бит оставляет только варианты «полностью невидимые», «видимые на 33%», «видимые на 66%» и «полностью непрозрачные», что не подходит для практического применения в поверхностях с плавной просвечиваемостью. Для этой цели Xbox 360 имеет псевдоформаты render target 2_10_10_10_AS_10_10_10_10 и 2_10_10_10_FLOAT_AS_16_16_16_16, из-за которых альфа-канал источника записывается в 10 бит или 16 бит, но в этом случае весь цвет RGBA больше не сможет поместиться в 32 бита, то есть будет использовать больше полосы пропускания между GPU и чипом eDRAM.

Финальная часть MSAA, резолвинг (усреднение значений цветов сэмплов для создания окончательного изображения с сглаживанием и запись результатв в основную ОЗУ GDDR3) тоже выполняется без ущерба полосе пропускания по сравнению с копированием изображения с одним сэмплом из eDRAM в основную ОЗУ. Два или четыре сэмпла получаются и усредняются самим чипом eDRAM по его внутреннему интерфейсу 256 ГБ/с, а на GPU передаётся только окончательное, занимающее малый объём односэмпловое изображение.

Выделенная полоса пропускания для буферов кадров, а также мультисэмплирование без затрат внесли важный вклад в чёткость картинки многих игр на консоли с разрешением 1280x720 при 30 FPS. Но давайте вернёмся к жестокой реальности эмуляции, в которой нам нужно воссоздать то, как сконфигурированы в консоли render targets, не имея при этом всей этой красивой и быстрой аппаратной архитектуры. Разумеется, все эти показатели полос пропускания нам в этом не помогут, единственное, что важно — нам нужно сделать то, что логика рендеринга игры ожидает от консоли, при этом правильно обрабатывать как можно больше пограничных случаев. И именно здесь самая любопытная и продуманная оптимизация становится самым ужасным узким местом.

Xbox 360 позволяет играм выполнять отрисовку только в eDRAM — после отрисовки и резолвинга (под этим подразумевается и резолвинг MSAA, то есть усреднение, и простое копирование в консоль) необходимо записать результат в задний буфер для отображения или в текстур для дальнейшего использования в шейдерах. Это соответствует способу выполнения рендеринга в тайловых GPU — область («тайл» или «бин») сцены отрисовывается с тестированием глубин, перерисовкой поверхностей и, возможно, MSAA, в небольшой блок памяти с широкой полосой пропускания (eDRAM или eSRAM), а окончательный результат (если необходимо, с усреднёнными сэмплами MSAA) затем записывается в соответствующую часть текстуры в обычной памяти; а затем всё это проделывается для другой области, и ещё для одной, пока не будет готово всё изображение.

В мобильных GPU, так как эти устройства работают от аккумуляторов, тайлинг необходим для максимизации полосы пропускания при низком энергопотреблении. Qualcomm Adreno 200, основанный на Xenos, имеет только 32-битный интерфейс для основной памяти с частотой 166 МГц, что обеспечивает полосу пропускания 1,3 ГБ/с — в 17 раз меньше, чем на Xbox 360. Память на чипе занимает большую часть пространства кристалла, поэтому на Adreno 200 доступно всего 256 КБ тайловой памяти, которой достаточно всего для части буфера кадров размером 256x128 с буфером цвета 32bpp и буфером глубин/стенсил-буфером без MSAA. Из-за этого тайловый рендеринг по принципу «отрисовываем первый тайл во всю тайловую память, резолвим, отрисовываем второй тайл, снова переписывая всю тайловую память, резолвим, и так далее» является практически единственным способом отрисовки сцены.

Однако на Xbox 360 такой памяти целых 10 МБ. Этого недостаточно для целой сцены в разрешении 1280x720 с MSAA — при 32-битном цвете и 32 битах буфера глубин/стенсил-буфера на пиксель для 2x MSAA потребуется 14 МБ, а для 4x — 28 МБ, поэтому в HD-играх с MSAA тоже использовался тайловый рендеринг, только с тайлами большего размера (1280x512 для 2x MSAA или 1280x256 для 4x, как в вышеупомянутом случае; размер тайлов был меньше, если запись выполнялась в несколько render targets). Однако этого достаточно для целых буферов кадров цвета и глубин размером 1280x720 без MSAA, или для более мелких render targets с 2x MSAA (например, для 1024x600 в серии Call of Duty: Modern Warfare), или нескольких render targets ещё меньшего размера для любой постобработки и композитинга, нужных игре. Это даёт играм большую гибкость в управлении буферами кадров, расположенных в eDRAM, в том числе и контроль срока жизни каждого распределения памяти eDRAM и его цели в каждой части кадра.

Пример с отложенным освещением, показанный в начале этого раздела, на Xbox 360 выглядел бы так (отличия от PC выделены курсивом; в качестве примера используется буфер кадров меньше полных 3600 КБ для 1280x720, потому что три буфера 1280x720 не поместятся в 10 МБ eDRAM):

  1. Создаём устройство с повехностью заднего буфера для окончательного вывода.
  2. Создаём текстуру нормалей.
  3. Создаём текстуру diffuse/glossiness.
  4. Создаём текстуру глубин.
  5. При отрисовке кадра:
    1. Привязываем диапазон eDRAM 0–3000 КБ как render target 0 в формате 10.10.10.2.
    2. Привязываем диапазон eDRAM 3000–6000 КБ как render target 1 в формате 8.8.8.8 .
    3. Привязываем диапазон eDRAM 6000-9000 КБ как render target глубин.
    4. Отрисовываем геометрию для заполнения G-буферов.
    5. Резолвим render target в диапазоне eDRAM 0–3000 КБ в текстуру нормалей.
    6. Резолвим render target в диапазоне eDRAM 3000–6000 КБ в текстуру diffuse/glossiness.
    7. Резолвим render target в диапазоне eDRAM 6000–9000 КБ в текстуру глубин.
    8. Привязываем диапазон eDRAM 0–3000 КБ как render target 0 в формате 8.8.8.8.
    9. Привязываем текстуру нормалей к пиксельным шейдерам.
    10. Привязываем текстуру diffuse/glossiness к пиксельным шейдерам.
    11. Привязываем текстуру глубин к пиксельным шейдерам.
    12. Отрисовываем проход освещения и вещи наподобие UI.
    13. Резолвим render target в диапазоне eDRAM 0–3000 КБ в поверхность заднего буфера.
    14. Передаём задний буфер на экран.

Заметьте, что теперь мы сначала выполняем отрисовку не напрямую в текстуры, а в участки eDRAM, а затем копируем данные в текстуры в основной памяти. И в отличие от PC, где каждый render target расположен в отдельном месте, теперь мы используем диапазон 0–3000 КБ памяти eDRAM в течение кадра в двух целях, сначала для записи нормалей, а затем для затенения и композитинга готового изображения.

При таком паттерне использования диапазон применяется для двух совершенно отдельных render targets точно так же, как и на PC. Именно так делает старая (основанная на Vulkan, но это не важно, поскольку различия заключаются только в высокоуровневой логике эмуляции) подсистема GPU эмулятора Xenia — она пытается управлять render targets eDRAM аналогично тому, как это реализовано в графике для PC. Здесь нет дескрипторов отдельных render targets. То есть ближайший используемый идентификатор — это свойства render target: расположение в eDRAM, ширина (не высота, она не требуется консоли, а потому не задаётся непосредственно в регистрах, что тоже является ещё одной серьёзной проблемой), количество сэмплов MSAA и формат. Разумеется, это не было намеренной конечной целью — мы предпринимали попытки к реализации правильной логики. Скорее, это была реализация-прототип, позволившая относительно быстро заставить работать множество игр, в основном кроссплатформенных, не использующих активно специфичные возможности Xbox 360, и во многих случаях это работало достаточно хорошо, в том числе и в показанном выше примере с отложенным освещением, более-менее решая задачу отображения игрового мира, иногда достаточного для игры, иногда для наблюдения за тем, как ведут себя разные части эмулятора.

Но что если игра делает с eDRAM что-то ещё, кроме «отрисовать один проход, резолвить, отрисовать ещё один проход, резолвить»?

Игра имеет полный контроль над тем, что сохраняется в eDRAM и как в ней размещены render targets. То есть для отрисовки области eDRAM ей достаточно указать адрес и параметры render target — ширину, количество сэмплов MSAA и формат. Однако простая привязка буфера кадров на Xbox 360 только изменяет конфигурацию логики пиксельного вывода — адресацию и упаковку формата. Она не повлияет на текущее содержимое памяти, пока мы не выполним отрисовку после задания новой конфигурации. И это, среди прочего, позволяет реинтерпретировать данные, в текущий момент хранящиеся в eDRAM, с другим форматом или схемой расположения. Вот часто встречающиеся примеры подобного:

  • Сброс render targets любого типа до единого значения. У GPU консоли Xbox 360 нет специальной функции очистки — в резолвинге нет логики очистки, она выполняет сброс вместе с копированием из eDRAM в основную ОЗУ; в противном случае единственным способом сброса является отрисовка прямоугольника с помощью обычной логики отрисовки. Раньше мы говорили о том, что мультисэмплирование на Xbox 360 выполнялось без лишних затрат — вся репликация сэмплов выполнялась внутри чипа eDRAM и записывалась в память с внутренней полосой пропускания 256 ГБ/с, а не 32 ГБ/с между GPU и логикой render target. Для вычисления цвета (не глубины, поскольку для пересечений полигонов тоже нужно выполнять сглаживание — глубина обрабатывается совершенно иначе) MSAA реплицирует одно и то же значение во все покрываемые сэмплы пикселя. Нам нужно заполнить весь render target одинаковым значением, так почему бы не использовать логику MSAA для сброса render targets 2xAA с удвоенной скоростью, а render targets с одним сэмплом — в 4 раза быстрее? В консоли Xbox 360 сэмплы render targets 2xAA хранятся как 1x2 односэмпловые пиксели, а сэмплы 4xAA отображаются на пиксели 2x2 без MSAA. То есть для сброса односэмплового render target размером 1280x720 игры (а конкретнее, графическая библиотека наподобие Direct3D 9, которая, в отличие от библиотеки на PC, является частью самого исполняемого файла игры, то есть с нашей точки зрения это просто код игры, напрямую взаимодействующий с оборудованием консоли) могли отрисовывать прямоугольник размером 640x360 с 4x MSAA в тот же участок памяти eDRAM, где находился односэмпловый прямоугольник 1280x720. Но графическая библиотека Xbox 360 на этом не останавливается. Для отрисовки render target цвета пиксельный шейдер должен выполняться для каждого покрываемого пикселя, после чего получившееся значение нужно скопировать через интерфейс 32 ГБ/с между GPU и чипом eDRAM. Но нам нужно записать одно и то же значение во все пиксели, то есть мы будем впустую тратить ресурсы, если постоянно будем выполнять пиксельный шейдер, всегда возвращающий одинаковое значение. Однако попиксельные данные не требуются глубине — для всего треугольника (размером в полэкрана) вся информация, необходимая для вычисления глубины в любой точке, составляет 24 байта — координаты Z и W его трёх вершин. Так как буфер кадров нужно сбрасывать на постоянное значение, наиболее оптимальным способом очистки буфера кадров на Xbox 360 является отрисовка треугольника параллельно «виду» (с постоянными координатами Z и W) в render target глубин 4x MSAA в той же области eDRAM с младшими 8 битами из необходимого упакованного 32-битного значения, записанного через стенсил, а старшие 24 бита преобразуются в число с плавающей запятой и делятся на 0xFFFFFF, чтобы превратиться в «глубину» отрисовываемого треугольника.
  • Повторная загрузка буфера глубин/стенсил-буфера в eDRAM. Иногда игра может отрисовывать что-то, относящееся к буферу глубин/стенсил-буферу за несколько проходов в разных частях кадра, но между этими операциями она может отрисовывать при помощи eDRAM что-то другое, а при наличии буфера глубин/стенсил-буфера может остаться недостаточно eDRAM для этих проходов в середине, поэтому она может временно избавиться от буфера глубин, отрезолвив его в основную память, а затем при необходимости перезагрузив обратно в eDRAM. Например, такое поведение может использоваться для 3D-маркеров UI, которые требуется скрывать, если они находятся за стенами, но при этом на них не должно влиять освещение, тональная коррекция и другие виды постобработки. В частности, это происходит, когда игра использует MSAA, а значит, и тайловый рендеринг, например Grand Theft Auto IV и основное меню с начальной катсценой в Halo 3. В них недостаточно памяти даже для отрисовки всего изображения за один проход, поэтому игре нужно использовать буфер глубин/стенсил-буфер после прохода тайлинга, а значит, ей обязательно нужно дампить каждый тайл в ОЗУ, а затем загружать их обратно. Для этого игра привязывает render target цвета в той же области, что и целевой буфер глубин, а затем записывает изображение глубин/стенсила как цвет. Даже несмотря на то, что Xbox 360 имеет вывод пиксельного шейдера с переопределением глубин, render target цвета необходим для загрузки загрузки данных и глубин, и стенсила — даже сегодня на многих GPU (Nvidia; и это также вызывает неудобства в новом кэше render target эмулятора Xenia) ссылочное значение стенсила можно задать только для вызова отрисовки целиком, а не для каждого пикселя по отдельности, поэтому используется render target формата 8.8.8.8, в котором 8 бит стенсила записываются в канал красного, а 24 бита глубин — как зелёный, синий и альфа.

При подходе, похожем на PC, обработка всех render targets как отдельных объектов означала бы, что даже простейшие операции, например, очистка буфера кадров, в частности, буфера глубин не сработали бы — если бы игрок сместился хотя бы немного назад, то игра не смогла бы больше отрисовывать сцену. Поэтому обмен содержимым eDRAM между render targets на PC совершенно необходим.

Если вы видели современные API для GPU наподобие Direct3D 12, Vulkan и Metal, то могли задаться вопросом: мы говорили о том, что буферы кадров на PC совершенно отделены друг от друга в старых API наподобие Direct3D 9, 11 и OpenGL, но как насчёт новых, где можно размещать ресурсы вручную в указанных областях при помощи объектов ID3D12Heap, VkDeviceMemory или MTLHeap? Сможет ли решить проблему их механика наложения?

Нет, это не так, и у них никогда не было такой задачи. Причина заключается в том, что на PC существует широкий ассортимент различных микроархитектур GPU разных производителей, и задача графического API на PC, будь то «stateful» API с внутренним отслеживанием ресурсов типа Direct3D 11, или более «bindless»-ориентированный, наподобие Direct3D 12 — предоставлять унифицированный способ программирования всех GPU. Изображения в целом, и в частности буферы кадров хранятся внутри графической памяти в перемешанном виде. Если изображение хранится линейным образом (все данные первой строки, затем второй строки, и так далее), то пиксели, соседние по оси X, хранятся друг рядом с другом, но по вертикальной оси соседние строки хранятся разреженно, что неэффективно с точки зрения кэша — текстуры могут поворачиваться на экране любым способом, а их фильтрация должна выполняться в обоих направлениях, к тому же паттерны доступа к буферам кадров должны более-менее соотноситься с тем, как выполняется доступ к текстурам. Каждая микроархитектура GPU имеет собственную структуру данных, оптимальную для этой конкретной архитектуры. AMD GCN представляет изображения в виде «микротайлов» размером 8x8 (не путать с тайловым рендерингом), выстроенных в некий макротайловый порядок, у GPU Nvidia имеется собственная концепция «групп байтов» в блоках. Кроме того, GPU могут иметь дополнительные структуры со сжатием без потерь для буферов кадров, используемые для снижения полосы пропускания, например, иерархаический буфер глубин, хранящий границы глубин в области и более компактное описание глубин (например, уравнения плоскостей полигонов) для одновременного отбрасывания множества пикселей, различные виды сжатия дельты цветов, маски быстрой очистки, структуры дедупликации значений сэмплов MSAA. Подобные подробности реализации не только индивидуальны для каждого из производителей, но и могут меняться в разных поколениях GPU в процессе совершенствования оборудования и нахождения более оптимальных паттернов доступа. Поэтому графический API для PC должен абстрагировать эти различия, обеспечивая унифицированный, предсказуемый интерфейс, который будет совместим со всем разнообразием современного и будущего оборудования.

У GPU Xbox 360 тоже есть собственные оптимальные структуры данных, но это игровая консоль с неизменной аппаратной конфигурацией, поэтому подробности упорядочивания данных известны разработчикам игр и они могут пользоваться непосредственным доступом к данным для оптимизации алгоритмов под Xbox 360. И буферы кадров в eDRAM тоже не являются исключением. eDRAM разделена на «тайлы», в основном не связанные с тайловым рендерингом (это просто общий термин для того, что связано с равномерной сеткой) — по 32 бита на сэмпл из сэмплов MSAA
размером 80x16 на тайл, что соответствует 80x16 пикселям без MSAA, 80x8 пикселям с 2x MSAA, и 40x8 пикселям с 4x MSAA. Кроме того, глубина хранится иначе, чем цвет — чётные и нечётные столбцы по 40 сэмплов внутри каждого тайла в отличие от буферов цвета в буферах глубин перевёрнуты, и это переворачивание выполняется в шейдерах игры явным образом, когда им нужно заново загрузить глубины после удаления их из eDRAM. Форматы с 64bpp тоже имеют собственную структуру — мы надеемся найти игру, в которой они реинтерпретируются из 32bpp или обратно.

Старая реализация эмуляции GPU отображала G-буфер нормалей в окончательном выводе из-за нескольких необрабатываемых случаев в потоке данных буфера кадров. Вот что происходит с буфером нормалей во время создания кадра Halo 3:

  • Игра записывает нормали в целочисленный (integer) render target формата 10.10.10.2 в eDRAM.
  • Render target 10.10.10.2 в eDRAM с нормалями копируется в текстуру, которая позже используется на проходе освещения.
  • Различные проходы кадра отрисовываются в разные render targets.
  • Затенённое изображение игры с bloom, а потом и с элементами интерфейса отрисовывается в render target 10.10.10.2 чисел с плавающей запятой, который по совпадению находится в том же диапазоне eDRAM, что и целочисленный 10.10.10.2.
  • GPU консоли Xbox 360 имеет ограничение: формат render target 10.10.10.2 с плавающей запятой доступен только для render targets, но не для текстур, которые могут считываться после экспорта из eDRAM в основную RAM. Проходы композитинга и интерфейса записывают цвета таким образом, чтобы их можно было передать на экран как целочисленное изображение формата 10.10.10.2. То есть после отрисовки интерфейса Halo 3 копирует целочисленный render target 10.10.10.2 (накладывающийся на изображение 10.10.10.2 с плавающей запятой) в целочисленную текстуру 10.10.10.2 и отправляет её на экран.

Как мы видим, данные готового изображения с затенённой игровой сценой и интерфейсом должны браться из целочисленного render target 10.10.10.2. Однако он был записан в ту же область eDRAM, что и render target с плавающей запятой, но поскольку передачи содержимого eDRAM не было, старый кэш render target просто искал последний образ буфера кадров Vulkan, который использовался для отрисовки как целочисленный 10.10.10.2, а это оказывался G-буфер нормалей, поэтому он выводился на экран.

До того, как началась разработка нового полнофункционального графического бэкенда на основе Direct3D 12, мы предприняли попытку добавить полнофункциональную передачу содержимого eDRAM между образами буферов кадров Vulkan. Мы надеялись, что так удастся заставить работать рендеринг Halo 3, однако мы столкнулись с ещё одним архитектурным препятствием.


Один из ранних экспериментов с передачей данных eDRAM — теперь мы не видим самой игры (Halo 3 компании Bungie)

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

На этот раз проблема связана с хранением текстур в основной ОЗУ. Они тоже имеют собственный формат хранения — 2D-текстуры хранятся как последовательность тайлов по 32x32 пикселя (или 32x32 блока, для сжатых текстур) с функцией адресации микротайлов, а 3D-текстуры имеют 32x32x4 тайла (хотя адресация повторяется через каждые 32x32x8 пикселей). Копирование из eDRAM в основную ОЗУ также выполняет запись в эту структуру.

Старый код графики обрабатывал эту структуру при загрузке текстур в CPU, чтобы преобразовать их в структуру, которую можно было загружать в текстуры Direct3D 12. Однако рендеринг в текстуру происходит внутри GPU (за исключением некоторых случаев обратного считывания из CPU). То есть предыдущая реализация эмуляции GPU в Xenia обрабатывала результаты рендеринга в текстуру отдельно от обычных текстур, загружаемых из унифицированной памяти консоли в CPU. Обычные текстуры загружались из памяти консоли и преобразовывались из структуры тайлов 32x32. Однако рендеринг в текстуру имел собственный кэш, и когда эмулятору нужно было загрузить текстуру, он сначала смотрел в этом кэше, было ли что-то скопировано из eDRAM разными свойствами текстуры, например, адрес (с эвристиками для обработки некоторых случаев тайлового рендеринга, для которых требуется копирование каждой части области посередине целевой текстуры, а не целиком), формат, ширина, высота. И если соответствующая запись кэша рендеринга в текстуру находилась, она использовалась вместо данных из унифицированной памяти.

Структура из тайлов 32x32 означает, что текстуры (предполагаем, что mip-текстуры отсутствуют) размером, допустим 33x33, 63x33, 64x33, 64x64 хранятся в памяти одинаково — если их размер не кратен 32x32, текстура просто заполняется до тайлов 32x32 неиспользуемыми байтами. То есть для вычисления адреса в текстуре нам не нужен точный размер в пикселях. Нам достаточно знать количество тайлов вдоль горизонтальной оси, чтобы понять, как далеко строки из тайлов 32x32 находятся друг от друга в памяти.

Во время копирования из eDRAM в ОЗУ графическая библиотека консоли указывает размеры конечной текстуры для вычисления адреса. Высота, передаваемая GPU, как ни удивительно, указывается в пикселях (даже несмотря на то, что это даже не требуется для копирования 2D-текстур и пригождается в редких случаях рендеринга в 3D-текстуру). Однако ширина округляется до тайла, то есть до 32.

Эффект bloom в Halo 3 выполняет несколько проходов изменения размера изображения, содержащего яркие части сцены, одновременно их размывая. В каждом из этих проходов нужно отрисовывать уменьшенное из размытое изображение в eDRAM, а затем копировать его в текстуру. И один из таких проходов создаёт изображение 72x40. Но как мы говорили, при рендеринге в текстуру GPU получает ширину, округлённую до тайлов. То есть Xenia видит, что игра хочет выполнить копирование области eDRAM в текстуру 96x40. И старая подсистема GPU создавала текстуру 96x40 в кэше рендеринга в текстуру.

Но для считывания из текстуры требуется точный размер, чтобы обеспечивать правильное масштабирование, повторение или ограничение координат. И позже игре требуется текстура размером именно 72x40. Но поскольку изображение было скопировано в текстуру 96x40, Xenia не может найти необходимую текстуру 72x40 в кэше рендеринга в текстуру и откатывается к загрузке её из памяти на стороне CPU, в которой она, разумеется, никогда не заполнялась, и вместо этого загружает только нули. Однако шейдер bloom Halo 3 реализован таким образом, что области с 0 в альфа-канале становятся в конце чёрными, а поскольку вся текстура bloom имеет альфу 0, то конечный результат получался полностью чёрным.

И именно тогда мы решили, что нам нужно создать нечто совершенно другое.

Продолжение следует...