Pull to refresh

Comments 21

Несколько мыслей / предложений / рекомендаций:

  1. Советую сделать автоматический "тестовый стенд", куда подставляешь очередную версию "ядра эмуляции" (библиотеку в вашем случае), прогоняется ряд тестов, которыми определяется, что вы не получили регрессии. Например, для нескольких своих эмуляторов я делал тесты вида "загрузить вот это, подождать, нажать клавишу, подождать, сделать скриншот и проверить его совпадение с эталоном". Постепенно накапливать базу таких тестов.

  2. Подумать про трейсинг -- задаём например число uint32 под маску что трассировать: команды CPU, обращения к внешним устройствам итд. Собрать трассу бывает весьма полезно при отладке.

  3. Подумать про API для отладчика. Это может быть набор вызовов. И/или это может быть полноценный GDB stub - позволит отлаживаться по GDB протоколу, для него есть UI инструменты.

  4. Делать отладчик, например, в виде консольного диалога. Отдельными командами можно запустить выполнение, управлять точками останова, смотреть регистры и память, сохранить/загрузить полное состояние эмулятора. Отладчик поможет в сложных случаях. Можно проходить инструкции "параллельной отладкой" в двух эмуляторах, сравнивая результаты.

  5. Реализовать сохранение состояний и их загрузку, убедиться что всё состояние эмулятора точно сохраняется и восстанавливается.

  6. Подумать про кросс-платформенность, чтобы библиотека собиралась и одинаково полноценно работала под Linux/Mac/Windows, а возможно и под ARM или Wasm.

Пока вы используете SDL для UI, как я понял. Посмотрите в сторону ImGui, им можно делать крутые инструменты визуализации и отладки.

Спасибо за такой содержательный совет. Очень интересно будет всё это реализовать.

Ну и стоит конечно посматривать как другие люди подобные эмуляторы пишут, например:

Стоит также глянуть, как в проекте MESS организовано разделение устройств и систем, подход к исчислению времени.

Как достичь максимальной производительности байткод-машины? Недавно на эту тему здесь была жаркая дискуссия (рекомендую читать по порядку: раз, два, три).

Вообще не в тему комментарий. Эмулятор конкретного железа не является байткод-машиной, потому что должен повторять потактово время выполнения инструкций эмулируемого железа, нет никакой необходимости добиваться некоей "максимальной" производительности.

Прошу прояснить чем эмуляция "чужой" системы команд принципиально отличается от вирутальной байт-код машины ?

По сути, автор уже почти реализовал то же самое, что обсуждалось "в горячей дискуссии".

Прошу прояснить чем эмуляция "чужой" системы команд принципиально отличается от вирутальной байт-код машины ?

Дискуссию не читал, но могу ответить как понимаю данный вопрос. Эмуляция NES к примеру требует соблюдения ждать определенное время, пока такты циклов пройдут, а это значит, что нужно ждать какое-то время. Если сделать так, чтобы выполнялось как можно быстрее код, то игра будет очень быстро играться и мы увидим, что игра ведет себя не так как на реальных консолях. В эмуляции байткода, например как в java, там уже не нужно ждать циклы процессора, там просто надо как можно быстрее выполнить код и поэтому затрачиваются средства на то, чтобы код выполнялся как можно быстрее. Я так думаю.

А если рассматривать задержку по времени как еще один опкод для VM который будет вставляться (вызываться неявно) после исполнения каждого опкода эмулируемой системы команд ?

На мой взгляд switch/case это очень плохое решение независимо от задачи. Таблица указателей более вреное решение - с ней проще работать, добавлять/удалять имплементации, экспериментировать с вариантами исполнения. В случае с длинным case код быстро превратится в спагетти перемешанный с временно закоментированными кусками, ifdef-ами и прочей шелухой.

