[Перевод] Руководство по разработке эмулятора NES (перевод)
Всем привет! Меня зовут Сергей!
Отступление
Прошу простить, тут не только перевод будет, но возможные рекомендации от меня (где-то как от переводчика, где-то как дополнение). Так же прошу простить, я с английским не дружу, и для перевода пользовался гуглопереводчиком+яндекспереводчиком. При переводе конечно старался привести всё в надлежащий вид, но не уверен что достаточно хорошо получилось.
Так как я делаю эмулятор Nes, то и статьи пока выкладываю именно по данной теме. Если вы решили сделать эмулятор, по сути любой эмулятор, то данная статья (перевод) может быть вам полезна для понимания что и как делать. Начинающим, всегда лучше делать самый простой эмулятор и набивать на этом руку. Даже если вы просто повторите создание эмулятора за кем-то и постараетесь внести изменения в созданный «вами» эмулятор, то это так же поможет в понимании создании эмуляторов!
Не надо рассматривать данный перевод как что-то плохое или хорошее. Как я понимаю, люди начинали его писать очень давно и у них было своё виденье на тот момент. Статья дополнялась, обрастала новой информацией. Что-то безнадёжно могло устареть, а что-то актуально по сей день.
Если вы нашли неточности или знаете как более правильно сформулировать определённые части документа, не стесняйтесь, пишите!
Свой эмулятор я немного усовершенствовал, процессор работает (не уверен что полностью, но пока графику не сделаю, не смогу проверить), сделал согласование процессора с PPU (только вертикальную синхронизацию в основном, работа с регистрами пока не реализована). Я не хочу глобальных ошибок совершать и слишком много переделывать свою же работу. Потому обдумываю как сделать и получается довольно долго всё это.
несколько советов
не обязательно читать весь документ, возможно в данный момент вас будет интересовать что-то одно.
начинаете делать эмулятор, начинайте с реализации процессора, если вы изначально начнёте что-то другое реализовывать, то пока процессор не будет готов, вы (практически) не сможете проверить свои наработки.
не старайтесь всё сразу оптимизировать, сначала реализуйте «на скорую руку», проверьте работоспособность, а уже потом оптимизируйте.
не стоит следовать за всеми советами, возможно вы уже сами что-то придумали.
не стоит игнорировать советы, вполне возможно по тем граблям по которым вы хотите идти, уже прошлись не один десяток раз.
будет сложно, и возможно долго.
не бойтесь сложностей, до вас уже многое сделали, следуйте по их стопам.
обязательно смотрите исходные коды уже готовых решений! Ни в коем случае не пренебрегайте ими (даже если вы не знаете данного ЯП, вполне возможно даже коментарии вам помогут, и даже самый простой код).
Давайте начнём.
Руководство по разработке эмулятора NES
Brad Taylor (BTTDgroup@hotmail.com)
4th release: April 23rd, 2004
Thanks to the NES community. http://nesdev.parodius.com.
Рекомендованная литерутура: 2A03/2C02/FDS technical reference documents
Обзор документа
руководство для программистов, пишущих собственное программное обеспечение эмулятора NES/FC
предоставляет множество советов по оптимизации кода (с упором на платформу персональных компьютеров на базе x86)
предоставляет списки функций для реализации в эмуляторе, предназначенном для общедоступного выпуска.
создано с целью улучшения качества игрового опыта пользователя NES
Обсуждаемые темы
1. Общая эмуляция PPU
1.1. Информация о PPU
1.2. Точная и эффективная эмуляция PPU
1.3. Зная, когда обновить экран
1.4. Флаг столкновения
1.5. MMC3 IRQ таймер
1.6. Уравнения CPUCC для координат X/Y
1. 7. Примечание по имитации игры Ms.Pac Man от Tengen.
1.8. Другие примечания
2. Методы рендеринга пикселей
2.1. Базовый
2.2. Индексированный регистр палитры VGA
2.3. Рендеринг на основе инструкций MMX
2.4. Предсказание ветвления
3. Объединение пикселей игрового поля и объектов
3.1. Другие советы
4. Оптимизация хранилища кадров
4.1. Краткая информация о встроенных кэшах x86
4.2. Предостережение о виртуальном буфере кадров
4.3. «Хранилища строк сканирования»
4.4. Преодоление «letterboxed» в дисплеях.
5. Плавное воспроизведение звука
5.1. Обзор
5.2. Почему на высоких частотах есть артефакты?
5.3. Решения
5.4. Простая реализация прямоугольного канала
5.5. Другие примечания
6. Методы декодирования и выполнения инструкций 6502
6.1. Способы эмуляций
6.2. Другие советы
7. Декодирование адреса эмуляции
8. Аппаратная очередь портов
8.1. Обзор
8.2. Реализация
8.3. Атрибуты элемента списка
9. Многопоточные приложения NES
10. Поддерживаемые функции эмулятора
11. Новая объектно-ориентированная спецификация формата файла NES
11.1. Что означает объектная ориентация?
11.2. Заметки
1. Общая эмуляция PPU
Скорее всего, ключом к производительности вашего эмулятора будет скорость, с которой он может отображать графику NES. Довольно легко написать медленный движок рендеринга PPU, так как в целом предстоит проделать большой объем работы. Точная эмуляция PPU затруднена из-за всех уловок, которые различные игры NES используют для достижения специальных видеоэффектов (например, прокрутки разделенного экрана), что в противном случае невозможно с помощью «чистых» или обычных средств. На самом деле все эти «фокусы» просто выполняются записью в соответствующие регистры PPU (или родственные) в нужный момент во время рендеринга кадра (картинки).
На аппаратном уровне CPU и PPU в NES работают одновременно. Вот почему игра может быть закодирована для записи в регистр PPU в определенное время в течение кадра, и в результате этого (экранный) эффект возникает в определенном месте на экране. Таким образом, при написании эмулятора NES возникает желание поочередно запускать CPU и PPU на каждом тактовом цикле. Результаты этого дадут очень точную эмуляцию, НО это также будет ОЧЕНЬ интенсивно использовать процессор (это будет в основном из-за всех накладных расходов на передачу управления программой такому большому количеству процедур аппаратной эмуляции за столь короткое время (1 такт ЦП)). В результате эмуляторы, написанные таким образом, оказываются самыми медленными.
1.1. Информация о PPU
Графика NES состоит из одного прокручиваемого игрового поля и 64 объектов/спрайтов. Разрешение экрана составляет 256×240 пикселей, и хотя в играх можно управлять графикой на попиксельной основе, этого обычно избегают, поскольку это довольно сложно. Вместо этого PPU упрощает программисту отображение графики, разделяя экран на плитки, которые индексируют растровое изображение размером 8×8 пикселей, которое появляется в этом конкретном месте. Каждый объект определяет 1 или 2 плитки, которые будут отображаться на случайно доступной координате xy на экране. В PPU также есть 8 таблиц палитр, на которые могут ссылаться растровые данные (данные растрового изображения игрового поля и объекта имеют по 4 палитры). Каждая палитра имеет 3 индексируемых цвета, поскольку растровые изображения тайлов состоят только из 2 битов на пиксель (»00» — нулевая комбинация считается прозрачностью). Также определен единый регистр цветовой палитры прозрачности, который используется только в качестве цвета фона на экране, когда перекрывающиеся пиксели всех плиток игрового поля/объекта определены как прозрачные.
По мере рендеринга графики (как описано в документе »2C02 technical reference») к таблицам имен последовательно обращаются для ссылки на растровое изображение плитки, которое используется в качестве данных пикселей для области экрана, которой соответствует запись индекса таблицы имен (смещено значениями регистра прокрутки). Таблицы атрибутов, которые располагаются так же, как таблицы имен (за исключением более низкого разрешения — 1 запись атрибута представляет кластер 2×2 экранных плиток), определяют значение выбора палитры для группы плиток, которые будут использоваться (1 из 4).
Память атрибутов объектов (ОЗУ спрайтов или »OAM», которая содержит частный индекс плитки и информацию о выборе палитры) оценивается для каждой отдельной строки развертки (проверяются записи координаты Y), а объекты в диапазоне имеют свои растровые изображения плитки, загруженные в PPU между строками развертки. Затем содержимое объединяется с пиксельными данными игрового поля в режиме реального времени.
1.2. Точная и эффективная эмуляция PPU
По большей части операции PPU являются линейными и последовательными, что упрощает разработку алгоритмов для них. Благодаря рендерингу игровых полей и объектов для каждой плитки эмуляция может быть быстрой и простой. Однако игры, в которых используются специальные эффекты (хитрость PPU в середине кадра), требуют специальной обработки, что, в свою очередь, усложняет разработку алгоритма.
Реализуя счетчик тактовых циклов в ядре ЦП, эмулируемое аппаратное обеспечение PPU может точно знать, когда происходит чтение/запись в регистр, связанный с PPU (или иначе, регистр, который с этого момента изменит рендеринг графики). Следовательно, когда происходит запись в регистр PPU, механизм PPU может затем определить, будет ли запись изменять способ рендеринга изображения и точный тактовый цикл (который действительно преобразуется в положение на экране).
Например, скажем, механизм ЦП выполняет инструкции. Затем на такте 13000 (относительно последнего VINT) производится запись в регистры прокрутки PPU (что вызывает эффект разделения экрана). Теперь сначала PPU переводит 13000 CC в координаты X/Y (в данном случае это строка сканирования на экране 93, примерно пиксель #126 (уравнения для выполнения этих вычислений будут раскрыты позже)). В идеале* все пиксели до этой точки теперь будут отображаться в буфере с использованием данных в регистрах PPU до записи. Теперь область экрана до того, как произошла запись, была отрисована точно, и экран будет постепенно обновляться таким образом по мере увеличения количества записей в середине кадра. Если больше ничего не происходит, когда процессор достигает количества тактовых циклов на кадр, остальная часть изображения (если таковая имеется) может быть отрисована.
Примечание: Как будет обсуждаться в следующих разделах «Оптимизация хранилища кадров» и «Ориентация на очередь аппаратных портов», поддержание «стека» или, точнее, очереди изменений PPU в середине кадра (которые влияют на то, как происходит последовательный рендеринг в кадре) и выполнение процедуры рендеринга PPU только один раз за кадр (которая затем обрабатывает стек записей в середине кадра) является более эффективным способом разделения задач эмуляции в вашем эмуляторе.
1.3. Зная, когда обновить экран
В следующем списке описаны регистры/биты состояния PPU, которые, если игра изменяется/модифицируется в середине кадра, изменят способ рендеринга остальной части кадра. O = обновить объекты, P = обновить игровое поле.
O бит включения объекта
O отсечение объектов левой колонки
O 8/16 объектов строки развертки
O таблица шаблонов активных объектов
O переключатель банков таблиц шаблонов (который влияет на таблицу шаблонов активных объектов)
PO биты цветового акцента
PO черно-белый/выбор цвета
P бит включения игрового поля
P вырезка игрового поля в левой колонке
P регистры прокрутки
P Выбор таблицы имен X/Y
P таблица имен bankwitch (гипотетическая)
P таблица шаблонов активного игрового поля
P переключатель банков таблиц шаблонов (который влияет на таблицу шаблонов активного игрового поля)
Примечание переводчика: более точно и подробно читайте про регистры PPU.
Обратите внимание, что любая отображенная память PPU (что означает имя, шаблон, атрибут и таблицы палитры) может быть изменена только тогда, когда объекты и игровое поле отключены (если только аппаратное обеспечение картриджа не предоставляет способ сделать это через карту памяти CPU). Поскольку в это время экран становится черным (независимо от текущего цвета прозрачности, запрограммированного для палитры), эти записи не влияют на то, как отображается экран, и, следовательно, обновление экрана может быть отложено.
1.4. Флаг столкновения
Игры без оборудования для подсчета строк сканирования часто опрашивают этот бит, чтобы узнать, когда делать запись в регистр PPU, что приведет к разделению экрана или переключению таблицы шаблонов/банков. Флаг столкновения устанавливается, когда первый непрозрачный пиксель объекта 0 сталкивается с пикселем игрового поля, который также не является родительским для X (xparent). Поскольку положение на экране первого сталкивающегося пикселя может быть определено в любое время (и, следовательно, точный такт процессора, на котором ожидается столкновение), когда игра запрашивает статус этого флага в первый раз, рутинная часть движка PPU может вычислить, на каком такте этот флаг будет установлен (вычисления будут показаны позже). Последующие запросы статуса флага столкновения после этого потребуют от движка только сравнения текущего такта процессора с рассчитанным тактом столкновения. Всякий раз, когда происходит изменение в середине кадра (независимо от того, влияет ли оно на игровое поле или объекты), такт, на котором сработает флаг столкновения, должен быть пересчитан (если он уже не сработал).
1.5. MMC3 IRQ таймер
Таймер IRQ в MMC3 основан на переключении строки A13 PPU, 42 раза за строку сканирования. В принципе, его подсчет выполняется более или менее с постоянной скоростью (что означает предсказуемость). Однако, когда шина PPU отключена (путем отключения игрового поля и объектов или в течение периода V-blank), счетчик должен прекратить подсчет. Ручное переключение битов адреса PPU в течение этого времени должно быть перехвачено, и таймер IRQ должен быть соответствующим образом активирован.
1.6. Уравнения CPUCC для координат X/Y
PPU визуализирует 3 пикселя за один такт ЦП. Следовательно, умножив значение CPU CC на 3, мы получим общее количество пикселей, которые были визуализированы (включая неотображаемые) с момента VINT. На одну строку сканирования визуализируется 341 пиксель (хотя отображаются только 256). Следовательно, разделив PPUCC на это, мы получим количество полностью визуализированных строк сканирования с момента VINT.
21 пустая строка сканирования визуализируется до отображения первой видимой. Таким образом, чтобы получить смещение строки сканирования в фактическом изображении на экране, мы просто вычитаем количество неотображаемых строк сканирования. Обратите внимание, что если это даст отрицательное число, то PPU все еще находится в периоде V-blank.
PPUCC = CPUCC * 3
Scanline = PPUCC div 341 - 21; X- coordinate
PixelOfs = PPUCC mod 341; Y- coordinate
CPUcollisionCC = ((Y+21)*341+X)/3
Обратите внимание, что если уравнение PixelOfs дает число больше 255, значит PPU находится в периоде H-blank.
1. 7. Примечание по имитации игры Ms.Pac Man от Tengen.
Для эмуляторов с ограниченным количеством циклов 6502 при попытке запустить эту игру может возникнуть небольшая проблема. Во время инициализации эта игра будет зацикливаться, ожидая установки флага vbl для $2002. При возникновении NMI процедура NMI считывает значение $2002 и отбрасывает это значение. Несмотря на то, что процедура NMI сохраняет регистр »A» из основного цикла (в который был загружен $2002), PC выйдет из этого цикла только в том случае, если $2002 вернет флаг vbl, установленный *непосредственно* перед выполнением NMI. Поскольку NMI вызывается в ожидании завершения текущей инструкции, а флаг vbl ЯВЛЯЕТСЯ флагом NMI, флаг VBL должен быть установлен в середине инструкции LDA. Поскольку в основном цикле есть 2 инструкции, вероятность того, что прочитанное значение из $2002 будет помещено в стек с установленным битом vbl, составляет около 50%. Обходной путь для эмуляторов, которые не могут справиться с этим табу на выполнение промежуточных команд, заключается в небольшой установке бита vbl перед вызовом процедуры NMI.
Примечание переводчика: бит 7 регистра $2002 (PPUSTATUS) сбрасывается при чтении. Если чтение данного регистра попадает в промежуток времени от -1 до 1 (2?) тактов в период NMI, то NMI не сработает. Программы часто проверяют данный регистр, чтоб понять должно произойти прерывание NMI или нет. Некоторые программы специально это делают, для того чтоб пропустить прерывания NMI.
Информация здесь.
1.8. Другие примечания
некоторые игры полагаются на правильную реализацию столкновений и сбрасывание флагов объектов в регистре $2002. Обычно это делается для реализации до 3 независимых прокручиваемых игровых полей с горизонтальной плиткой. Убедитесь, что эти флаги установлены в нужное время и остаются установленными до строки развертки 20 следующего кадра (относительно /NMI).
(предоставлено Xodnizel): «Когда я возился с эмуляцией игр MMC3 таким образом (описанным выше), я получил наилучшие результаты, сбрасывая счетчик count-to-42 на 0 при записи в $C001. Или, другими словами, я сбрасывал счетчик »count to zero» на 42».
2. Методы рендеринга пикселей
В этом разделе описаны 3 метода рендеринга. Все они используются в режиме реального времени. В неизданной версии этого документа обсуждалось решение для рендеринга на основе тайлового кэша. Однако кэширование фрагментов быстро теряет свою эффективность в тех играх, которые используют трюки в середине кадра (или даже в середине строки сканирования) для изменения наборов символов или даже значений палитры. Кроме того, поскольку мощные ПК на базе процессоров Pentium седьмого поколения на сегодняшний день являются самыми медленными компьютерами в мире, больше нет необходимости использовать алгоритмы кэширования растровых изображений для эмуляции графики NES, как это было необходимо во времена ПК на базе 486, чтобы добиться полной эмуляции частоты кадров в NES.
2.1. Базовый
Этот метод, который является наиболее простым, заключается в сохранении 52-цветной матрицы PPU в виде постоянных данных в регистрах палитры VGA (или в других регистрах палитры, используемых для графического режим
Habrahabr.ru прочитано 2527 раз