[Перевод] Эмуляторы SNES всего в нескольких пикселях от абсолютного совершенства
Мы так близки к созданию эмулятора, способного идеально воссоздавать все функции реального железа и ПО SNES.
В течение последних 15 лет как кодер эмулятора bsnes я пытался довести до совершенства эмуляцию Super Nintendo, но теперь мы столкнулись с последней проблемой: точным таймингом тактов видеопроцессоров SNES. Чтобы достичь этого завершающего этапа точности эмуляции, требуется помощь всего сообщества, и я надеюсь на вашу поддержку. Но для начала я расскажу, чего мы уже добились.
Текущее состояние
Сегодня ситуация с эмуляцией SNES очень хороша. Если не считать необычные периферийные устройства, которые сопротивляются эмуляции (например, клюшка для гольфа с датчиком света, велосипедный тренажёр и dial-up-модем, который в Японии использовали для ставок на конных скачкахJapan), все официально лицензированные игры для SNES полностью играбельны, и ни в одной игре нет очевидных проблем.
Эмуляция SNES стала такой точной, что мне даже пришлось разделить эмулятор на две версии: higan, которая стремится к абсолютной точности и соответствию документации железа, и bsnes, которая стремится к скорости, широким возможностям и простоте использования.
В последнее время в области эмуляции SNES получено множество интересных достижений, и в том числе:
… и многое другое!
Ну так что, дело сделано? Все хорошо поработали, пока, и спасибо за рыбу? Ну… не совсем.
Сегодня мы добились точности на уровне тактов почти всех компонентов SNES. Единственными исключениями остались PPU (picture processing unit, модули обработки изображений), используемые для генерации передаваемых на экран кадров видео. Мы по большей мере знаем, как работают PPU, но относительно части функций нам приходится использовать догадки, что приводит к неидеальной точности.
В общем масштабе оставшиеся проблемы довольно мелки. Если вы не стремитесь к стопроцентно точной идеальности эмуляции из любви к искусству, то я не смогу убедить вас в необходимости дальнейшего совершенствования эмуляции PPU. Как и в любой сфере, чем мы ближе к идеалу, тем меньше отдача.
Но я могу сказать, почему это важно для меня: это работа всей моей жизни, и я не хочу, чтобы мне пришлось сказать, что я настолько близко подобрался к завершению, не сделав последний шаг. Я старею, и я не вечен. Мне хочется, чтобы последний кусок головоломки был решён, чтобы, уйдя на покой, я был уверен, что наследие SNES надёжно и полностью сохранено благодаря эмуляции. Я хочу сказать, что задача решена.
Если вы всё ещё заинтригованы то продолжайте чтение, чтобы познакомиться с подоплёкой проблемы и предлагаемыми мной решениями.
Моделирование архитектуры SNES
Давайте начнём с перечисления компонентов, из которых состоит SNES:
Схема системы Super NES.
Стрелками показаны направления, в которых различные процессоры SNES могут обмениваться друг с другом данными, а пунктирными линиями обозначены соединения с чипами памяти.
Самое важное для нас сейчас — заметить, что вывод видео и звука передаётся напрямую из PPU и DSP. Это означает, что они действуют как «чёрные ящики», и мы никак не можем увидеть, что происходит у них внутри. Позже это станет для нас важным.
Корректность
Представьте, что мы эмулируем команду ЦП «умножить», которая берёт два регистра (переменные), перемножает их, получает результат и несколько флагов, обозначающих состояние результата (например, переполнение).
Мы можем написать программу, умножающую любое возможное значение от 0 до 255 как множитель и множимое. Затем мы можем вывести численные и флаговые результаты умножения. Таким образом мы получим две таблицы по 65 536 элементов.
Анализируя эти таблицы, мы можем точно определить, как и где результаты вычислений ЦП задаются определённым образом. Тогда мы можем модифицировать эмуляторы так, чтобы при прогоне такого же теста получались точно такие же таблицы за то же самое время.
Теперь допустим, что ЦП умеет выполнять умножение 16 бит x 16 бит. При тестировании каждого возможного значения будет сгенерировано 4 миллиарда результатов, которые уже практически невозможно протестировать за разумное время. Если у ЦП есть умножения 32 бит x 32 бит, то на практике невозможно будет протестировать все комбинации входящих значений до тепловой смерти Вселенной (по крайней мере, на текущем уровне технологий).
В подобных случаях мы действуем в тестах более выборочно и пытаемся определить, когда точно могут меняться флаги, когда может возникать переполнение результатов, и так далее. В противном случае нам бы пришлось выполнять тесты, которые бы никогда не завершились.
Умножение — довольно тривиальная операция, но тот же принцип можно распространить и на весь процесс реверс-инжиниринга, в том числе и на более сложные операции, например, на передачу данных по DMA (direct memory access) во время обратного горизонтального хода луча. Мы создаём тесты, которые пытаются определить, что происходит в пограничных случаях, а затем проверяем, ведёт ли себя наша эмуляция идентично поведению реальной SNES.
Генераторы сигналов и такты
SNES имеет два генератора сигналов (осциллятора): кварцевый генератор, работающий с частотой примерно 21 МГц (он управляют ЦП и модулями PPU), и керамический резонатор, работающий с частотой примерно 24 МГц, который управляет SMP и DSP. В сопроцессорах картриджей иногда используется кварцевый генератор ЦП на 21 МГц, а иногда собственные генераторы сигналов, работающие с другими частотами.
Воссоздать эту печатную плату Super Famicom в коде сложнее, чем кажется.
Такт — базовый элемент тайминга любой системы, и SNES рассчитана на выполнение различных задач с определёнными частотами и промежутками времени.
Если представить 100-герцовый такт, то это будет устройство с двоичным выводом, переключающимся на высокое логическое состояние сигнала (например, +5 В), а затем на низкое состояние сигнала (0 В, или землю) 100 раз в секунду. То есть каждую секунду напряжение на выводе будет колебаться 200 раз: 100 раз повышая и 100 раз понижая фронт тактового сигнала.
Тактовым циклом обычно считается один полный переход, то есть 100-герцовый такт сгенерирует 100 тактовых циклов в секунду. Некоторые системы требуют различия между повышением и понижением фронтов, и для них мы разбиваем цикл на полуциклы, чтобы обозначить каждую фазу (высокую или низкую) тактового сигнала.
Важнейшей задачей точного эмулятора является выполнение задач точно таким же образом и за точно такое же время, как и на реальном оборудовании. При этом не очень важно, как именно выполняются задачи. Единственное, что важно — чтобы эмулятор, получая такие же входные сигналы, генерировал те же выходные сигналы за то же время, что и на реальном железе.
Тайминги
Иногда на выполнение операций требуется время. Возьмём к примеру умножение в ЦП SNES. Вместо того, чтобы приостанавливать работу и ждать, пока умножение завершится, ЦП SNES вычисляет результат умножения по одному биту за раз в фоновом режиме в течение восьми тактов опкодов ЦП. Потенциально это позволяет коду выполнять другие задачи, ожидая завершения умножения.
Скорее всего, любое коммерческое ПО будет ждать в течение этих восьми тактов, потому что если попробовать считать результат до того, как он будет готов, то мы получим частично выполненный результат. Однако раньше эмуляторы SNES выдавали правильные результаты мгновенно, не ожидая в течение этих дополнительных тактов.
Когда фанаты консолей начали создавать и тестировать в эмуляторах самописное ПО, это расхождение начало вызывать определённые проблемы. Часть ПО, например, множество первых ROM-хаков Super Mario World, работало правильно только в этих старых эмуляторах, но не на настоящем железе SNES. Так происходило потому, что они разрабатывались с учётом мгновенного (недостоверного с точки зрения реального оборудования) получения результатов умножения.
В процессе улучшения эмуляторов совместимость старого ПО нарушалась, и поэтому нам пришлось добавить в новые эмуляторы опции совместимости, чтобы не потерять эти программы. Да, как ни сюрреально это звучит, но сегодня эмуляторы должны эмулировать другие эмуляторы!
Удобство этой задержки умножения в ЦП заключается в том, что она очень предсказуема: восемь тактов вычислений начинаются сразу же после запроса операции умножения. Написав код, считывающий результаты после каждого цикла, мы смогли убедиться, что ЦП SNES использует для умножения алгоритм Бута.
Синхронизация тактов
Другие операции смоделировать не так просто, потому что они асинхронно выполняются в фоновом режиме. Одним из таких случаев является обновление DRAM центрального процессора SNES.
Во время рендеринга каждой растровой строки весь ЦП SNES на определённом этапе приостанавливает свою работу на короткий промежуток времени, пока обновляется содержимое чипа ОЗУ. Это необходимо потому, что для снижения стоимости в SNES в качестве основной памяти ЦП использовалась динамическая (а не статическая) ОЗУ. Для сохранения содержимого динамического ОЗУ его необходимо периодически обновлять.
Для создания действительно идеального эмулятора недостаточно обеспечить играбельность всех трёх с половиной тысяч игр SNES. Нужно ещё и добиться симуляции каждой функции системы с потактово идеальной точностью.
Ключевым фактором в анализе точных таймингов этих операций стала возможность использования горизонтального и вертикального счётчиков PPU. Эти счётчики выполняют инкременты и сбрасываются после каждого обратного горизонтального и вертикального хода луча. Однако их точность равна только четверти от частоты генератора сигналов ЦП SNES; иными словами, горизонтальный счётчик выполняет инкремент каждые четыре тактовых цикла.
Считывая несколько раз значения счётчиков, мне удалось определить, с какой четвертью тактового цикла согласован счётчик. Соединив это знание со специально созданной функций, способной делать шаг на точное указанное пользователем количество тактовых циклов, я смог идеально согласовать ЦП SNES с любой нужной мне точной позицией тактового цикла.
Благодаря итеративному обходу множества тактовых циклов мне удалось определить, когда точно происходят определённые операции (например, обновление DRAM, передача HDMA, опрос прерываний, и т.п.). После этого я мог в точности воссоздать всё это в эмуляции.
Чип SMP консоли SNES тоже имеет собственные таймеры, и для этого процессора тоже был выполнен успешный реверс-инжиниринг. Я могу посвятить целую статью только регистру SMP TEST, который позволяет программистам управлять делителем частоты SMP и его таймером, не говоря уже о других ужасных вещах. Достаточно будет сказать, что это был непростой и небыстрый процесс, но в конечном итоге мы победили.
Собираем сопроцессоры
Чип SuperFX — это только один из множества сопроцессоров картриджей, с которыми эмулятор SNES может правильно справляться.
Существует целая куча сопроцессоров SNES, использованных внутри различных игровых картриджей, которые нам тоже нужно было укротить. От отдельных ЦП общего использования наподобие SuperFX и SA-1, цифровых сигнальных процессоров наподобие DSP-1 и Cx4 до ускорителей распаковки наподобие S-DD1 и SPC7110, или тактовых генераторов реального времени Sharp и Epson, а также многого другого…
Это значит, что эмулятор SNES должен справляться с кэшами инструкций и пикселей SuperFX; со схемой разрешения конфликтов шины памяти SA-1 (позволявшей ЦП SNES и SA-1 одновременно использовать одни чипы ПЗУ и ОЗУ); со встроенной прошивкой DSP-1 и Cx4; с основанными на прогнозировании арифметическими кодерами S-DD1 и SPC7110;, а также с нечётными пограничными случаями BCD (binary-coded decimal) в генераторах реального времени. Медленно, но верно, применяя все описанные выше техники определения корректности и таймингов, нам удалось научиться почти идеально эмулировать все эти чипы.
Потребовалось много усилий и тысячи долларов на снятие крышек чипов и извлечение программной прошивки из цифровых сигнальных процессоров, используемых в разных играх. В одном из случаев эмуляция NEC uPD772x позволила использовать код из higan для сохранения голоса покойного Стивена Хокинга!.
В другом случае нам нужно было выполнить реверс-инжиниринг целого набора инструкций архитектуры Hitachi HG51B, потому что документацию этой архитектуры никто и никогда не публиковал. В ещё одном случае оказалось, что одна игра (Hayazashi Nidan Morita Shougi 2) имеет мощный 32-битный ЦП ARM6 с частотой 21 МГц, выполняющий ускорение японской игры сёги!
Одно сохранение всех сопроцессоров SNES оказалось многолетним процессом, полным трудностей и сюрпризов.
Обработка цифрового сигнала
Чип Sony S-DSP (цифровой сигнальный процессор), который не нужно путать с сопроцессором картриджа DSP-1, генерировал уникальный звук SNES. В этом чипе были соединены восемь звуковых каналов с 4-битным кодированием ADPCM, что обеспечивало создание 16-битного стереосигнала.
Внешне, да и из представленной выше схемы системы, поначалу кажется, что DSP представляет собой «чёрный ящик»: мы настраиваем звуковые каналы и параметры микшера, после чего чип генерирует звук, передаваемый на динамики.
Но одна важная функция позволила разработчику под ником blargg выполнить полный реверс-инжиниринг этого чипа: это был эхо-буфер. DSP консоли SNES имеет функцию, микширующую выходные сигналы предыдущих сэмплов для создания эффекта эха. Это происходит в самом конце процесса генерации звука (если не считать одного последнего флага блокировки звука, который можно применить для отключения всего вывода звука.)
Благодаря написанию кода с правильным таймингом тактов и отслеживанию получившегося эха нам удалось определить точный порядок операций, выполняемый DSP для генерации каждого сэмпла и создания идеально точного по тактам и битам звука.
Сохранение PPU
Всё это привело нас к последней части архитектурной схемы SNES: чипам PPU-1 и PPU-2. Благодаря Джону Макмастеру у нас есть сканы чипов S-PPU1 (revision 1) и S-PPU2 (revision 3) с двадцатикратным увеличением.
Скан с двадцатикратным увеличением кристалла первого PPU SNES…
… и второго PPU
Оба скана кристаллов дают нам понять, что чипы очевидно не являются ЦП общего применения, как и не являются специализированными архитектурами, выполняющими коды операций из внутреннего ROM программы прошивки. Это отдельные логические цепи с жёстко прописанной логикой, получающие входящие сигналы от разных регистров и памяти, и создающие видеосигнал на монитора по одной растровой строке за раз.
PPU остаются последним препятствием к эмуляции SNES потому, что в отличие от всех описанных выше компонентов, PPU на самом деле являются «чёрным ящиком». Мы можем сконфигурировать их под любое состояние, но ЦП SNES никак не может напрямую следить за тем, что они генерируют.
Если использовать в качестве аналогии наш предыдущий пример с умножением, то представьте, что вы запросили результат 3×7, но вместо двоичного ответа вы получаете на экране нечёткое аналоговое изображение цифр »21». Любой, кто запустит ваше ПО, сможет увидеть 21, но вы не можете написать тестовую программу для автоматической проверки того, видит ли он правильный ответ. Ручная проверка человеком подобных результатов не может масштабироваться больше, чем до нескольких тысяч тестов, а для максимального уточнения поведения PPU их потребуются миллионы.
Я знаю, о чём вы подумали: «Но ведь проще использовать плату захвата, выполнить обработку изображений, приблизительно сопоставить их с изображением на цифровом экране эмулятора, и проводить тесты исходя из этого?»
Ну да, возможно! Особенно если тест заключается в проверке двух огромных цифр, занимающих весь экран.
Но что, если тестирование имеет множество нюансов, а мы пытаемся распознать разность цветов полутона одного пикселя? Что если мы хотим прогнать по порядку миллион тестов, и не не всегда знаем пока, что будем генерировать, но всё равно хотим сопоставить результат с выводом нашей эмуляции?
Ничто не сравнится в удобстве и точности с цифровыми данными — точным потоком битов, которые могут только или совпадать, или не совпадать. Аналоговая природа ЭЛТ-сигнала не может нам этого обеспечить.
Почему это важно?
За исключением одной игры (Air Strike Patrol), всё официально лицензированное ПО для SNES (должно было быть) основано на растровых строках. Эти игры не пытаются изменить состояние рендеринга PPU посередине текущей рендерящейся растровой строки (такой трюк программистов называется «растровым эффектом»). Это означает, что тайминги выполнения подавляющего большинства игр не должны быть особо точными; если вы успеваете к следующей полной растровой строке, то всё в порядке.
Но это важно для одной единственной игры.
В этой серии изображений показан сложный в эмуляции эффект, использованный в сообщении «Good Luck» игры Air Strike Patrol.
На изображениях выше вы видите покадрово поворачивающийся текст «Good Luck» из игры Air Strike Patrol. Игра реализует его при помощи изменения позиции вертикально скроллинга фонового слоя 3 (BG3). Однако дисплей приборной панели слева (где видно, что у игрока 39 ракет) тоже находится на том же фоновом слое.
Игре удаётся выполнить это разделение благодаря изменению в каждой растровой строке позиции скролла BG3 после рендеринга левой приборной панели, но перед тем, как начнёт рендериться текст «Good Luck». Это возможно сделать, потому что за пределами приборной панели и текста BG3 прозрачен и между двумя этими точками нечего рисовать, вне зависимости от значения регистра вертикального скроллинга. Такое поведение показывает нам, что регистры скроллинга можно менять на любом этапе рендеринга.
Эта небольшая тень под самолётом вызвала кучу головной боли у одержимого точностью разработчика эмулятора.
На изображении выше показана печально известная тень самолёта. Этот эффект рендерится изменением регистра яркости экрана короткими пульсациями на протяжении пяти растровых строк.
Во время игры можно заметить, что эта тень довольно хаотична. На показанном выше изображении она немного похожа на букву «c», но её форма в каждой растровой строке меняется по длине и начальной точке с каждым кадром. Разработчики Air Strike Patrol просто приблизительно наметили, где должна появляться тень, и решили эту задачу напрямую. В большинстве случаев это работает.
Для правильной эмуляции такого поведения требуется идеальный по тактам тайминг, который в эмуляторе получить совершенно точно чрезвычайно сложно.
На экране паузы Air Strike Patrol используются растровые эффекты, которые не применялись намеренно ни в какой другой игре для SNES.
Теперь поговорим об экране паузы. Он включает BG3 на время отрисовки жёлто-чёрной границы слева и снова отключает его во время той же границы справа, чтобы отрисовать на экране серые линии. Также он попеременно через кадр переключает растровые строки, в которых отображаются эти серые линии для создания эффекта дрожания оверлея.
Если увеличить показанное выше эмулированное изображение, то можно заметить, что в течение пары растровых строк в левом углу этих серых линий есть несколько отсутствующих пикселей. Так получилось потому, что моя эмуляция PPU неидеальна по тактам на 100%. В данном случае она вызывает эффект включения BG3 чуть позже, чем должна.
Я могу очень легко изменить тайминги, чтобы это изображение рендерилось правильно. Но такое изменение скорее всего пагубно повлияет на другие игры, которые изменяют регистры дисплея PPU посередине растровой строки. Хотя Air Strike Patrol — единственная игра, которая делает это намеренно, существует по крайней мере дюжина игр, в которых это происходит случайно (возможно, IRQ срабатывает в них слишком рано или поздно).
Иногда это вызывает краткие заметные повреждения картинки, на которые не обращают внимание при разработке (например, в Full Throttle Racing при переходе между магазином и игрой). Иногда запись выполняется во время отрисовки прозрачного в остальном экрана, а потому не вызывает визуальных аномалий (например, как в случае отображения состояния HP в Dai Kaijuu Monogatari II.) Но даже такие «невидимые» пограничные случаи могут вызывать проблемы в менее точных рендерерах растровых строк, которые используются в самых производительных эмуляторах.
Даже если игнорировать Air Strike Patrol, все эти случайные (но действующие) растровые эффекты в ПО для SNES не позволяют функционально спроектировать рендерер PPU, генерирующий всю растровую строку с идеальной тактовой точностью.
В случае bsnes за годы проб и ошибок мы создали список таких игр с «растровыми эффектами». Также мы создали индивидуальные позиции рендеринга, позволяющие гораздо более быстрому рендереру на основе растровых строк правильно отображать все эти игры (за исключением Air Strike Patrol, конечно). Но по сути, это куча неприятных для нас хаков, рассчитанных на конкретные игры.
У меня также есть рендерер PPU на основе тактов, которому не требуются все эти хаки, но время от времени он создаёт небольшие (от одного до четырёх пикселей) отличия с рендерингом настоящего оборудования, как на показанном выше скриншоте Air Strike Patrol.
Внутренние регистры-защёлки
Причина всех этих маленьких промахов сводится к таймингам защёлкивания.
Допустим, SNES рендерит своей знаменитый mode 7, который является аффинным преобразованием текстур с изменениями параметров в каждой растровой строке. Для определения любого пикселя экрана необходимо выполнить подобные вычисления:
px = a * clip(hoffset - hcenter) + b * clip(voffset - vcenter) + b * y + (hcenter << 8) py = c * clip(hoffset - hcenter) + d * clip(voffset - vcenter) + d * y + (vcenter << 8)
Реальная SNES не сможет выполнить все эти шесть умножений достаточно быстро для каждого пикселя, который рендерится в кадре. Но ни одно из этих значений не меняется для каждого пикселя (или, по крайней мере, не должно меняться), поэтому нам достаточно вычислить px и py один раз в начале каждой растровой строки. То есть PPU кэширует статические результаты в регистры-защёлки, которые по сути своей являются копиями регистров PPU. В дальнейшем они могут преобразовываться, или оставаться неизменными.
Затем координаты x, y преобразуются режимом mode 7 следующим образом:
ox = (px + a * x) >> 8 oy = (py + c * x) >> 8
Хотя x меняется для каждого пикселя, мы знаем, что инкремент каждый раз выполняется на единицу. Благодаря хранению внутренних накопителей, мы просто можем для каждого пикселя прибавлять к ox и oy постоянные значения a и c, а не выполнять для каждого пикселя по два умножения.
Тогда перед нами встаёт вопрос: в какой конкретно позиции такта PPU считывает значения a и c из внешних регистров PPU, к которым имеет доступ ЦП?
Если мы возьмём их слишком рано, то это может сломать несколько игр. Если возьмём слишком поздно, то это может сломать другие игры.
Проще всего дожидаться отчётов о багах и настраивать эти позиции так, чтобы устранять проблемы в каждой конкретной игре. Но в таком случае мы никогда не найдём точных позиций, только их аппроксимации.
И каждый раз, когда мы будем изменять одну из этих переменных, нам нереально будет повторно тестировать все три с половиной тысячи игр из библиотеки SNES для обнаружения ухудшений, которые могли внести наши изменения.
Из огня, да в полымя
Художественная интерпретация процесса устранения ошибок эмуляции.
Подобный стиль методологии тестирования «просто любой ценой заставим работать интересующую нас игру» привёл к явлению, которое я называю эмуляцией «из огня, да в полымя».
В самом начале развития эмуляции SNES, когда в игре возникали проблемы, то любое исправление в этой игре, позволявшее ей работать, принималось и добавлялось в эмулятор. Этот фикс обязательно ломал какую-нибудь другую игру. А затем исправляли эту игру, после чего ломалась третья. Исправление третьей игры снова ломало первую. Так продолжалось многие годы.
Ошибка здесь заключалась в том, что разработчики пытались учесть только одну переменную за раз. Допустим, у нас есть игра, и чтобы она работала, события должны происходить между тактами 20 и 120. Мы не знаем точного такта, поэтому просто выберем 70, ровно посередине.
Позже мы получаем отчёт о баге в другой игре, и определяем, что для работы этой игры значение такта должно находиться между 10 и 60. Так что теперь мы меняем его на 40, что подойдёт обеим играм. Звучит логично!
Но затем появляется третья игра, в которой событие должно сработать между тактами 80 и 160! Теперь мы никак не сможем заставить работать все три игры одновременно с одним значением.
Это заставило разработчиков эмуляторов создавать хаки под конкретные игры. Кодерам не хочется выпускать эмулятор, в котором нельзя запустить Mario, Zelda или Metroid. Поэтому для общего случая используется такт 40, но при загрузке Metroid мы принудительно переключаем значение тайминга на 100.
Как такое возможно, почему двум играм нужны разные значения? Так получается, потому что здесь задействована не только одна переменная. Тайминг, который вы ранее использовали для срабатывания другого события, может влиять на то значение тайминга, которое потребуется для следующего события.
Представим это в виде простого алгебраического выражения:
2x + y = 120
Можно решить его, взяв x=10, y=100. Или x=20, y=80. Или x=30, y=60. Если мы думаем только о значении x, которое позволяет одновременно запустить одно множество игр, то упускаем то, что на самом деле проблема может заключаться в неправильном y!
Первые версии эмуляторов для повышения совместимости просто переопределяли значение x в зависимости от запущенной игры. Такие индивидуальные хаки игр сохранялись, даже если позже обнаруживалось правильное, единственное значение x. Таким образом, проблема y никогда не была бы раскрыта!
Однако в случае SNES одновременно задействованы не одна и не две переменные. Один только PPU консоли SNES имеет 52 внешних регистра, что составляет примерно 130 параметров. В процессе рендеринга одной растровой строки задействуются все 130 этих параметров и неизвестное количество внутренних регистров и регистров-защёлок. Это слишком большой объём информации для того, чтобы кто-то снаружи смог осознать полное состояние PPU в конкретный момент времени.
Этот аспект эмуляции неочевиден для непосвящённых, но очень справедлив: точность не равна совместимости. Мы можем создать эмулятор с 99-процентной точностью, способный запускать 10% игр. И можно написать точный на 80% эмулятор, запускающий 98% игр. Иногда правильная реализация в кратковременной перспективе ломает популярные игры. Это необходимая жертва, если вы пытаетесь достичь и 100-процентной точности, и 100-процентной совместимости.
Решаем проблему
До текущего этапа эмуляции PPU мы добрались благодаря дедуктивным размышлениям и результатам в реальном мире.
Мы знаем, что два PPU имеют доступ к двум чипам VRAM. Мы знаем, что они могут считывать из каждого чипа известное количество байтов данных на одну растровую строку. Мы знаем приблизительные подробности того, как работает каждый из видеорежимов SNES. И на основании этого мы можем наметить обобщённый паттерн того, как может выглядеть архитектура. Например, вот краткий пример того, как могут работать первые три видеорежима SNES:
if(io.bgMode == 0) { bg4.fetchNameTable(); bg3.fetchNameTable(); bg2.fetchNameTable(); bg1.fetchNameTable(); bg4.fetchCharacter(0); bg3.fetchCharacter(0); bg2.fetchCharacter(0); bg1.fetchCharacter(0); } if(io.bgMode == 1) { bg3.fetchNameTable(); bg2.fetchNameTable(); bg1.fetchNameTable(); bg3.fetchCharacter(0); bg2.fetchCharacter(0); bg2.fetchCharacter(1); bg1.fetchCharacter(0); bg1.fetchCharacter(1); } if(io.bgMode == 2) { bg2.fetchNameTable(); bg1.fetchNameTable(); bg3.fetchOffset(0); bg3.fetchOffset(8); bg2.fetchCharacter(0); bg2.fetchCharacter(1); bg1.fetchCharacter(0); bg1.fetchCharacter(1); }
Дырки в чёрном ящике
Стороннему наблюдателю PPU раскрывает только малую часть своего состояния: флаги горизонтального и вертикального обратного хода луча (horizontal/vertical blanking), счётчики горизонтальных и вертикальных пикселей и флаги наложения тайлов в интервале для спрайтов. Это не так уж много, но повторюсь — нам помогает каждый крошечный элемент доступного для наблюдателя состояния.
VRAM (video RAM, видеопамять) чипа PPU во время рендеринга закрыта для ЦП SNES, даже на чтение. Но как оказалось, OAM (память спрайтов) и CGRAM (память палитр) открыты. Хитрость в том, что в это время PPU управляет адресной шиной. Поэтому считывая OAM и CGRAM во время рендеринга экрана, я могу наблюдать, что же получает PPU из этих двух блоков памяти в такое критически важное время.
Это ещё не все фрагменты пазла, но их достаточно, чтобы я смог реализовать практически правильные паттерны получения спрайтов.
При помощи паттернов доступа к открытым OAM и CGRAM, флагов PPU, общим наблюдениям (то есть догадкам) из отчётов об ошибках в разных играх и дедуктивным рассуждениям, мы смогли создать рендереры PPU на основе тактов, способные идеально почти запускать все выпущенные игры.
Но ситуация по-прежнему шаткая: если кто-нибудь начнёт создавать homebrew-игры с использованием точного тайминга тактов и растовых эффектов, то все наши современные эмуляторы с этим не справятся. В том числе программные и аппаратные реализации на основе FPGA.
Нужно сказать чётко: сегодня все лишь догадываются о внутреннем порядке операций и поведения защёлкивания в чипах PPU консоли SNES. Никто не знает, как эмулировать их идеально. По крайней мере, пока.
Возможные варианты решений
Что же нам с этим делать? Как определить точный порядок операций в PPU, если с точки зрения ЦП SNES он представляет собой «чёрный ящик»?
Я вижу четыре возможных варианта: анализаторы логики, цифровой вывод видео в тестовом режиме, переходные платы и снятие крышек с чипов.
Анализаторы логики
Если взглянуть на показанные выше сканы кристаллов PPU, то можно заметить чёрные площадки по краям чипа. Это площадки, присоединяющиеся к контактам чипов.
Эти контакты хранят состояние чипов PPU во время каждого тактового цикла выполнения. Здесь можно найти текущий адрес, к которому чипы осуществляют доступ в чипе видеопамяти, значения данных, передаваемых из одного PPU во второй, и многое другое.
Эта информация недоступна для кода, выполняемого в ЦП SNES, но она даёт ценные наблюдения о внутреннем порядке операций PPU.
Подключение PPU консоли Super NES к подобному анализатору логики может стать ключом к «чёрному ящику».
Критическая проблема анализаторов логики заключается в том, что ими не очень удобно управлять: если попробовать сэмплировать живые данные работающей системы, то мы получим поток результатов, который довольно сложно расшифровать. С той же проблемой вы столкнётесь, если попытаетесь проанализировать аналоговый RGB-выход системы: для захвата этих данных придётся вручную выполнять каждый из тестов. Такая система не очень хороша для создания воспроизводимых автоматизированных регрессированных тестов.
Цифровой вывод видео в тестовом режиме
Недавно благодаря сканированию срезов кристаллов с 20-кратным увеличением в чипах PPU консоли SNES обнаружился секретный тестовый режим. Если внести небольшую аппаратную модификацию, то PPU начинают выводить 15-битный цифровой RGB-сигнал!
Это же почти то, что нам нужно! Однако этот режим имеет проблемы, поскольку знаменитый mode 7 не может выводить в нём правильную картинку. Похоже, эту функцию так до конца и не доделали.
Кроме того, для реализации этого способа всё равно требуется модификация консолей SNES вручную и соответствующий механизм для захвата и анализа вывода в тестовом режиме. Тем не менее, в отличие от решения с захватом аналогового RGB-сигнала, такой цифровой сигнал можно подвергать автоматическому тестированию, что может позволить нам быстро завершить большой объём работ по реверс-инжинирингу PPU.
Переходные платы
Учитывая то, что PPU статичны, мы могли бы извлечь чипы PPU из работающей консоли SNES и подключить их к плате для прототипирования или специально изготовленной печатной плате вместе с двумя чипами VRAM. После этого можно расположить между PPU и USB-интерфейсом микроконтроллер, а интерфейс подключить к PC, что позволит кодеру программировать все внешние регистры видеопамяти и PPU. Кроме того, кодер сможет вручную управлять тактовыми циклами PPU и считывать получившиеся сигналы на разъёмах ввода-вывода, регистрах и в памяти PPU в каждом такте.
Модифицировав программный эмулятор так, чтобы он генерировал такие же внутренние значения разъёмов ввода-вывода, мы смогли бы напрямую сравнивать настоящее железо с эмуляцией, даже в реальном времени. Однако это будет очень тяжёлой работой, потому что мы пока не можем увидеть внутренние операции PPU.