Я не знаю, сколько студентов играли в симуляторы игровых приставок, таких как Xiaobawang и GBA, когда они были молоды? С эмулятором интересно не только играть, но и сам процесс его написания. Далее мы расскажем, как использовать JavaScript для создания новой игры с процессором, памятью, вводом и выводом, а также играть в старые игры, объем которых не превышает 3 КБ.эмулятор.
Приступаем к разработке эмулятора
Если вы считаете следующую теорию немного скучной, вы можете просто открыть ее и поиграть с нашими окончательными результатами:Merry8эмулятор. Он использует 2,5 КБ кода JavaScript для поддержки языка ассемблера 1970-х годов, что позволяет вам испытать игру PONG, написанную на этом языке, на Canvas (да, шарик для пинг-понга, который прыгает вперед и назад), а также поддерживает черезNPMустановить и использовать его,Если вы считаете это интересным, пожалуйста, нажмитеStarиди снова 😀
После окончания прелюдии. Может быть, для подавляющего большинства студентов тренажер — странное понятие, так что же это за штука?
В широком смысле, от Hello World до Alpha Go, все программное обеспечение представляет собой не что иное, как логику кода, которая передает [выход] на [ввод]. Итак, симулятор — это тоже программное обеспечение, какие у него входы и выходы? Подумайте, как вы играете в Super Mario:
- Вам нужно открыть Super Mario ROM с помощью эмулятора, это эмуляторвойти.
- Нужно нажимать клавишу во время игры, это тоже эмуляторвойти.
- В эмуляторе есть изображение и звук, это эмуляторвыход.
Итак, пока ваш код реализуетОткройте и запустите ПЗУ, отвечая на ввод пользователя, это рабочий симулятор!
Таким образом, вопросы, над которыми нам нужно подумать, уточняются до следующего:
- Какой формат у игры ROM и как его открыть?
- Как запустить код в ПЗУ игры?
- Когда ПЗУ работает, как получить пользовательский ввод и вывести результат?
Это и如何把大象装进冰箱
Нравится трилогия, очень простая и понятная? Ниже мы ответим на эти три вопроса один за другим:
открыть дверь холодильника: Какой формат у игры?
Исполняемый файл для Windows.exe
формат, исполняемый файл Linux.elf
формат, а исполняемый файл игровой приставки — ROM. Игровые приставки разных платформ поддерживают разные форматы ПЗУ. Однако в целом ониМашинный кодСформированный бинарный формат. Некоторые передовые студенты могут быть знакомы сArrayBuffer
Эта структура данных очень подходит для выражения такого содержания. Итак, то, что мы делаем, когда открываем ПЗУ, очень просто, нам нужны только эти два шага:
- Отправьте запрос Ajax, чтобы получить статические файлы ПЗУ.
- Преобразование файлов ROM в массивы JS ArrayBuffer.
После этого шага мы получаем массив ArrayBuffer, каждый элемент имеет размер в0x00
прибыть0xFF
числа между ними. Студенты, знакомые со значениями цвета CSS, смеялись, это не в шестнадцатеричном формате0~255
Хорошо! Однако значение здесь имеет отношение не к глубине цвета, а к реальному машинному коду. Как расшифровать значение этой цепочки цифр?
положить слона в: Как запустить код ПЗУ?
Когда речь заходит о [машинном коде] и [языке ассемблера], первое, что приходит на ум многим студентам, — это боязнь оказаться под властью принципов микрокомпьютеров... Но не волнуйтесь, это не так сложно (мне кажется, тогда на тесте набрали больше 70 баллов 😅) . Этот шаг может показаться громоздким, но его также можно разделить на два небольших шага, которые очень легко объяснить:
- Пучок
0xF0
ТакойМашинный код, переведенный на более читаемыйассемблерный код. - Выполните его в соответствии со смыслом ассемблерного кода.
От Xiaobawang до GBA, от Apple II до 80x86, все виды аппаратных платформ, которые раньше были массовыми, имеют очень совершенное аппаратное обеспечение.документация для разработчиков. Документация скажет вам что-то вроде этого:
8xy3 - XOR Vx, Vy
Set Vx = Vx XOR Vy.
Что это значит? Основная идея: числовое значение удовлетворяется8xy3
Машинный код , соответствующая ассемблерная инструкция называетсяXOR
. Функция этой команды состоит в том, чтобыVx
Значение регистра устанавливается равнымVx
а такжеVy
Значение после операции XOR (здесь x и y похожи на подстановочные знаки, соответствующие однозначному значению, которое появляется в соответствующей позиции).
Таким образом, мы можем удовлетворить все значения8xy3
машинный код, который транслируется вXOR
Руководство по сборке. Если выразить в виде функции, функция будет выглядеть примерно так:
function convert (num) {
if (num[0] === 8 && num[3] === 3) {
return 'XOR'
}
}
Приведенные выше условия суждения явно неверны (шестнадцатеричные и нижние индексы пишутся вслепую), но его идея очень близка к реальной используемой версии: введите значение машинного кода и определите, что это за инструкция в соответствии с документом, пока напишите куча плоскихelse if
Достаточно, не сложно?
После преобразования значения машинного кода в ассемблерный код все, что нам нужно сделать, это основное содержимое:Выполните его в соответствии со смыслом ассемблерного кода.Для этого требуется очень высококлассный, тонкий, интеллектуальный и общий шаблон проектирования -
Солдаты приходят блокировать, режим покрытия вода-земля
За этим паттерном стоит очень мощная идея программирования, которая подчеркивается в коде.Чего не хватает, что нужно. При написании симуляторов мы руководствуемся этим шаблоном:
- Когда мы увидим, что некоторые ассемблерные коды будут переходить на адреса, мы добавим один.
count
Адресная переменная, смоделируйте адресную информацию и дайте ей измениться. - Видя, что некоторые ассемблерные коды меняют регистры, составим несколько
Vx
Переменная, смоделируйте регистр и дайте ему измениться. - Видя, что некоторые ассемблерные коды могут читать и записывать память, мы добавим один
memory
Массив, смоделируйте память и позвольте ей измениться. - Видя, что некоторые ассемблерные коды изменят массив стека, мы добавим один
stack
массив иpointer
Переменные, имитирующие стек, который может входить и выходить. - ...
Многие люди злоупотребляют этим паттерном, тривиально возясь с каждым небольшим изменением. Здесь наше намерение на самом делеЭто после прочтения документации, выяснения того, что будут изменять все директивы, и имитации этого с помощью переменных.В псевдокоде ассемблерная инструкция, которую мы имитируем, выглядит примерно так:
function ADD (a, b) {
return a + b
}
Сначала мы определяемADD
функция, которая обрабатывается внутри функцииADD
Содержание изменено инструкцией по сборке, поэтому мы имитируем инструкцию по сборке! Фактический код включает в себя некоторые манипуляции с битами, но в целом вы можете рассматривать каждую инструкцию как простую функцию.
После понимания функций этого набора инструкций по сборке последний ключевой вопрос:Как запустить?Мы знаем, что каждый ЦП имеет определенную рабочую частоту, и после запуска он будет постоянно выполнять инструкции в соответствии с этой частотой. Следовательно, мы можем смоделировать этот способ работы процессора как бесконечный цикл:
while (true) {
// 读取下一条指令。
const ins = nextIns()
// 将指令喂给 CPU 执行。
cpu.run(ins)
}
Отлично! Пока что мы знаем, что для написания эмулятора на JavaScript нам нужно всего лишь:
- Эмулирует функциональность инструкции с помощью функции.
- Эмулируйте регистры, память и стек с переменными.
- Моделируйте работу ЦП с помощью циклов.
Этого достаточно для запуска эмулятора? Это не так просто... просто держись!
закрыть дверь холодильника: Как обрабатывать ввод и вывод?
Студенты, знакомые с функциональным программированием, должны понимать концепцию [побочных эффектов]. Побочные эффекты представляют собой вещи, которые не являются чистыми в дополнение ко всем вычислениям.Наиболее типичными побочными эффектами являются [вход] и [выход].
Если побочных эффектов нет, то эмулятор, несомненно, попадет в бесконечный цикл (например, если пользователь откроет игру, не нажимая клавишу, интерфейс зависнет вPress Start to Continue
например, титульный экран не двигается). Следовательно, нам нужно реализовать некоторый механизм на основе предыдущего шага, чтобы правильно обрабатывать ввод и вывод.
В семантике JavaScript у нас естьsetTimeout
Концепция чего-либо. С помощью таймеров мы можем превратить синхронный код в асинхронное выполнение. Симуляция постоянных вычислений ЦП заблокировала бы наш основной поток, поэтому для симулятора реального мира нам пришлось бы использовать некоторые асинхронные приемы, чтобы освободить место для ввода и вывода. Этот процесс можно упростить до:
- поставь оригинал
while
постоянное выполнение командСинхронизироватьБесконечный цикл становится циклом, выполняющим несколько инструкций через равные промежутки времени.асинхронныйцикл. - Настройте прослушиватели событий, когда нажимается определенная кнопка,Изменить состояние эмулятора(В этот момент цикл ЦП приостанавливается таймером).
- Каждый раз, когда запускается асинхронный цикл ЦП и выполняются некоторые инструкции для оценки состояния ввода, можно получить состояние, измененное входным событием.
С этим мы решили проблему ввода! Проблема вывода намного проще: просто визуализируйте Canvas, пока ЦП выполняет инструкции вывода. Кроме того, вы можете запустить другой таймер для непрерывной визуализации состояния экрана.
На данный момент мы представили реализацию следующих основных функций симулятора:
- Как читать ROM-файлы.
- Инструкции по имитации работающей машины.
- Как обрабатывать ввод и вывод.
Теоретического уровня уже достаточно, а ниже реальный бой.
Введение в Чип-8
нашMerry8Симулятор реализуетChip-8Язык ассемблера. Это средневековый язык 1970-х годов. В отличие от ассемблера 6502, используемого Xiaobawang NES, Chip-8 не имеет официальной аппаратной реализации.Пока полный набор инструкций реализован в соответствии с его спецификациями, он может запускать совместимое ПЗУ. Благодаря своей простой конструкции он очень удобен в качествеВводный язык для разработки симуляторов.
Ресурсы, доступные интерпретатору, совместимому с Chip-8 (или виртуальной машине, эмулятору...), включают:
- 4 КБ памяти
- 16 16-битных регистров от V0 до V15
- счетчик ПК
- индексный регистр I
- указатель стека SP
- таймер задержки
- 16-клавишная клавиатура
- регистр эффекта
- Черно-белый экран 64x32.
Основываясь на приведенном выше справочном введении, мы можем естественным образом абстрагировать эти ресурсы в JavaScript:
- Память и стек: храните непрерывные данныеМассив ArrayBuffer!
- Регистры и счетчики: представляет соответствующее значениечисловая переменная!
- Клавиатура: указывает, нажата ли каждая клавишамассив логических значений!
- Черно-белый экран:логический 2D-массив, представляющий цвет!
Для инструкций базовая спецификация Chip8 содержит в общей сложности 35 инструкций.Хотя длина каждой инструкции фиксирована и составляет 2 байта, форматы параметров в разных инструкциях различаются. Например, машинный код прочитанной инструкции60 12
, весь процесс обработки:
- соответствует инструкции
6xkk
инструкция, котораяLD
(Загрузить), первый операнд равен 0, а второй операнд равен 12. - Согласно документации,
0x12
Этот операнд записывается в регистр V0.
Поняв значение этой инструкции, мы можем смоделировать логику ее инструкций для управления смоделированными аппаратными ресурсами. Переписав эти 35 инструкций, мы можем реализовать весь симулятор.
Детали реализации для каждой инструкции, см.Чип-8 Документацияа такжеИсходный код процессора симулятораЗдесь есть соответствующая информация, поэтому я не буду повторять их здесь.
Архитектура эмулятора Merry8
Эмулятор Merry8 является автором впрошлое РождествоВзял выходные, чтобы достичь. Это тожеMerry
Происхождение имени. Однако, учитывая, что на тот момент опыта работы с фронтендом было менее полугода, он не был элегантен в некоторых инженерных деталях и был ближе к приложению, чем библиотека классов в целом, и после этого им пренебрегали. написать его и выложить на Github. В сезон белого альбома, после оптимизации некоторой структуры кода, теперь это колесо с удобным API и четкой структурой модулей, основные модули включают в себя:
-
disassembler
Модуль, отвечающий за дизассемблирование машинного кода в читаемый формат. -
ops
Модуль, который инкапсулирует логику инструкций процессора Chip-8. -
view
Модуль, отвечающий за отрисовку состояния в Canvas.
Работа всего симулятора в основном такая же, как описано выше.Чтобы было понятно в одном предложении, этоЛогика обработки инструкций в асинхронном цикле.
в самом последнемv0.3.0
В обновлении проясняется взаимосвязь между несколькими модулями на основе базового способа ОО: можно пройтиnew Merry8()
Создайте новый экземпляр эмулятора, каждый экземпляр эмулятора имеет свой собственный виртуальный процессор, память, указатель стека, регистры и контекст Canvas. Таким образом, вы можете легко создавать несколько эмуляторов на одной странице, загружать разные ПЗУ и иметь разные элементы управления.
С точки зрения фронтенд-инжиниринга в этой игрушке также есть несколько надежных практик:
- Сводная сборка.
- Lint в стиле StandardJS.
- Реализованы модульные тесты для нескольких дизассемблированных функций и инструкций процессора.
В настоящее время Merry8 все еще находится в состоянии бета-тестирования, его совместимость с играми не идеальна, а тестовое покрытие также очень слабое, но если вы заинтересованы в участии в его улучшении, вы можете отправить PR!
Суммировать
Нет никаких сомнений в том, что Merry8, каким бы совершенным он ни был, не более чем игрушка. Так почему же я готов приложить столько усилий, чтобы реализовать, поддерживать и внедрять его всерьез? Я могу назвать несколько причин:
- Симулятор разработки — этоУзнайте, как работают основы работы с компьютеромхороший способ. Мало того, что забавно легко показывать результаты, это также позволяет вам изучить основы того, как работают инструкции, как распределяется память и как увеличивается и уменьшается стек.
- Написание эмулятора важнее любого другого способа понимания лежащей в его основе технологии.Меньшее бремя мышления. Например, вам не нужно учиться использовать язык программирования низкого уровня, такой как C или C++. Обратите внимание, что вам не нужно уметь писать на ассемблере для разработки эмулятора, и я до сих пор не использую ассемблер Chip-8 для написания игр, достаточно просто знать, что делает каждая инструкция.
- Это дает вам реальное чувствоПродумывание структуры модуля в соответствии с документациейВозможность. В отличие от связующего кода [формат входного параметра, формат выходного параметра], который ежедневно пишется в соответствии с документом интерфейса, вы хотите получить копиютехнические характеристики. Не забывайте, что ECMA-262, который многие съели, тоже просто техническая спецификация.
- это может тренировать васОтладка и модульное тестированиеСпособность. В цикле, который выполняется тысячи раз в секунду,Небольшое смещение одной инструкции может вывести из строя весь эмулятор. Итак, вам нужно использовать модульные тесты, чтобы убедиться в правильности каждой инструкции, и использовать сравнения, когда что-то идет не так.
console.log
Более надежная технология отладки для обнаружения и решения. - это может воспитать васДиагностика узких мест производительностии оптимизированный способ мышления. Например, в первой версии загрузка ЦП эмулятора всегда была полной. Я думал, что проблема в логике синхронизации, но после профилирования я обнаружил, что проблема была в слое рендеринга: в то время использовалась отрисовка DOM, и полное обновление 60 кадров в секунду для тысяч узлов DOM 64X32 уже перегрузило браузер. После перехода на Canvas проблема с загрузкой процессора была решена плавно.
Наконец, мы должны упомянуть, что во время отладки ПЗУ эмулятора у вас будет больше страха перед технологиями и историей. Не чувствуйте в пределах 3КБ, понимаете, насколько великолепен симулятор, вы знаете, что это всего лишь имитация игры PONG.246 байт! Бог его знает, как его автор реализует коллизию, скоринг и IO-взаимодействие на пространстве более 200 байт,Может быть, это божественная работа программистов в древние времена..
Если тема этой статьи покажется вам интересной, в моемGithubЕсть несколько подобных игрушек, которые нацелены на реализацию основ компиляторов, интерпретаторов, интерфейсных фреймворков и т. д. с максимально простой логикой. Добро пожаловать, чтобы подписаться 😀