Всем привет! Меня зовут Сергей!
Отступление
Прошу простить, тут не только перевод будет, но возможные рекомендации от меня (где-то как от переводчика, где-то как дополнение). Так же прошу простить, я с английским не дружу, и для перевода пользовался гуглопереводчиком+яндекспереводчиком. При переводе конечно старался привести всё в надлежащий вид, но не уверен что достаточно хорошо получилось.
Так как я делаю эмулятор 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.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.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.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 (или в других регистрах палитры, используемых для графического режима 8 бит на пиксель). Прежде чем пиксель можно будет нарисовать, вычисляется цвет пикселя (с помощью таблицы шаблонов и данных выбора палитры). Регистры палитры PPU тем или иным образом просматриваются, и содержимое элемента регистра палитры записывается в виртуальный кадровый буфер в виде данных пикселей. Этот метод проще всего реализовать, и он обеспечивает наиболее точную эмуляцию PPU. Однако, поскольку для каждого нарисованного пикселя требуется независимый поиск палитры, этот метод, естественно, очень медленный.
Один из способов ускорить этот стиль рендеринга — создать таблицу палитр, предназначенную для одновременного просмотра 2 или более пикселей. Преимущества очевидны: вы можете легко сэкономить много времени (приблизительно вдвое при одновременном поиске двух цветов) на рендеринге игрового поля. Недостатки заключаются в том, что таблица поиска увеличивается с 2 ^ 2 * 1 = 4 байта для поиска с одним пикселем до 2 ^ 4 * 2 = 32 байта для поиска с 2 пикселями и до 2 ^ 8 * 4 = 1024 байта для поиска. 4-пиксельный поиск. Каждый из 4 цветов палитры также отражается в этих таблицах, и это необходимо поддерживать. Поскольку я никогда не пробовал этот метод оптимизации, я не могу сказать вам, насколько он эффективен (или когда он перестает быть эффективным).
Еще один способ увеличить скорость этого подхода — изменить порядок битов в таблицах шаблонов, хранящихся в памяти, в пользу этого алгоритма рендеринга. Например, сохраните растровое изображение для любой строки сканирования тайла в 8-2-битном формате упакованных пикселей вместо 2-8-битного планарного метода, используемого по умолчанию. Это позволит процедуре рендеринга тайлов легко извлекать 2-битное число для индексации 4-цветной палитры, связанной с конкретным тайлом. Конечно, при изменении таблиц шаблонов всякий раз, когда память таблицы шаблонов читается или записывается, формат данных должен быть преобразован. Так как это случается гораздо реже (даже в играх, использующих CHR-RAM), это хорошая идея.
2.2. Индексированный регистр палитры VGA
Этот метод включает программирование регистров палитры VGA для отражения прямых значений, содержащихся в регистрах палитры PPU. Палитра VGA будет разделена на 64- по 4 цветовых палитры. Когда необходимо отрисовать последовательные горизонтальные пиксели, может произойти большая (32-битная или более) выборка данных таблицы шаблонов (пиксели для плиток таблицы шаблонов должны быть организованы в памяти так, чтобы 2 бита для каждого горизонтально последовательного пикселя хранились с шагом 8 бит). Затем этот фрагмент извлеченных пиксельных данных может быть замаскирован (чтобы другие пиксельные данные из фрагмента не использовались), к значению может быть добавлено индексированное значение «VGA palette select», и, наконец, затем может быть записано в виртуальный буфер кадров за одну операцию сохранения. Значение «VGA palette select» извлекается через таблицу выбора палитры VGA, которая соответствует 8 классическим палитрам PPU (4*2 элемента в таблице; поэтому данные атрибута плитки (PF или OBJ) используются в качестве индекса в этой таблице). Эта таблица указывает, какую 4-цветную группу из 64 групп в палитре VGA использовать для выбора цвета для группы записываемых пикселей. Идея заключается в том, что когда происходит изменение палитры в середине кадра (или в любое время, если на то пошло), затронутая палитра PPU в этой таблице изменяется, чтобы указать, где будут сделаны новые изменения палитры в палитре VGA. Соответствующие записи палитры VGA также должны быть обновлены соответствующим образом (обычно обновления палитры VGA производятся в режиме кольцевого буфера. Указатель, отслеживающий первые доступные 4 записи палитры, будет увеличиваться при изменении любых записей в 4-цветной палитре PPU).
По сути, этот метод предлагает самый быстрый возможный способ рендеринга графики NES, поскольку данные извлекаются из памяти таблицы шаблонов и записываются непосредственно в виртуальный буфер кадров. Количество одновременно обрабатываемых пикселей может достигать 8 (с инструкциями MMX). Однако количество возможных изменений палитры PPU в середине экрана ограничено 64 разами (или 32 для PF и 32 для OBJ, если один из битов в каждом пикселе должен использоваться для различения пикселя игрового поля от объекта), но несколько последовательных изменений одной палитры PPU из 4 цветов считаются только одной фактической модификацией.
2.3. Рендеринг на основе инструкций MMX
В 1995 году архитектура x86 была благословлена инструкциями MMX: набором инструкций типа RISC с одной функцией и несколькими данными, эти функции предоставляют возможность для решения большого количества современных логических задач. Почти все инструкции имеют очень низкую задержку в 1 или 2 тактовых цикла во всех процессорах класса x86, которые их поддерживают, поэтому эти инструкции очень желательно использовать. Инструкции работают вокруг 8-элементного (64 бита/элемент) плоского регистрового файла, который перекрывает устаревшие регистры мантиссы x87. БОЛЬШАЯ особенность использования инструкций MMX для рендеринга пикселей заключается в том, что одновременно можно обрабатывать 8 пикселей, при условии, что размер каждого пикселя не превышает байта. Следующая процедура, написанная на ассемблере, может полностью вычислить цвет пикселя для 8 горизонтально последовательных пикселей для каждой итерации цикла (пример фактически рендерит первые 4 строки развертки плитки).
Примечание: процессоры Pentium 4 и Athlon XP/64 поддерживают 128-битные версии инструкций MMX, поэтому это может позволить вам увеличить производительность гораздо больше, чем то, что уже предлагается алгоритмом, описанным ниже. Очень полезно для виртуальной многозадачности NES, когда 20 или более экранов NES должны быть анимированы и отображены одновременно в режиме экрана с высоким разрешением.
Таблицы шаблонов уже реорганизованы таким образом, что данные битовой карты для 4 строк развертки данных плитки могут быть загружены в регистр MMX и использованы максимально эффективным способом. Данные пикселей для 4 последовательных строк развертки под одной и той же горизонтальной координатой хранятся в одном байте, причем 2 старших бита содержат самую низкую логическую координату строки развертки. Последовательные байты, вплоть до 8-го, содержат данные пикселей для каждой последующей горизонтальной позиции. Наконец, первые 8 байтов данных таблицы шаблонов плитки содержат полные данные битовой карты для первых 4 строк развертки плитки, а следующие 8 байтов содержат последние 4 строки развертки.
; register assignments
;--------------------
;EAX - указатель пикселя назначения
;EBX - указывает на палитру, которая будет использоваться для этой плитки
; (в основном определяется поиском в таблице атрибутов)
;ESI - исходный указатель для 32-пиксельного растрового изображения, которое
; будет загружено из таблицы шаблонов
;MM4 - (8 - точное значение горизонтальной прокрутки)*8
;MM5 - (точное значение горизонтальной прокрутки)*8
; выбрать 32 пикселя из таблицы шаблонов, организованных как 8 по горизонтали
; и 4 по вертикали.
;--------------------
movq mm3,[esi]
mov ecx,-4; загрузить отрицательное количество циклов
; переместить константы, связанные с вычислением цвета, непосредственно в
; регистры. Они должны храниться в памяти, поскольку инструкции MMX не
; позволяют использовать непосредственные данные в качестве операнда.
@1: movq mm0,_C0x8; contains (содержит) C0C0C0C0C0C0C0C0h
movq mm1,_00x8; contains 0000000000000000h
movq mm2,_40x8; contains 4040404040404040h
; генерировать маски в зависимости от величины 2 старших битов в каждом упакованном байте (обратите внимание, что это сравнение со знаком).
pcmpgtb mm0, mm3
pcmpgtb mm1, mm3
pcmpgtb mm2, mm3
psllq mm3, 2; сдвиг битовой карты для доступа к следующей строке пикселей
; для поиска цвета используется предварительно вычисленная таблица палитры и
; выполняется операция ANDed с результирующими масками последней операции.
; Поскольку операции XOR используются для объединения результатов, для этого
; требуется, чтобы элементы в таблице палитры были объединены XOR со смежными
; значениями, чтобы они были отменены в конце логической обработки здесь.
; Требуемая предварительно рассчитанная комбинация XOR каждого элемента цвета
; показана в комментариях ниже соответствующим элементом. Обратите внимание,
; что размер каждого поиска составляет 8 байт; для этого требуется, чтобы
; одни и те же данные палитры для одного элемента отражались во всех 8
; последовательных байтах.
pand mm0,[ebx+00]; 2^3
pand mm1,[ebx+08]; 3^0
pand mm2,[ebx+16]; 0^1
pxor mm0,[ebx+24]; 1
pxor mm1,mm2
pxor mm0,mm1
; эта логика выполняет функцию сдвига, чтобы реализовать точную горизонтальную
; прокрутку. Альтернативой этому является простая запись 64 бит в
; невыровненные адреса для значений точной горизонтальной прокрутки, отличных
; от нуля, но поскольку это может привести к большим потерям на современных
; процессорах, то данный вариант, как правило, предпочтительный способ
; создания эффекта точной горизонтальной прокрутки.
movq mm1,mm0
psllq mm0,mm4
psrlq mm1,mm5
por mm0,[eax]
movq [eax+8],mm1
movq [eax ],mm0
; техническое обслуживание контура
add eax, LineLen; переместить указатель пикселя на следующую
; позицию строки сканирования
inc ecx
jnz @1
Примечание переводчика: обратите внимание, что используются «старые» регистры mm0-mm7, а не xmm0-xmm7.
Чтобы использовать рендерер, укажите EAX на начало вашего буфера рендеринга (из-за того, как работает точная горизонтальная прокрутка, плитки должны рендериться рядом друг с другом, увеличиваясь вдоль горизонтальной оси плитки). Без какой-либо уродливой дополнительной логики буфер рендеринга должен быть увеличен в размере на 8 пикселей на строку сканирования, чтобы обеспечить дополнительную выборку шаблона плитки, необходимую всякий раз, когда значение точной горизонтальной прокрутки не равно нулю. После того, как процедура будет выполнена достаточное количество раз для заполнения вашего буфера рендеринга, считайте, что начальные горизонтальные координаты рендеринга игрового поля должны быть смещены на 8 пикселей из-за необходимой «области вытекания» для случая, когда первый шаблон плитки для этой строки должен быть смещен за пределы экрана.
2.4. Предсказание ветвления
Процессоры Pentium MMX и более поздние процессоры имеют улучшенное аппаратное обеспечение предсказания ветвлений по сравнению с оригинальным Pentium и, следовательно, могут правильно определять шаблон условия ветвления, пока условие не остается тем же самым более 4 раз подряд. Новая система основана на отслеживании последних 4 известных условий для любого ветвления, которое может быть выделено в BTB (Branch Target Buffer). Эти 4 бита используются для индексации 16-элементной таблицы для извлечения 2 бит, которые указывают на предсказанное условие ветвления (строго принято, принято, не принято, строго не принято), которое затем записывается обратно после использования насыщенного сложения для увеличения или уменьшения значения на основе фактического условия ветвления, полученного из программы.
-
Вышеуказанный рендерер на основе MMX требует 4 или менее итераций цикла для рендеринга плиток. Это количество циклов очень подходит для эффективного выполнения на современных процессорах. Пока это количество циклов остается относительно постоянным во время рендеринга игрового поля и всегда меньше 5, должно происходить очень мало неверных предсказаний.
-
Не изменяйте приведенный выше алгоритм для рисования полной 8-строчной плитки. Вместо этого используйте другой счетчик циклов, чтобы повторно использовать код рендерера, когда нужно нарисовать больше 4-строчных блоков плитки.
-
Старайтесь поддерживать буфер рендеринга не менее 32 строк развертки. Этого размера достаточно, чтобы гарантировать, что счетчик рендеринга строки сканирования для объектов самого большого размера может оставаться постоянным на уровне 16 на протяжении всего рендеринга (при условии, что игра не делает ничего, чтобы нарушить непрерывность рендеринга объектов), и это поможет избежать ошибочных прогнозов ветвления в цикл рендеринга объектов.
3. Объединение пикселей игрового поля и объектов
Самый эффективный способ эффективно объединить данные игрового поля и объекта в ваш финальный отрендеренный кадр — это всегда сначала отрендерить ваше игровое поле (или его часть, в случае работы с разделенным экраном) непосредственно в сам буфер изображения. На этом этапе, чтобы эффективно объединить пиксели объекта с игровым полем, каждый пиксель в вашем буфере изображения должен иметь дополнительные 2 бита, связанные с ним, один из которых будет представлять статус прозрачности для пикселя игрового поля, а другой — то же самое, за исключением пикселей объекта (когда они будут отрисованы позже).
Естественно, после рендеринга игрового поля в буфере изображения не будет пикселей со статусом прозрачности для пикселей объекта, помеченных как False. Но теперь, когда объекты рендерятся, условие, при котором фактический пиксель отрисовывается, зависит от этих двух битов статуса прозрачности, собственного статуса прозрачности объекта и его приоритета. Начиная в порядке от объекта 0 (самый высокий приоритет) до 63, битовые карты объектов «объединяются» с игровым полем, таким образом, как покажут следующие несколько строк псевдокода:
F (SrcOBJpixel.xpCond=FALSE) THEN
IF ((DestPixel.OBJxpCond=TRUE) AND ((DestPixel.PfxpCond=TRUE) OR
(SrcOBJpixel.Pri=foreground))) THEN
DestPixel.data := SrcOBJpixel.data
FI
DestPixel.OBJxpCond := FALSE
FI
Итак, как видите, OBJxpCond адресата помечен как false, даже если пиксель объекта не предназначен для рисования. Это делается для предотвращения отрисовки объектов с более низким приоритетом (числово более высоким номером) в этих местах.
Это может вызвать вопрос: «Почему вы визуализируете объекты в порядке 0-> 63 (фактически требуя 2 бита для статуса прозрачности), когда вы можете визуализировать их в противоположном направлении (что требует только 1 бит для статуса прозрачности)? " Ответ заключается в том, что происходит при конфликте приоритетов (см. раздел «PPU pixel priority quirk» в документе «2C02 technical reference»). Рендеринг объектов в порядке 0->63 — единственный способ правильно эмулировать эту функцию PPU (и некоторые игры ДЕЙСТВИТЕЛЬНО зависят от этой функциональности, поскольку она позволяет заставить игровое поле скрывать пиксели объектов с приоритетом переднего плана). В противном случае (для 63->0) было бы необходимо объединить объекты в буфер изображения, заполненный текущим цветом прозрачности, а затем также объединить данные игрового поля с буфером. Конечно, для этого метода потребуется только 1 бит состояния прозрачности (приоритет фона) на пиксель, но поскольку операции слияния медленные, а этот метод требует их гораздо больше, этот метод уступает вышеупомянутому.
3.1. Другие советы
-
В зависимости от реализации пиксельного рендеринга вы можете хранить 2 бита состояния прозрачности внутри самих данных пикселя. Например, если генерируется только 52 комбинации отрендеренного пикселя, для этого хранения можно использовать верхние 2 бита в байте пикселя. Это может означать, что вам придется отразить информацию RGB регистра палитры вашего видеобуфера 4 раза, но в остальном это хорошая идея. Для 8-битных цветных режимов VGA устаревший регистр маски (3C6h) позволяет программисту маскировать любые биты записанных данных пикселя, которые не связаны с генерацией цвета.
-
Не используйте ветвление, чтобы где-то не отрисовывать пиксель. Во-первых, он позволяет обрабатывать только 1 пиксель за раз, что очень медленно. Во-вторых, ЦП с трудом предсказывает ветвления на основе случайных данных (или, как минимум, данных, которые создают шаблон ветвления, который слишком длинный для хранения в целевых буферах ветвлений ЦП). Наконец, последовательности арифметических и логических операций SIMD могут использоваться для одновременного объединения нескольких байтов данных (особенно с инструкциями MMX).
-
Избегайте невыровненного доступа к памяти любой области данных, используемой вашими процедурами рендеринга. Каждое невыровненное хранилище влечет за собой минимальный штраф в 3 такта на 486 и гораздо больше тактов на современных процессорах. Как правило, код сдвига и слияния, необходимый для выравнивания данных, которые могут храниться на любой битовой границе, не займет более 5 тактов на любом процессоре. (Пример с кодом MMX, показанный ранее, демонстрирует, как выполнить операцию сдвига и слияния.)
-
Используйте встроенный код в небольших циклах с постоянным числом итераций, особенно если количество циклов невелико и является самым внутренним. Это снижает накладные расходы, избегая обратного перехода и требуемых счетчиков циклов и индексов. Например, при рисовании тайлов было бы неплохо встроить код для отрисовки 8 пикселей по горизонтали.
4. Оптимизация хранилища кадров
Один из самых простых подходов к эмуляции рендеринга — отрисовать все игровое поле в видеобуфере, а затем поместить соответствующие данные объекта в буфер (это упрощает расчет решений относительно пикселей объекта/игрового поля, поскольку пиксели игрового поля уже отрисованы). Этот прямой подход также был бы наиболее эффективным способом работы с рендерингом графики NES, если бы не определенные предостережения по использованию видеобуфера:
-
Чтение буфера видеокадров мучительно медленное. Независимо от того, насколько быстра видеокарта компьютера, чтение видеопамяти будет как минимум в 10 (десять!) раз медленнее, чем запись в нее. Это повлияет на время рендеринга объектов, когда содержимое игрового поля, которое перекрывает объект, должно быть считано и объединено с пикселями объекта.
-
Запись в случайные места видеопамяти мучительно медленная. Поскольку современные устройства ввода-вывода (PCI, AGP) в ПК имеют общие адресные линии с линиями данных (по одной и той же шине), при записи в случайное место в видеопамяти возникают накладные расходы. Эта идея была разработана с учетом потоковой передачи данных, поэтому, когда происходит последовательная запись (довольно распространенное явление), линии шины, которые в противном случае были бы потрачены впустую на отслеживание последовательно увеличивающегося адреса, теперь могут использоваться для передачи данных. Получается, непоследовательная передача данных на видеокарту может занять вдвое больше времени, чем последовательная передача. Один только этот момент делает рендеринг игрового поля для каждого тайла (где вы в основном сохраняете только 8 последовательных пикселей за раз для каждой строки развертки тайла) непосредственно в видеобуфер — один из худших подходов. Последовательная передача данных на видеокарту близка к оптимальной — 256 байт за раз и выше.
-
Запись в случайные невыровненные места в видеопамяти невероятно медленная. Это связано с тем, что на аппаратном уровне должна выполняться операция слияния: чтение с видеокарты (которая и так медленная) и запись. Однако эта операция требуется только в начале и в конце невыровненной последовательной передачи. Таким образом, невыровненная потоковая передача данных на видеокарту имеет меньше штрафа, чем больше размер передачи (я измерил дополнительные накладные расходы на 11% при невыровненном последовательном сохранении 512 байтов в буфер видеокарты и удвоил этот показатель для 256-байтового xfer). Адреса видео, которые делятся на 64 байта, считаются выровненными.
-
Запись в видеопамять небольшими (побайтовыми) операциями сохранения — плохая идея. Хотя современное аппаратное обеспечение ЦП/чипсета ПК может обнаруживать и объединять несколько небольших последовательных сохранений в видеобуфере как полноразмерные, нет никакой гарантии, что это произойдет. Старое оборудование, конечно, этого не делает. И, если небольшие записи не объединяются вместе, то угадайте что? Чипсет выполнит операцию слияния для каждого переданного элемента данных, который не является полным размером. Очевидно, что это может быть наихудшим возможным способом отправки данных в видеобуфер (ну, после хранения небольших данных в случайных местах видеопамяти).
-
Запись в нелинейный буфер кадров (LFB) медленная. По крайней мере на одной карте, которую я тестировал, наблюдалось увеличение скорости записи видеобуфера на 333% после перехода с использования устаревшей по адресу $000A0000. Я понимаю, что в принципе любая видеокарта PCI имеет возможности LFB, но может быть недоступна из-за BIOS или драйверов. Я предполагаю, что это действительно ответственность ОС, но в любом случае: используйте LFB любым доступным способом.
Теперь вы должны увидеть, что это просто не очень хорошая идея — рендерить графику напрямую в видеобуфер (хотя я не думаю, что кто-то это сделает, в любом случае). Старые версии этого документа обсуждали использование виртуального буфера кадров, который по сути был буфером, выделенным в обычной памяти, используемым для рендеринга графики (вместо непосредственного видеобуфера). Когда виртуальный буфер заполнялся, он затем копировался в видеобуфер большой последовательной операцией (что как раз подходит для видеокарты!). Однако этот метод на самом деле довольно неэффективен, как объясняется далее.
4.1. Краткая информация о встроенных кэшах x86
Если вы знаете, как работает виртуальная память, то кеш ЦП в основном похож на аппаратную версию этого, хотя и не для диска, а для основной оперативной памяти системы. ЦП кэширует данные на (так называемой) построчной основе. Каждая строка имеет размер от 16 (486) до 32 (Pentium) и 64 (Athlon) байт, и, скорее всего, в будущих процессорах она будет увеличиваться. Таким образом, если из памяти нужно прочитать только один байт, то в кеш всё равно загружается целая строка (вот почему выравнивание данных и группировка связанных полей данных вместе в записях важны для обеспечения эффективности кеша ЦП). Это действие также выталкивает из кеша другую строку (в идеале наименее использовавшуюся), и если она была изменена, то она будет записана обратно в основную память.
486-е начали тенденцию кэширования процессоров x86 на кристалле, с целыми 8 Кбайт, общими для данных и кода. Модели Intel 486DX4 имели 16 Кбайт. У Pentium были отдельные кэши по 8 Кбайт, каждый для данных и кода. Процессоры x86 6-го поколения снова удвоили размер кэша на кристалле (хотя сохранили раздельную архитектуру кэширования кода/данных, начатую Pentium). Дело в том, что размер кэша (уровня 1) по сути является размером памяти, к которой процессор может произвольно обращаться в течение наименьшего возможного времени. Даже для 486 это означает до 8 Кбайт кэшируемых структур данных, что на самом деле может быть довольно большим объемом памяти, если программное обеспечение написано аккуратно.
Процессоры x86 с кэшем 2-го уровня на чипе (представленные с ядром Intel Celeron второго поколения) эффективно расширяют объем кэшируемых данных, хранящихся в процессоре, при этом иногда скрывая задержки доступа, спекулятивно загружая кэшированные структуры данных 2-го уровня в кэш 1-го уровня, когда алгоритм кэширования считает, что данные будут использованы программным алгоритмом очень скоро. Хорошим примером этого может служить процедура, которая выполняет последовательные операции с большим массивом памяти.
Секрет эффективного использования кэша заключается в том, как пишется программное обеспечение. Лучше всего писать программные алгоритмы, которые работают с объемом временной памяти, меньшим, чем размер кэша уровня 1 ЦП. Даже вычислительные алгоритмы, которые, по-видимому, требуют большого объема памяти, иногда можно разбить на подалгоритмы, чтобы уменьшить требуемый объем временной памяти. Хотя такой подход и влечет за собой небольшие накладные расходы на загрузку/сохранение, важнее, чтобы ваши данные оставались в кэше любым возможным способом. Эти рекомендации практически гарантируют, что ваше программное обеспечение будет работать максимально эффективно на любом ЦП с внутренним кэшем.
4.2. Предостережение о виртуальном буфере кадров
Давайте рассмотрим модель виртуального буфера кадров (VFB). Начинаем рендеринг нашего игрового поля. Доступ к таблицам имен и таблицам шаблонов осуществляется, и это нормально (таблицы имен легко кэшируются, и даже некоторые данные таблиц шаблонов кэшируются). Затем мы сохраняем наши отрисованные пиксельные данные в нашем буфере. Пиксельные данные сохраняются в VFB с использованием размера данных 4 байта (или, в противном случае, максимального размера, который процессор позволит программисту хранить). Однако размер строки кэша ЦП всегда больше этого, и поэтому ЦП выполняет операцию слияния с записанными данными и строкой кэша данных, в которую записываются.
Теперь вот первая проблема: цель операции сохранения в VFB вряд ли находится в кеше. Это означает, что ЦП фактически *читает* основную память после вашего первого 4-х байтового хранилища пикселей. Конечно, теперь вы можете писать в эту строку бесплатно, но доступ к основной памяти медленный, и, учитывая то, что мы здесь делаем (то есть исключительно операции сохранения), это как-то нелепо, что программист не может сообщить процессору что операция слияния (и тем более чтение основной памяти) не нужна, так как мы планируем перезаписать все исходные данные в этой конкретной строке (расширения инструкций MMX, представленные в процессорах Pentium 2 и более поздних версиях, предлагают разумные способы работы с невременной памятью).
В любом случае, вы поняли идею: после каждых нескольких сохранений в VFB новая строка из VFB будет считываться из основной памяти (или кэша уровня 2, если он там есть). Но угадайте что? Это даже не худшая часть. По мере того, как вы продолжаете заполнять VFB, кэш вашего ЦП переполняется, поскольку кэш L1 вашего ЦП меньше, чем VFB, с которым вы работаете. Это означает, что ваш рендеринг VFB в конечном итоге вытеснит из кэша все строки, которые не используются напрямую процедурой рендеринга (вызывая потерю циклов даже для локальных процедур, которым они могут понадобиться сразу после рендеринга), но и после рендеринга, когда вы копируете VFB в видеопамять, весь буфер должен быть загружен обратно в кэш ЦП.
Конечно, размер кэша ЦП здесь решает все. Из-за последовательных шаблонов доступа модели рендеринга виртуального буфера кадра этот алгоритм может на самом деле скромно работать на ЦП с большим кэшем уровня 2 на кристалле (из-за спекулятивной загрузки данных из кэша уровня 2 в кэш уровня 1). Однако я не могу сказать, что знаю, какие потери производительности могут возникнуть при запуске этого алгоритма на ЦП с внешним кэшем уровня 2. Поэтому в целом я бы не рекомендовал использовать модель алгоритма виртуального буфера кадра, предназначенную для ЦП без кэша уровня 2 на кристалле объемом не менее 128 КБ.
4.3. «Хранилища строк сканирования»
Уменьшая размер VFB с полного размера до нескольких строк развертки (или даже до одной), можно избежать большинства или всех упомянутых оговорок. Поскольку обычно строка развертки VFB составляет 256 байт (в примере для PPU NES), это делает требования к памяти достаточно малыми, чтобы обеспечить хорошую производительность даже на 486.
Конечно, это создает новую проблему для написания движка рендеринга PPU — плитки больше не могут быть отрисованы полностью (если только вы не используете VFB с 8 строками развертки, но в остальной части этой темы предполагается, что вы используете только VFB с одной строкой развертки). Некоторых накладных расходов, вызванных отрисовкой только одной строки развертки плитки за раз, можно избежать, предварительно вычислив работу указателя для каждой последовательной плитки и сохранив ее в массиве, чтобы вычисления можно было повторно использовать для других строк развертки плитки. Подобный метод можно использовать и для вычислений указателя объекта.
Вероятно, возможное повышение производительности, которое можно получить за счет тщательной разработки механизма рендеринга строк развертки, заключается в том, что можно разрешить сохранение обработанных пикселей игрового поля непосредственно в видеобуфере, поскольку все пиксели в строке развертки игрового поля могут визуализироваться последовательно и, следовательно, могут быть сохранены таким образом. Однако существуют условия, которые определяют эффективность этого.
Первое - работать с пикселями объекта, которые перекрывают области любой строки развертки игрового поля, будет очень сложно (без использования хотя бы буфера строки развертки), поскольку рендеринг тайлов игрового поля обычно выполняется последовательно, в то время как тайлы объектов обычно должны рендериться в случайной горизонтальной плоскости. координаты на одной и той же строке сканирования (чтобы правильно эмулировать приоритеты объектов).
Второе - условие зависит от выравнивания. Если точное смещение горизонтальной прокрутки PPU делится без остатка на размер, используемый для хранения пиксельных данных в буфере кадра, то выравнивание не является проблемой. Однако в случае, если это не так (а это будет происходить часто, поскольку почти все игры NES используют плавную горизонтальную прокрутку), то следует использовать метод сдвига и объединения пикселей в регистрах CPU для эффективного выполнения плавной горизонтальной прокрутки, чтобы избежать невыровненного хранения данных и непростительного штрафа, связанного с выполнением этого действия непосредственно в буфере кадра.
4.4. Преодоление «letterboxed» в дисплеях.
Поскольку NES не использует никаких сложных функций для генерации своей графики (таких как наклон, сдвиг, поворот, поворот, вращение или масштабирование), сглаживание никогда не было важным для пиксельной эмуляции графики NES. Однако из-за странной природы разрешений VGA, чтобы избежать отображения игрового экрана NES в виде «letterboxed» (то есть, когда по бокам есть большие черные границы неиспользуемой области экрана), вам придется либо масштабировать эмулируемую графику самостоятельно, либо найти способ заставить аппаратное обеспечение видеоадаптера делать это.
Для масштабирования графики, предназначенной для отображения на мониторе компьютера, сглаживание крайне важно, чтобы гарантировать, что требуется только минимальное разрешение экрана, чтобы артефакты (т. е. искаженные или асимметричные пиксели) были максимально неразличимы для геймеров. Соотношение 5 целевых пикселей к 2 исходным пикселям может использоваться для растяжения 256 исходных пикселей до 640 целевых (очень распространенное горизонтальное разрешение VGA). Для расчета цвета для среднего пикселя из 5 необходимо усреднить два исходных значения цвета. Обратите внимание, что для этого пиксели должны быть чистыми значениями RGB (в отличие от значений индекса палитры). Другие разрешения VGA, например 512*384, также могут оказаться полезными.
5. Плавное воспроизведение звука
В этой главе описываются способы улучшения звуковой эмуляции NES.
5.1. Обзор
Очень немногие эмуляторы NES эмулируют операции звукового канала с такой точностью, с какой это делает NES, и в результате эмуляция некоторых высокочастотных прямоугольных и шумовых волн, которые часто производят многие игры NES, в конечном итоге будет звучать так, как будто в звуке есть артефакты (т. е. присутствуют две или более очевидных частот, хотя должна быть слышна только одна частота). Увеличение частоты воспроизведения сэмплов может решить эту проблему, но в конечном итоге частоты сэмплирования на звуковых картах, имеющихся в ПК и подобных устройствах, могут быть только такими высокими (только определенная частота).
5.2. Почему на высоких частотах есть артефакты?
Каждый звуковой генератор NES имеет скорость обновления/разрешение аудиовыхода 1,79 миллиона выборок в секунду (приблизительно). По сравнению со средней скоростью окупаемости Sound Blaster (44100 Гц) это означает, что звуковые каналы NES имеют разрешение 3125/77, или в 40 и 45/77 раз больше, чем разрешение сэмпла. Таким образом, когда всего один расчетный сэмпл PCM должен представлять 40,6 от звуковых каналов NES (в том же временном интервале), неудивительно, что звук звучит так ужасно на высоких частотах: примерно 39,6 сэмпла исходного звука были пропущены и предположительно все равны единой выборке.
PCM — импульсно-кодовая модуляция.
5.3. Решения
Sound blasters имеют аппаратное обеспечение, позволяющее прозрачно преодолеть это для пользователя, когда требуется цифровой захват аудиосигнала. Доказательством служит сэмплирование музыки NES с частотой 44100 Гц, 16 бит/сэмпл: нет никакой различимой разницы между тем, как звучит аналоговый звук, сгенерированный в реальном времени с NES, по сравнению с цифровым сэмплом. Они либо используют примитивные схемы RC-интегратора на входах своих АЦП для аппроксимации накопленного во времени среднего напряжения между сэмплами АЦП, либо они сэмплируют сигнал во много раз быстрее, чем выходная частота дискретизации PCM (примерно 2^n кратно), и используют цифровое усредняющее оборудование для получения каждого «пониженного» результата PCM. Вот еще, любезно предоставлено ветераном NESdev:
Я предлагаю вам сделать это с высокой частотой дискретизации, кратной степени 2 выходной частоты, например, 4*44100 = 176400 выборок в секунду. Вы бы сложили все четыре выборки вместе и поделили на четыре (понижение частоты выборки), и это была бы ваша выходная выборка.
Предположим, что амплитуда вашей волны равна 1. Вот несколько примеров генерации одной выходной выборки:
Пример 1
Oversample Results: 1, 1, 1, 1
Downsampled Output: (1 + 1 + 1 + 1) / 4 = 4 / 4 = 1
Пример 2
Oversample Results: 1, 1, -1, -1
Downsampled Output: (1 + 1 + -1 + -1) / 4 = 0 / 4 = 0
Пример 3
Oversample Results: 1, -1, 1, 1
Downsampled Output: (1 + -1 + 1 + 1) / 4 = 3 / 4 = 0.75
Поэтому ваши выходные выборки не всегда будут просто 1 или -1. Вы на самом деле повышаете частоту выборки, а затем преобразуете результаты обратно в выходную частоту выборки.
5.4. Простая реализация прямоугольного канала
Простые звуковые каналы, такие как прямоугольные волны, могут быть спроектированы для аппроксимации точного выходного сигнала канала без необходимости прибегать к каким-либо методам понижения частоты дискретизации.
-
Используйте счетчик длин волн на основе целого числителя для уменьшения на 40 и 45/77 после рендеринга каждого образца PCM; это имитирует прошедшее время в обычных тактовых циклах ЦП 6502, которое проходит между воспроизведением образцов PCM с частотой 44100 Гц.
-
Когда счетчик "wavelength.whole" становится отрицательным (счетчик истекает), это означает не только то, что выход прямоугольной волны переключился где-то в середине временного интервала выборки PCM, но и то, что выходной объем будет масштабироваться на основе того, сколько циклов выход канала был положительным в течение временного интервала выборки PCM. Для расчета этого можно использовать оставшееся значение в счетчике длин волн.
-
Если оставшееся значение длины волны представляет волну, пока она положительна, то значение «wavelength.whole» можно заменить на отрицательное; в противном случае добавьте к нему 40 и 45/77.
-
Чтобы рассчитать окончательную выборку выходного сигнала PCM, просто измените уровень громкости канала на отношение между скорректированным счетчиком длины волны и 40 и 45/77.
-
Предостережение: выходные прямоугольные формы волн не могут менять состояние более одного раза на произведенный образец PCM, и это делает точную эмуляцию длин волн менее 40 и 45/77 тактовых циклов напрямую невозможной с помощью этого алгоритма. Однако длины волн, которые ниже этого значения, могут быть увеличены за счет абсолютной разницы двух значений, чтобы создать картину выходной волны, аналогичную фактической, которая будет создана. Однако, как правило, эти частоты не могут быть услышаны человеком, и поэтому точная реализация не так важна, если вообще необходима.
5.5. Другие примечания
-
Всегда представляйте счетчики, не основанные на целых числах (например, те, которые должны увеличиваться на числа, такие как 40 и 45/77ths) с помощью целых чисел, сгруппированных по принципу рациональных числителей и знаменателей, а не используйте числа с плавающей точкой для представления отношения. Хотя числа с плавающей запятой могут быть очень точными, из-за того, что шаблоны битов рациональных чисел повторяются вечно, точность вычислений никогда не гарантируется на 100%, и это делает последовательные вычисления на основе вычисленных данных плохой идеей. Однако счетчики с целыми числителями можно увеличивать с помощью целочисленных дельта-значений, чтобы гарантировать отсутствие потери точности арифметических вычислений. Наконец, эти действия следует выполнять, если числитель становится численно больше знаменателя после операции увеличения:
* уменьшить значение счетчика числителя на знаменатель.
* увеличить счетчик целых чисел.
-
Убедитесь, что вы используете информацию о подсчете циклов, передаваемую в процедуры эмуляции звукового оборудования из ядра ЦП, для воздействия на выходные сигналы звукового канала в правильные моменты времени в эмулируемом кадре. Это означает, что обновления работы звукового канала *не* должны осуществляться покадрово, даже несмотря на то, что этот метод работает для большинства музыкального кода игр NES. Многие записи в регистры звукового канала вступают в силу почти сразу после записи, и, по-видимому, некоторые игры NES фактически используют преимущества синхронизированного кода звукового порта для создания некоторых действительно приятных звуковых эффектов. Кроме того, для эмуляторов, которые поддерживают больше обычного количества 6502 тактов на кадр, звуковое оборудование должно игнорировать любые такты, превышающие 29780 и 2/3 относительно того, когда в последний раз запускалась основная процедура звуковой анимации игры (предполагая, что для звуковой анимации используются NMI на основе PPU, но иногда для этого используется счетчик кадров 2A03).
6. Методы декодирования и выполнения инструкций 6502
6.1. Способы эмуляций
-
Эмуляция на основе компонентов инструкций.
Эта базовая модель разбивает все коды операций 6502 всего на два компонента: режим адресации и операция ALU. Поскольку режимы адресации и операции ALU объединяются для создания всех кодов операций 6502, кажется, имеет смысл эмулировать коды операций 6502 на этой основе. В результате нужно будет кодировать только основные основные процедуры 6502, и это не только сэкономит много памяти кода, но и упростит реализацию. Кроме того, этот метод лишь немного медленнее подхода с обработкой кодов операций из-за дополнительного скачка в процессе декодирования инструкций, но это компенсируется производительностью кэша центрального процессора из-за более эффективного использования структур кода. В целом, этот метод обеспечит наилучшую сбалансированную производительность для любой платформы ПК. -
Интерпретация кода операции 6502 на основе инструкций.
В этой модели ядра ЦП извлеченные коды операции 6502 используются в качестве индекса в таблице переходов из 256 элементов, где каждая цель перехода указывает на встроенную процедуру, которая обрабатывает все действия 6502 для имитации этой инструкции. Эта модель ЦП является самой популярной, так как ее проще всего реализовать, и она может быть достаточно быстрой, в зависимости от того, насколько хорошо написаны обработчики кодов операций (встраивание подпрограмм и развертывание любых циклов, содержащихся в обработчиках кодов операций, будет важно для быстрой эмуляции). Единственным реальным недостатком этой техники является то, что она не очень оптимально использует область хранения памяти, так как многие последовательности кода в обработчиках кодов операций придется дублировать десятки раз. Это приведет к некоторому снижению производительности на тех ЦП, у которых кэши кода L1 меньше (16 КБ или меньше). -
Динамическая перекомпиляция кода операции 6502.
В этой модели ядра ЦП коды операций 6502 декодируются, но вместо эмуляции поведения ЦП с помощью подпрограмм генерируется и выполняется машинный код ЦП, зависящий от платформы, на основе декодированной инструкции. В конечном итоге все коды операций 6502 будут транслироваться и кэшироваться в карте памяти эмулятора, при условии, что ядру будет предоставлено достаточное время обработки для обработки всего кода 6502, который оно когда-либо может выполнить. Пропускная способность выполнения перекомпилированных инструкций 6502 на самом деле может быть выше, чем на самом реальном 6502, при условии, что программист хорошо поработает над реализацией оптимизаций в перекомпилированных инструкциях (т. е. требование включения кода обслуживания флагов для большинства перекомпилированных инструкций не является обязательным, поскольку на них полагаются только инструкции ветвления и сложения/вычитания. Другая оптимизация может быть возможна за счет использования таблиц тактовых циклов для сегментов кода 6502 (код, который определен между целями ветвления или инструкциями PC xfer), чтобы также исключить инструкции обслуживания тактовых циклов в части перекомпилированного кода). Предостережения этой модели ядра ЦП (помимо очень сложной реализации архитектуры) включают требование больших объемов оперативной памяти (несколько или более мегабайт) и другие сложности, которые возникают, когда программа 6502 часто изменяет свой собственный код (хранящийся в оперативной памяти), который уже был транслирован и кэширован ядром ЦП. Однако для многозадачности десятков, а то и сотен приложений NES на одном современном компьютере динамическая перекомпиляция — единственный выход. -
Интерпретация кода операции 6502 на основе микрокода.
В этой модели ядра ЦП при извлечении кода операции 6502 байт используется в качестве индекса в таблице из 256 элементов, содержащей короткий список указателей подпрограмм для каждого элемента, которые представляют действия, которые будет выполнять движок 6502 на каждом такте, для которого выполняется инструкция кода операции. Эти последовательности микрокода повторно используются в разных кодах операции в разных комбинациях, чтобы сформировать действия, которые выполняет один код операции. Требуется гораздо меньше инструкций микрокода, чем инструкций кода операции, и это снижает сложность ядра. Вращая события, происходящие в ядре 6502, вокруг таблицы микрокода, вы можете сделать возможным для новой инструкции 6502 (т. е. старой "jam") изменять таблицу, так что будущие приложения NES смогут программировать в своих собственных, более полезных и эффективных инструкциях 6502, чтобы улучшить скорость и качество игры NES. Из-за гранулярного выполнения тактового цикла эта модель эмулятора более объектно-ориентирована, чем любая другая, и обеспечивает максимально близкую симуляцию событий, происходящих в реальном 6502 (сюда входит простая и логичная реализация всех мертвых циклов инструкций 6502). Однако с точки зрения средней скорости эмуляции эта техника сильно отстает от других.
6.2. Другие советы
-
Некоторые игры для NES полагаются на дополнительный цикл фиктивного сохранения, который инструкции RMW выполняют на 6502. Обычно это делается для подачи небольшого импульса в порт маппера с помощью одной инструкции RMW. Другие "функции" 6502 (даже недокументированные коды операций) также могут предполагаться реализованными в главном процессоре для игры NES (или иногда коды/патчи game genie), поэтому не пропускайте никаких деталей во время реализации ядра. Для получения дополнительной информации ознакомьтесь с документом "2A03 technical reference".
Примечание переводчика: RMW - read-modify-write. -
Внедрите в свой механизм 6502 счетчик тактовых циклов, который будет поддерживаться при выполнении каждой инструкции 6502. Этот счетчик в основном будет использоваться PPU для определения того, как запись по времени повлияет на то, как будет отображаться выходное изображение. Однако, если он используется также в качестве терминального счетчика, по истечении срока отсчета управление программой может быть передано обработчику, первоначально запрашивающему операцию подсчета (например, для генерации сигнала PPU VINT/NMI). Кроме того, не забывайте, что вы можете управлять любым количеством «виртуальных счетчиков циклов», даже не заставляя ядро ЦП поддерживать более одного физического счетчика. Аппаратное обеспечение NES может иметь несколько счетчиков, генерирующих IRQ, работающих одновременно, но порядок, в котором каждый из них вызовет IRQ, всегда известен эмулятору, поэтому регистр счетчика циклов должен быть запрограммирован только на значение счетчика для возникновения следующего IRQ (после чего следующий счетчик, который должен истечь, может быть загружен в регистр счетчика циклов).
-
Поскольку инструкции 6502 обычно требуют обновления регистра P (состояние процессора или флаги) после операции ALU, инструкции ALU x86 (или, в противном случае, другого платформенно-зависимого ЦП) обновляют свой регистр флагов аналогичным образом. Поэтому после эмуляции поведения ALU инструкции 6502 с помощью инструкции x86 используйте инструкции типа "LAHF" или "SETcc" для получения статуса кодов состояния знака, нуля, переноса и переполнения. Кроме того, пусть ваш эмулятор сохранит флаги 6502 в том формате, в котором они хранятся на ЦП x86. Таким образом, флаги не придется форматировать, что сэкономит время. Единственный раз, когда флаги придется преобразовывать в/из порядка 6502, это когда выполняются инструкции 6502 PHP, PLP, BRK #xx, RTS и аппаратные прерывания. Поскольку они встречаются гораздо реже, чем более распространенные арифметические и логические инструкции, обрабатывать флаги таким образом более эффективно.
-
Используйте регистры ЦП, специфичные для платформы, для хранения некоторых часто используемых регистров указателей 6502, если это возможно, так как это уменьшает зависимости загрузки/сохранения и блокировки генерации адресов (AGI) в программном коде эмуляции. Это в основном включает внутренние регистры PC, S, X, Y и TMPADDR 6502.
-
6502, по-видимому, имеет около 12 опкодов, которые заклинивают машину (процессор). Эти опкоды идеально подходят для реализации расширений набора инструкций эмулятора 6502 для целей ловушки/отладки.
7. Декодирование адреса эмуляции
Декодирование адреса эмуляции берет сформированный адрес 6502, плюс статус чтения/записи 6502, и пропускает его через (чаще всего) статическую логику для определения метода доступа и эквивалентного эмулятору адреса памяти, которому соответствует этот адрес 6502, чтобы правильно эмулировать доступ к этой ячейке памяти. При таком подходе эти декодированные адреса в вашем эмуляторе можно рассматривать либо как прямой указатель на данные, либо как указатель на подпрограмму, которую ядро ЦП вызывает, когда необходим дополнительный код для точной эмуляции событий этого тактового цикла 6502. Наиболее известная техника для этого обсуждается ниже.
Использование таблиц декодирования адресов 1:1 для чтения и записи карт памяти 6502 является самым быстрым и точным способом определения, где находится область памяти NES и какому адресу она соответствует. Обычно байт должен использоваться как один элемент в картах памяти для представления типа области памяти (до 256 типов для каждой таблицы), и у вас будет 128 КБ из них, поскольку строка R/W 6502 также используется во время вычислений адреса. Несмотря на то, что эта «technique _seems_ to» тратит много памяти, таблицы декодирования памяти чаще всего доступны параллельно с областями памяти, содержащими структуры NES ROM и RAM, и это означает, что кэшированные структуры данных, находящиеся в главном процессоре эмулятора (из-за имитированных передач шины памяти 6502), обычно никогда не потребуют памяти больше, чем в два раза больше обычного. Это небольшая цена за то, чтобы гарантировать, что адаптация вашего ядра 6502 к любой иностранной архитектуре/технологии NES/FC будет такой же простой, как добавление нескольких новых обработчиков типов областей памяти в ядро вашего эмулятора, а затем создание новой таблицы декодера адресов.
8. Аппаратная очередь портов
Аппаратная очередь портов позволяет ЦП записывать (а иногда даже считывать) данные, предназначенные для аппаратного порта в карте памяти виртуального 6502, без необходимости прерывать эмуляцию ЦП для вызова процедуры/обработчика эмуляции аппаратного порта. Это возможно за счет буферизации записи в определенный порт, так что процедура аппаратной эмуляции может обрабатывать данные позже (т. е. не в том порядке, в котором их выдает ядро ЦП).
Плюсы:
-
передача программного управления исключается, когда ядро ЦП обращается к общим аппаратным портам. Это, в свою очередь, уменьшает промахи кода и кэша данных и, особенно, неправильные прогнозы ветвления в физическом ЦП, на котором работает программное обеспечение эмуляции.
-
динамическое добавление аппаратных устройств к карте виртуальной памяти ядра ЦП станет проще благодаря архитектурным усовершенствованиям, которые требуют поддержки ядра ЦП для организации очереди аппаратных портов.
-
в файле образа программного обеспечения эмулятора будет создано меньше кода, поскольку в нем будет меньше обработчиков эмуляции аппаратных портов.
-
Можно избежать больших накладных расходов, возникающих, когда циклы рутинной эмуляции оборудования (например, для рендеринга пикселей, создания аудиосэмплов и т. д.) должны быть разорваны (из-за того, что ядро ЦП записывает данные в аппаратный обработчик в этот момент в симулируемом кадре). Это важно по двум причинам:
-
ядро вашего эмулятора NES теперь можно спроектировать для работы в одном большом цикле, не беспокоясь о вмешательстве со стороны других аппаратных устройств в течение того же времени виртуальной эмуляции NES, за исключением случаев крайней необходимости. Это означает, что, скажем, движок PPU может визуализировать полный кадр в любой момент (в отличие от необходимости зависеть от данных, отправляемых в движок PPU в режиме реального времени через ядро ЦП), благодаря организации очереди аппаратных портов.
-
независимо от того, как ваш код 6502, написанный для NES, злоупотребляет PPU, APU, MMC и т. д. аппаратное обеспечение в NES, ваши основные механизмы всех этих устройств теперь могут быть спроектированы для использования почти постоянного количества тактовых циклов ЦП, как на физическом процессоре, на котором работает программное обеспечение вашего эмулятора, благодаря простой схеме циклов основных устройств эмулятора в сочетании с решениями кода без ветвлений для конструкций if/else и т. п.
Минусы:
-
использует некоторые дополнительные структуры данных/память.
-
сложнее реализовать, чем стандартный подход на основе обработчика в реальном времени.
8.1. Обзор
Концепция очередей аппаратных портов выгодна только для тех аппаратных устройств, которые не взаимодействуют с ядром ЦП (т. е. не изменяют и не влияют на его работу), за пределами читаемых портов, таких как $2002. Так, например, вы не захотите буферизировать записи в оборудование картографа, если оно влияет на PRG-банк (из-за того, что запись должна немедленно влиять на эмуляцию ЦП), но для изменений в CHR-банке это делать можно. Так что, по сути, это критерии, на которых вы должны основывать свои решения, когда решаете, какие аппаратные порты следует поставить в очередь.
С аппаратными устройствами, которые генерируют прерывания на ЦП, немного проще иметь дело, поскольку источники прерываний почти всегда исходят от какого-либо текущего счетчика в NES (счетчик строк сканирования MMC3 является небольшим исключением, поскольку он полагается на тактирование A13 на виртуальном PPU). Выполнение событий, которые должны произойти в тактовом цикле подсчета терминала, может быть поставлено в очередь ЦП путем создания экземпляра виртуального счетчика циклов с помощью процедуры аппаратной эмуляции, которая в нем нуждается.
8.2. Реализация
Идея "очереди портов" на самом деле вращается вокруг назначения обратных и прямых указателей для "_all_" аппаратно-зависимых (PPU, в этом примере) адресов памяти, которые могут быть изменены ЦП. Затем эти указатели связываются в 1+2-канальный список, который представляет данные в очереди для этого адреса памяти. Это означает пару указателей для:
-
каждый стандартный регистр PPU (2000-2007, хотя вам может не понадобиться делать их все)
-
каждый элемент памяти палитры
-
каждый элемент OAM
-
каждый элемент таблицы имен*
-
каждый элемент таблицы шаблонов
-
любые регистры переключения банков
-
каждый элемент в CHR-RAM, если он существует*
-
и т. д...
(* здесь необходимо учитывать только физические адреса, поскольку любые переключения банков будут поставлены в очередь.)
Когда ядро ЦП декодирует записи в порты, такие как $4014, ядро ЦП проверит статус этого порта как порта в очереди, а также указатель на последнюю выделенную ссылку в списке очередных записей для этого порта будет декодирован. Если для этого порта включена организация очереди, ЦП будет использовать информацию указателя вместе с информацией о выделении памяти и текущим счетчиком циклов, чтобы вставить новую ссылку в этот список, содержащую данные записи ЦП.
8.3. Атрибуты элемента списка
-
такт процессора, в котором произошла эта запись, относительно последней записи
-
следующая выделенная ссылка для этого списка
-
последняя выделенная ссылка для этого списка
-
ссылка на указатель кадра
-
данные
Относительное значение тега тактового цикла позволяет процедурам аппаратной эмуляции считывать значение позже, чтобы определить, когда произойдет следующая соответствующая запись в этот порт.
Указатели вперед/назад используются в каждом элементе списка для двустороннего перемещения. Это необходимо, поскольку часто необходимо, чтобы оборудование знало последнее известное значение любой памяти, к которой оно может иметь доступ.
Третий, односторонний указатель в каждом элементе списка будет использоваться для связывания всех узлов, созданных из одного и того же ядра движка в вашем эмуляторе. Это делает освобождение всех этих ссылок очень простым, при этом длина списка является прямой функцией количества аппаратных записей, которые произошли в этом кадре (так что, как правило, не так много). Обратите внимание, что ссылки с полем "last allocated link" = 0 *не* должны быть освобождены, поскольку они представляют ссылки, которые должны присутствовать для вычислений следующего кадра.
Для записи в порты типа $2004 и $2007, которые предназначены для потоковой передачи данных, это потребует некоторой дополнительной логики со стороны ядра ЦП для вычисления адреса списка ссылок (поскольку требуется дополнительный поиск и увеличение адреса). Обычно это делается с помощью аппаратного обработчика портов, но такой подход не одобряется, поскольку весь смысл реализации аппаратной очереди портов заключается в том, чтобы избежать передачи управления программой эмуляции из ядра ЦП в другие модули, если только это не является абсолютно необходимым.
Для обработки операций чтения ЦП из аппаратных портов достаточно просто определить, нужно ли вызывать обработчик порта или нет. Например, когда считывается $2002, его статус часто не меняется, пока не будет достигнут определенный (независимый) тактовый цикл в кадре. В этом случае порт будет считан в первый раз, и обработчик будет вызван. Затем обработчик вычислит следующий тактовый цикл, в котором ожидается изменение статуса $2002, и создаст экземпляр виртуального счетчика циклов, запрограммированный на выполнение другого обработчика, связанного с $2002, когда счетчик циклов истечет. Между тем, обработчик изменяет макет карты памяти ЦП, так что последующие считывания с этого порта просто заставляют ядро ЦП считывать с обычного адреса памяти, где хранится последнее известное значение порта, тем самым избегая ненужного вызова обработчика чтения $2002, пока виртуальный счетчик не сработает.
Для обработки операций чтения ЦП из таких портов, как $2004 и $2007, ядру ЦП просто нужно вернуть последнее известное значение элемента, к которому осуществляется доступ, из очередей массива.
9. Многопоточные приложения NES
В последнее время ПК на базе x86 стали настолько молниеносно быстрыми, что эмуляция всего одной виртуальной NES на современном ПК, по-видимому, будет пустой тратой вычислительной мощности. С учетом сказанного, современные ПК обладают достаточной вычислительной мощностью для эмуляции десятков виртуальных машин NES, но есть одна большая проблема с многозадачными приложениями NES: они никогда не были разработаны для потоковой обработки. Вместо этого, целый кадр тактовых частот NES CPU должен быть потрачен впустую для каждого приложения NES, чтобы считать вычисления кадра приложения завершенными, независимо от того, может ли это быть правдой (и если нет, произойдет замедление). Следующие подсказки и советы предлагают способы сокращения потерянного времени при эмуляции виртуального 6502, которое обычно теряется из-за циклов ожидания вращения, опроса или подсчета циклов.
-
Отслеживание потока процедуры прерывания. Все процедуры прерывания могут быть потоковыми независимо от того, выполняется ли правильная инструкция RTI в конце обработчика. Перехватывая доступ к значению адреса PC, сохраненному в стеке из выполняемого прерывания, обработчик может получить управление в следующий момент, когда старый адрес PC будет снова доступен, что, скорее всего, произойдет, когда процедура прерывания будет завершена. Из этого есть исключение: игры, которые только устанавливают флаги в обработчике прерывания, а затем возвращаются. В этом случае поток будет коротким, поэтому доступ к сохраненному адресу PC должен быть получен дважды, прежде чем поток на основе прерывания следует считать завершенным.
-
Для портов, часто используемых в циклах опроса (например, $2002), эти обработчики могут выполнять базовое сравнение цикла опроса с текущим местоположением PC, чтобы определить, опрашивается ли порт, и условие, при котором будет выполнен выход из цикла. Поскольку флаги, такие как vblank, >8sprites и priobjcollision, происходят в статический момент в эмулируемом кадре, легко заставить обработчик PPU перевести счетчик циклов ЦП непосредственно на тактовый цикл, в котором эти флаги будут соответствовать условию выхода из цикла, и таким образом сэкономить виртуальное время ЦП 6502.
-
Записи в оборудование NES, обуславливающие их текущую работу, которой не предшествовало событие прерывания или опрашиваемый порт, можно считать синхронизированными кодом подсчета циклов. В этом случае, если алгоритм может обнаружить наличие простого цикла подсчета циклов, десятки тысяч тактовых импульсов центрального процессора на кадр могут быть сэкономлены путем замены этого типа цикла 6502 на специальные инструкции jam 6502, которые просто сообщают ядру 6502 о необходимости ждать указанное количество циклов перед продолжением работы.
10. Поддерживаемые функции эмулятора
В этом разделе просто содержатся некоторые инновационные и интересные предложения по функциям для поддержки в новых разрабатываемых эмуляторах NES.
-
Совместимость с оригинальными контроллерами NES/SNES (документ, объясняющий, как подключить их к ПК, называется «NES 4 player adapter documentation»). Это не только позволяет геймерам играть в игры NES на вашем эмуляторе с оригинальным контроллером/световым пистолетом и т. д. (вместо того, чтобы использовать клавиатуру), но и позволяет неиспользуемым кнопкам на контроллере SNES иметь настраиваемую функциональность во время игры (изменение игры/состояния, приостановка, быстрая перемотка вперед, сохранение/загрузка состояния машины и функции сброса были бы очень удобными).
-
Полностью регулируемая виртуальная эмуляция частоты кадров PPU. Этот элемент управления позволяет геймерам программировать на лету частоту кадров PPU. Поскольку практически весь игровой код вращается вокруг прерываний кадров PPU, изменение этой частоты фактически изменяет скорость, с которой идет игра (обычно также управляет звуком). Это может быть полезно для эффектов ускоренной перемотки или замедления, особенно когда для достижения эффектов используются запасные кнопки контроллера. Кроме того, я обнаружил с друзьями, что игра в игру NES с более высокой частотой кадров (в нашем случае 90 Гц) действительно добавляет новые сложности и веселье практически к любой старой игре NES, которую вы можете себе представить.
-
Полностью регулируемая виртуальная эмуляция частоты кадров APU. Для игр, использующих этот источник прерываний, изменение частоты этого сигнала изменит скорость воспроизведения звука игры.
-
Замедления в играх NES должны быть устранены либо путем предоставления пользователю возможности регулировать количество тактов ЦП для выполнения на кадр PPU, либо путем потоковой обработки обработчика NMI игры. Кроме того, если игрок хочет замедлить действие игры, он должен иметь возможность сделать это, активировав кнопку замедления, а не быть вынужденным замедляться просто всякий раз, когда расчеты кадров игры становятся слишком тяжелыми для стандартного кадра на основе 29780 2/3 cc.
-
Количество спрайтов, отображаемых на каждой строке развертки, должно быть регулируемым (в целях разработки) или, если нет, неограниченным (поскольку это устраняет *большую* часть мерцания спрайтов).
-
предоставить пользователю возможность индивидуального микширования звука, генерируемого любым звуковым оборудованием NES, используемым в игре, в 6 звуковых дорожек ля воспроизведения через звуковую систему 5.1.
-
предоставить пользователю возможность запрограммировать альтернативную, пользовательскую форму волны, которая будет использоваться для воспроизведения канала треугольной волны, а также рабочие циклы 4+4, используемые между каналами прямоугольной волны. Изменение высоты тона и программируемые задержки звука, выполняемые на клонированном источнике аудиоканала, — это еще один способ услышать на NES несколько интересных новых звуков для любой старой игры.
-
Позвольте пользователю указать пользовательский размер и дополнительное смещение прокрутки для применения к отображаемому игровому полю PPU (вместо того, чтобы просто задать его по умолчанию 256*240, 0:0+ScrollCtrs) в вашем эмуляторе. Это не только позволяет геймерам обрезать края игрового поля игры NES, которое имеет беспорядочную графику вокруг, но и позволяет геймерам расширить размер игрового поля, чтобы включить отображение содержимого 1 или 3 других таблиц имен одновременно, что очень полезно для таких игр, как Pin Ball, Wrecking Crew, Super Mario Bros., Duck Tales, Metroid, Jackal и Gauntlet, и это лишь некоторые из них. Также должна быть предусмотрена опция, позволяющая предотвратить использование счетчиков прокрутки PPU (X или Y) при окончательном вычислении смещения прокрутки игрового поля, а скорее применять их к смещению рамки объекта (это заставляет объекты перемещаться по экрану, вместо того, чтобы игровое поле делало это, в то время как объекты оставались относительно середины игрового поля).
-
Предоставьте графический фильтр для обмена виртуальными наборами OAM. Этот метод используется, когда игре необходимо отобразить больше объектов, чем поддерживает оборудование PPU за кадр. Игры чередуются между двумя (или более) наборами OAM между кадрами, и это позволяет игроку видеть дополнительные объекты, но не без необходимости довольствоваться большим количеством мерцающих спрайтов. Примитивный метод фильтрации обмена наборами OAM заключается в расширении количества спрайтов, отображаемых в любом кадре, для включения одного или нескольких из предыдущих кадров. Обычно требуется сохранять только набор OAM последнего кадра, чтобы устранить серьезное мерцание спрайтов в играх вроде Mega Man 2, но иногда необходимы два или более старых набора OAM. В этом случае лучше реализовать сложную систему поиска шаблонов наборов OAM, которая устраняет высокие накладные расходы на повторную визуализацию одного и того же типизированного и размещенного спрайта, появляющегося в 2 или более наборах OAM.
-
Обеспечивает поддержку перемотки воспроизведения и записи фильмов NES. Эти два фактора работают вместе, вместе с сохранением состояний, позволяя создавать фильмы NES только с вашими лучшими игровыми моментами в любимой игре.
-
Поддержка аппаратно эмулированных подпрограмм FDS ROM BIOS. Это существенно снижает нагрузку на диск и экономит время ожидания до нуля. В результате старые игры Famicom на базе FDS будут работать так же быстро, как и игры на базе ROM.
-
Поддерживайте онлайн-галерею текста и искусства. Пользователи должны иметь возможность просматривать коллекцию изображений в формате bitmap, относящихся к NES (это могут быть скриншоты, отсканированные страницы инструкций, этикетки и т. д.). Просто подумайте, как выглядит меню выбора игры "Super Mario All Stars", а теперь представьте, что там гораздо больше вариантов выбора, и они простираются в двух измерениях. Теперь вы говорите об интересной новой функции для реализации в эмуляторе NES.
-
Разрешите несколько экземпляров виртуальных машин NES в вашем эмуляторе. Это может позволить геймеру с очень быстрым ПК преобразовать свою коллекцию игровых ПЗУ NES/FC в персональную домашнюю видеоаркаду NES/FC с помощью режима отображения видео высокого разрешения. Эмулятор может автоматически выполнять поиск в локальном репозитории файлов для сбора списка всех доступных образов ПЗУ NES и т. п. и загружать игровые состояния (начальные, если нет других) на виртуальные экраны мониторов, эмулируемые в главном рабочем окне эмулятора. «Окно просмотра» эмулятора позволит пользователю прокручивать виртуальную стену экранов видеомониторов NES; именно так пользователь может перемещаться между различными играми и состояниями NES (т. е. мы полностью отбрасываем концепцию необходимости выбирать игры и состояния по имени файла). Состояния игры изначально могут быть загружены в виртуальную матрицу монитора на основе алгоритма квадратной спирали, но после этого операции вырезания, копирования, вставки, перемещения и удаления могут быть выполнены над этими состояниями игры, чтобы манипулировать ими по своему усмотрению, возможно, увеличивая или уменьшая размер матрицы монитора. И, конечно, персональные настройки эмуляции могут быть сохранены для каждого состояния игры, так что, например, только выбранные состояния игр NES будут анимированы во время работы эмулятора аркады NES (состояния приостановленной игры могут просто отображаться таким образом на мониторах в виртуальной аркаде NES).
11. Новая объектно-ориентированная спецификация формата файла NES
В этом разделе подробно описывается новый, чрезвычайно простой в использовании стандарт для цифрового хранения данных образов NES ROM и связанной с ними информации, который обеспечивает максимальную объектную ориентацию для отдельных файлов.
11.1. Что означает объектная ориентация?
В данном случае я использую его для описания возможности пользователя получать доступ к определенной информации, связанной с любой игрой NES/FC, хранящейся в цифровом виде в локальном файловом хранилище, будь то данные ПЗУ программы, данные ПЗУ таблицы шаблонов, информация о маппере, картинки и другие цифровые изображения (арт-макеты, страницы руководства к игре и т. д.), состояния сохраненных игр, состояния ОЗУ батареи и т. д., без необходимости полагаться на какое-либо пользовательское или фирменное программное обеспечение NES/FC, просто сохраняя несколько компонентов, составляющих цифровую копию игры NES, в отдельных файлах известных/установленных типов и группируя эти файлы в подкаталоге, названном в честь игры.
Возьмем, к примеру, стандарт UNIF: это прекрасный пример монолитной структуры формата файла. UNIF — это формат файла, который заставляет людей полагаться на совместимые с UNIF инструменты для доступа к фрагментам данных внутри него, будь то растровые изображения, JPEG, данные ПЗУ программ и т. д., тогда как если бы эти фрагменты данных просто хранились как отдельные файлы в каталоге локального репозитория, не было бы необходимости в рекомендациях UNIF для доступа к этим данным.
Итак, в основном, идея здесь заключается в том, чтобы использовать существующие форматы файлов для хранения всей информации, связанной с одной игрой, в частном каталоге в вашей файловой системе среди других, составляя вашу электронную библиотеку игр NES. Все подобные типы файлов могут иметь похожие расширения, имея при этом разные имена файлов, обычно относящиеся к конкретному описанию того, что представляет файл (т. е. файлы, относящиеся к информации о состоянии сохранения, могут иметь заголовок, описывающий местоположение или статус игры состояния, или файлы патчей могут описывать работу патча во время эмуляции и т. д.). Поскольку другие соответствующие форматы файлов (например, *.jpeg, *.gif, *.bmp и т. д.) давно являются устоявшимися компьютерными стандартами, здесь определены только форматы файлов, относящиеся к работе NES.
*.PRG - идеальная цифровая копия программного ПЗУ игры.
*.CHR - идеальная цифровая копия таблицы шаблонов игры ROM или RAM (для сохранения состояний).
*.MMC - текстовый тег, идентифицирующий полный тип маппера ПЗУ PRG/CHR.
*.INES - классический 16-байтовый заголовок iNES, используемый как альтернатива файлу *.MMC.
*.WRAM - 2К ОЗУ привязано к шине 2A03 (6502)
*.VRAM - 2К ОЗУ привязано к шине 2C02
*.XRAM - дополнительная оперативная память (кроме CHR RAM), используемая на игровом картридже
*.SRAM - любая оперативная память с питанием от батарейки, используемая на игровом картридже
*.PRGPATCH - патч ПЗУ программы
*.CHRPATCH - патч ПЗУ персонажа
*.PRGHACK - текстовый файл, содержащий список патчей ПЗУ программы.
*.CHRHACK - текстовый файл, содержащий список патчей ПЗУ персонажей.
Этот список не является полным (поскольку структуры памяти 2A03, 2C02 и MMC всегда будут специфичны для эмулятора), но он должен дать вам представление о том, как отделить файлы, относящиеся к необработанным дампам больших внутренних структур памяти, используемых внутри NES, чтобы улучшить переносимость файлов ROM, больших структур RAM, дампов состояния сохранения, патчей, хаков и т. п.
*.PRG и *.CHR: цифровое содержимое ПЗУ программ и персонажей, обнаруженных на игровой плате NES. Было бы неплохо, чтобы эти файлы всегда поддерживали количество байтов 2^n, за исключением случаев, когда к их соответствующим файлам должны быть добавлены другие ПЗУ PRG/CHR, из-за возможности того, что игра NES может использовать два или более ПЗУ разного размера, чтобы составить один больший (до 1987 года это в основном делалось для увеличения размера ПЗУ игры с помощью большего количества чипов, поскольку, похоже, ПЗУ размером более 32 КБ были либо очень дорогими, либо недоступными в то время). Имя файла всегда связано с названием игры, включая информацию о том, была ли она взломана, стране, из которой она была произведена, и т. д. Файлы *.CHR, которые создаются для сохранения состояния, когда игровые картриджи NES используют CHR-RAM, используют описание соответствующего состояния сохранения в качестве имени файла.
*.MMC: просто обычный текстовый файл, содержащий тег типа платы в кодировке ASCII, который используют игры NES и Famicom. Используйте размер файла для определения длины цифрового тега. Формат UNIF довольно хорошо описывает различные типы плат картриджей NES/Famicom; это текстовые теги для использования в этом файле. Имя файла *.MMC указывает на PRG ROM, связанный с типом картографа, указанным в файле MMC.
*.INES: 16-байтовый файл, содержащий заголовок iNES, эквивалентный тому, что обычно представляет текстовый файл *.MMC. Этот файл существует только потому, что цифровое хранение игровых ПЗУ NES в настоящее время доминирует в устаревшем формате iNES. Поддержка не рекомендуется в новых эмуляторах (если вы не являетесь частью решения, вы являетесь частью проблемы, верно?).
*.WRAM, *.VRAM, *.XRAM: все они определяют файлы, которые содержат зеркальные образы чипов RAM, которые они представляют в эмулируемой NES. Имя файла для всех них относится к описанию состояния сохранения.
*.SRAM: определяет область RAM с питанием от батарейки для игры. Имя файла относится к описанию резервной RAM (зависит от игры и состояния). Поддержка нескольких копий SRAM полезна для хранения большего количества сохраненных игровых данных, чем позволяет один файл SRAM (обычно это 3 файла сохранения на SRAM, хотя это всегда зависит от игры).
*.PRGPATCH, *.CHRPATCH: Эти файлы содержат (little-endian) 32-битное смещение, за которым следуют необработанные данные для исправления в типе ПЗУ, указанном расширением. Имя файла здесь всегда относится к эффектам, которые патч оказывает во время эмуляции. Размер файла используется для определения длины патча (минус 4, чтобы исключить значение смещения).
*.PRGHACK, *.CHRHACK: эти файлы определяют списки в виде простого текста, которые определяют файлы патчей для применения к эмуляции игры, когда этот конкретный файл HACK выбран для применения к эмулируемой игре. Имя файла относится к группе патчей, которые вы выбрали для этого файла (обычно это не имеет значения, но это полезно для хранения нескольких профилей хаков (тех, которые делают игру проще, сложнее, страннее, ведут себя как файл NSF, меняют графику и т. д.)). Используйте коды ASCII formfeed и/или возврата каретки (13 и 10) для разделения перечисленных типов патчей в файле.
11.2. Заметки
-
Если в каталоге одной игры хранится более одного файла типа *.PRG, *.CHR (если он не основан на ОЗУ) или *.SRAM, эмулятор должен гарантировать, что игрок сможет выбрать активную ОЗУ/ПЗУ для использования во время эмуляции, поскольку эмуляция игры может основываться только на одном из этих источников.
-
Эмулятор должен иметь возможность обнаруживать и отображать все различные файлы HACK, доступные в каталоге игры, поскольку эффекты только одного файла HACK могут быть применены к выбранному игровому ROM.
-
Все форматы файлов, специфичные для эмулятора, должны быть четко задокументированы автором.
-
Любые игровые ПЗУ, не имеющие соответствующего имени файла типа MMC в том же каталоге, должны привести к тому, что эмулятор откажется эмулировать игровые ПЗУ.
-
Этот формат "_does_" немного усложняет транспортировку файлов NES ROM для обычных пользователей эмуляторов/геймеров, но в конечном итоге для транспортировки требуются только файлы PRG, MMC и необязательные файлы CHR и SRAM (то есть максимум 2..4 файла). Это едва ли сложно понять даже для начинающего пользователя.
Конец.
Автор: Seenkao