Самый простой способ написать эмулятор и виртуальную машину - это написать интерпретатор, с центральным бесконечным циклом, внутри которого будет происходить выборка команд с последующим switch-ем по опкодам. В случае эмулятора нет смысла делать что-то другое (типа threaded code), потому что после каждой команды нужно проделать некоторые общие вещи: посчитать число тактов, проверить, не возникло ли прерывание, проэмулировать другие компоненты, помимо CPU. Кроме того, архитектура реальных машин такова, что код хранится в памяти, то есть может быть изменён в процессе выполнения, и стек тоже находится в памяти. Это значит, что JIT/AOT применять не получится. А виртуальная машина - она "виртуальная", потому что у неё, как правило, отдельно память данных, отдельно хранилище кода, отдельно локальные переменные, отдельно стек возвратов, отдельно стек операций. То есть высокоуровневые вещи, ограничивающие всякие трюки в коде, но позволяющие JIT/AOT и прочие оптимизации.

То есть для эмуляторов отказываться от цикла со switch-ем - это, с одной стороны, усложнение кода, а с другой стороны - вообще не нужно, всё равно скорость и задержки надо эмулировать.

Тогда я думал так, в switch выборка идет по бинарному поиску, но я не был уверен в том, будет ли такой же вестись поиск, если числа будут перемешаны в case.

А чем это было обосновано?

Может вы заглянули в дизассемблер, или хотя бы проверили производительность?

То, что вы сделали отдельные функции не позволяет компилятору оптимизировать код, расположив нужные данные в регистры. Конечно, если всё в одном файле, то компилятор может попробовать инлайнить ваш код, чтобы сделать аналог первоначального switch-case.

https://godbolt.org/z/9x5GTd5vv

Хотя, конечно, для потактового эмулятора древних консолей это может быть непринципиально.

Может вы заглянули в дизассемблер, или хотя бы проверили производительность?

То, что вы сделали отдельные функции не позволяет компилятору оптимизировать код, расположив нужные данные в регистры. Конечно, если всё в одном файле, то компилятор может попробовать инлайнить ваш код, чтобы сделать аналог первоначального switch-case.

Да, раньше смотрел в дизассемблер как switch устроен, даже в ссылке, что вы в godbolt привели, там видно, что с оптимизацией в -O3 конечно будет быстрее. Я же смотрел на код, который был всегда без оптимизации. Вот код запуска из массива функций, решил тоже в оптимизации -O3 скомпилировать.

  ─└─└────> 0x000057f0      0fb75304       movzx edx, word [rbx + 4]   ; cpunes.c:664  uint16_t real_pos = emu->cpu.PC - 0x8000;
  │ ╎ ╎││   0x000057f4      6681c20080     add dx, 0x8000
  │ ╎ ╎││   0x000057f9      488b05c8d1..   mov rax, qword [obj.pnes_handler] ; cpunes.c:666  pnes_handler [emu->mem[real_pos]] (emu); ; [0x129c8:8]=0
  │ ╎ ╎││   0x00005800      4889df         mov rdi, rbx
  │ ╎ ╎││   0x00005803      0fb7d2         movzx edx, dx
  │ ╎ ╎││   0x00005806      0fb69413b0..   movzx edx, byte [rbx + rdx + 0x114b0]
  │ ╎ ╎││   0x0000580e      ff14d0         call qword [rax + rdx*8]

Я хотел сам оптимизировать код, без компилятора. Мне нравиться заниматься оптимизацией и я не уверен, что лучше делать обычный код через switch, кто знает как для arm или arduino компилятор поведет себя. Хотя для arm есть gcc, но для arduino наверное другой компилятор. Хотя так то да, в switch можно было бы инлайнить прямо в case все функции. Но инлайнинг обозначает, что код будет разрастаться, вместо того, чтобы вызывать функцию с одинаковым кодом. Хотя в моем случае я использую макросы, так что код итак будет такой какой есть.

Я сейчас борюсь с тем, чтобы сделать код как-то компактнее и не сильно смотрю на оптимизирующий компилятор. Если бы не вычет из 0x8000, то кода было бы меньше, так как над всего лишь нужно было бы получить указатель на массив функций и вызывать её.

Думаю, что вы правы, но я переделывать в switch уже точно не буду. На тот момент мне показалось, что идея с массивом функций будет работать быстрее. Тогда код был проще. И опять же я не смотрел на оптимизацию со стороны компилятора.

256 операций в свитче не так много, в своём большинстве всё влезет в один кэш (и уж точно в любой кэш новых процессоров).

Я тоже думал "оптимизировать" подобным образом, но оставил свитч, понимая, что лучшая оптимизация будет на ассемблере. Остальная "оптимизация" на ЯВУ даже ни в какое сравнение не пойдёт.

По статье, извиняюсь, но я ожидал большей выкладки информации, а так получается статья просто рассказать о том что у тебя получилось эмулировать Nes. Молодец, но лично я ожидал большего.

Следующая статья может вам тоже не понравиться, там вся статья с описанием как я искал баг, вот, только что выпустил.

https://habr.com/ru/articles/877372/

Но я пока не могу о технических моментах тоже рассказывать, так как меняю иногда реализацию. Это цикл статей как я пишу эмулятор. Да, извиняюсь, мало технических аспектов, но и кода тоже мало. Как только сделаю уже готовый продукт, то можно написать хорошую статью о том, что и как устроено.

Я же смотрел на код, который был всегда без оптимизации

А зачем его смотреть? Код без оптимизации нужен ТОЛЬКО для отладки.

Он раздутый и очень медленный.

Я хотел сам оптимизировать код, без компилятора. 

Вы разорвали мне шаблон.

Эффективная кодгенерация никак у вас не отбирает возможность оптимизации.

Компилятор за вас практически ничего не сделает, но может сэкономить время.

Я сейчас борюсь с тем, чтобы сделать код как-то компактнее и не сильно смотрю на оптимизирующий компилятор.

Вы делаете совершенно противоположные этому вещи.

Такая оптимизация начинается с ключа -Os

https://godbolt.org/z/EGz77be6e

дополню, компактность (даже ассемблерного кода) не всегда лучшее решение.

В тему разработки для NES и других ретроплатформ, дам ссылку на https://8bitworkshop.com/, где уже существует среда разработки.
Так же автор сайта написал книги посвященные разработке игр для 8-битных компьютеров и приставок на Си, а также написал книгу посвященную разработке игр на NES.

Making 8-Bit Arcade Games in C

With this book, you'll learn all about the hardware of Golden Age 8-bit arcade games produced in the late 1970s to early 1980s. We'll learn how to use the C programming language to write code for the Z80 CPU. The following arcade platforms are covered: * Midway 8080 (Space Invaders) * VIC Dual (Carnival) * Galaxian/Scramble (Namco) * Atari Color Vector * Williams (Defender, Robotron) We'll describe how to create video and sound for each platform.

Making Games For The NES

Learn how to program the NES in C using the NESLib library! We'll show you how to uncompress tile maps, scroll the screen, animate sprites, create a split status bar, play background music and sound effects and more. We'll write some 6502 assembly language too, programming the PPU and APU directly. We'll use different "mappers" which add bank-switching and IRQs to cartridges, producing advanced psuedo-3D raster effects.

Так же, тему программирования на ассемблере для NES раскрывает Keith 'Akuyou' в разделе посвященному программированию для 6502.

С учетом количества "invalid_opcode" я бы лучше сделал компактную таблицу, а потом бы уже в райнтайме разворачивал бы ее в таблицу на 256 элементов.

И команды в этой таблице можно было бы описать более изощренным образом, например, описать тип аргументов и исходя из этого сделать разные типы хендлеров по сигнатуре.

Макросы зло...

Я макросы уже убрал. Да, в который раз спотыкаюсь об это. Что первый эмулятор делал, там были макросы, отлаживать трудно было в tui. Переделал на обычные функции.
И вот опять делал с макросами. Но вчера исправлял несколько часов, так что сейчас работает лучше, даже марио находит, но не прыгает. Пока не знаю как этот баг отловить.

Всё посещают мысли сделать тесты, но я боюсь, что это займет целый день. Надо настроиться, ведь до этого я с радостью занимался такой рутиной.

Sign up to leave a comment.

Articles