Добро пожаловать дорогой читатель. Сегодня я расскажу о проекте, который я разрабатываю некоторое время. Это эмулятор NES. Создаю его в виде библиотеки, чтобы можно было добавить его в свою сборку эмулятора, игры или залить в микроконтроллёр. В целом, меня тянет именно в электронику, которую я мечтаю постичь уже не один год. Сама разработка игр для NES меня не очень интересует, да и в игры я не особо люблю поиграть, а вот создавать какие-то сложные вещи мне хочется всё больше и больше, чем возиться с простыми задачами. До этого я начал разрабатывать эмулятор i386 intel процессора, но понял, что не хватает сил продолжать проект и нужно выбрать что-то более простое, и это простое как мне казалось, была разработка эмулятора NES. И так я начал его разработку.
Первым делом я нашел документацию и принялся расписывать в switch case ... опкоды. В отличии от i386 здесь каждый опкод имел один байт, что упрощало разработку. Начал этот проект в октябре 2024 года, а потом забросил. Когда вновь занялся им, то я почувствовал, что у меня пропало такое ощущение, когда тебе неинтересен проект, потому что он скучный в разработке. Это ощущение пропало и я просто начал писать и писать код. В первый день я написал все case в switch, а потом понял, что это будет медленно работать. Тогда я думал так, в switch выборка идет по бинарному поиску, но я не был уверен в том, будет ли такой же вестись поиск, если числа будут перемешаны в case.
На следующий день я решил переписать это творчество так, чтобы любой опкод выполнялся всего лишь вот так.
pnes_handler [emu->mem[emu->cpu.PC]] (emu);
Это стоило того, я потратил около часа или двух, пока вписывал все 256 строки кода в инициализацию, которая выполняется один раз. Вот она.
if (!is_init_global_func) {
DEFINE_STATIC_FUNC_NES_HANDLER ()
ADD_HANDLER (brk_implied) /* 0x00 */
ADD_HANDLER (ora_indirect_x) /* 0x01 */
ADD_HANDLER (invalid_opcode) /* 0x02 */
ADD_HANDLER (invalid_opcode) /* 0x03 */
ADD_HANDLER (invalid_opcode) /* 0x04 */
ADD_HANDLER (ora_zeropage) /* 0x05 */
ADD_HANDLER (asl_zeropage) /* 0x06 */
ADD_HANDLER (invalid_opcode) /* 0x07 */
ADD_HANDLER (php_implied) /* 0x08 */
ADD_HANDLER (ora_immediate) /* 0x09 */
ADD_HANDLER (asl_accumulator) /* 0x0a */
ADD_HANDLER (invalid_opcode) /* 0x0b */
ADD_HANDLER (invalid_opcode) /* 0x0c */
ADD_HANDLER (ora_absolute) /* 0x0d */
ADD_HANDLER (asl_absolute) /* 0x0e */
ADD_HANDLER (invalid_opcode) /* 0x0f */
ADD_HANDLER (bpl_relative) /* 0x10 */
ADD_HANDLER (ora_indirect_y) /* 0x11 */
ADD_HANDLER (invalid_opcode) /* 0x12 */
ADD_HANDLER (invalid_opcode) /* 0x13 */
ADD_HANDLER (invalid_opcode) /* 0x14 */
ADD_HANDLER (ora_zeropage_x) /* 0x15 */
ADD_HANDLER (asl_zeropage_x) /* 0x16 */
ADD_HANDLER (invalid_opcode) /* 0x17 */
ADD_HANDLER (clc_implied) /* 0x18 */
ADD_HANDLER (ora_absolute_y) /* 0x19 */
ADD_HANDLER (invalid_opcode) /* 0x1a */
ADD_HANDLER (invalid_opcode) /* 0x1b */
ADD_HANDLER (invalid_opcode) /* 0x1c */
ADD_HANDLER (ora_absolute_x) /* 0x1d */
ADD_HANDLER (asl_absolute_x) /* 0x1e */
ADD_HANDLER (invalid_opcode) /* 0x1f */
ADD_HANDLER (jsr_absolute) /* 0x20 */
ADD_HANDLER (and_indirect_x) /* 0x21 */
ADD_HANDLER (invalid_opcode) /* 0x22 */
ADD_HANDLER (invalid_opcode) /* 0x23 */
ADD_HANDLER (bit_zeropage) /* 0x24 */
ADD_HANDLER (and_zeropage) /* 0x25 */
ADD_HANDLER (rol_zeropage) /* 0x26 */
ADD_HANDLER (invalid_opcode) /* 0x27 */
ADD_HANDLER (plp_implied) /* 0x28 */
ADD_HANDLER (and_immediate) /* 0x29 */
ADD_HANDLER (rol_accumulator) /* 0x2a */
ADD_HANDLER (invalid_opcode) /* 0x2b */
ADD_HANDLER (bit_absolute) /* 0x2c */
ADD_HANDLER (and_absolute) /* 0x2d */
ADD_HANDLER (rol_absolute) /* 0x2e */
ADD_HANDLER (invalid_opcode) /* 0x2f */
ADD_HANDLER (bmi_relative) /* 0x30 */
ADD_HANDLER (and_indirect_y) /* 0x31 */
ADD_HANDLER (invalid_opcode) /* 0x32 */
ADD_HANDLER (invalid_opcode) /* 0x33 */
ADD_HANDLER (invalid_opcode) /* 0x34 */
ADD_HANDLER (and_zeropage_x) /* 0x35 */
ADD_HANDLER (rol_zeropage_x) /* 0x36 */
ADD_HANDLER (invalid_opcode) /* 0x37 */
ADD_HANDLER (sec_implied) /* 0x38 */
ADD_HANDLER (and_absolute_y) /* 0x39 */
ADD_HANDLER (invalid_opcode) /* 0x3a */
ADD_HANDLER (invalid_opcode) /* 0x3b */
ADD_HANDLER (invalid_opcode) /* 0x3c */
ADD_HANDLER (and_absolute_x) /* 0x3d */
ADD_HANDLER (rol_absolute_x) /* 0x3e */
ADD_HANDLER (invalid_opcode) /* 0x3f */
ADD_HANDLER (rti_implied) /* 0x40 */
ADD_HANDLER (eor_indirect_x) /* 0x41 */
ADD_HANDLER (invalid_opcode) /* 0x42 */
ADD_HANDLER (invalid_opcode) /* 0x43 */
ADD_HANDLER (invalid_opcode) /* 0x44 */
ADD_HANDLER (eor_zeropage) /* 0x45 */
ADD_HANDLER (lsr_zeropage) /* 0x46 */
ADD_HANDLER (invalid_opcode) /* 0x47 */
ADD_HANDLER (pha_implied) /* 0x48 */
ADD_HANDLER (eor_immediate) /* 0x49 */
ADD_HANDLER (lsr_accumulator) /* 0x4a */
ADD_HANDLER (invalid_opcode) /* 0x4b */
ADD_HANDLER (jmp_absolute) /* 0x4c */
ADD_HANDLER (eor_absolute) /* 0x4d */
ADD_HANDLER (lsr_absolute) /* 0x4e */
ADD_HANDLER (invalid_opcode) /* 0x4f */
ADD_HANDLER (bvc_relative) /* 0x50 */
ADD_HANDLER (eor_indirect_y) /* 0x51 */
ADD_HANDLER (invalid_opcode) /* 0x52 */
ADD_HANDLER (invalid_opcode) /* 0x53 */
ADD_HANDLER (invalid_opcode) /* 0x54 */
ADD_HANDLER (eor_zeropage_x) /* 0x55 */
ADD_HANDLER (lsr_zeropage_x) /* 0x56 */
ADD_HANDLER (invalid_opcode) /* 0x57 */
ADD_HANDLER (cli_implied) /* 0x58 */
ADD_HANDLER (eor_absolute_y) /* 0x59 */
ADD_HANDLER (invalid_opcode) /* 0x5a */
ADD_HANDLER (invalid_opcode) /* 0x5b */
ADD_HANDLER (invalid_opcode) /* 0x5c */
ADD_HANDLER (eor_absolute_x) /* 0x5d */
ADD_HANDLER (lsr_absolute_x) /* 0x5e */
ADD_HANDLER (invalid_opcode) /* 0x5f */
ADD_HANDLER (rts_implied) /* 0x60 */
ADD_HANDLER (adc_indirect_x) /* 0x61 */
ADD_HANDLER (invalid_opcode) /* 0x62 */
ADD_HANDLER (invalid_opcode) /* 0x63 */
ADD_HANDLER (invalid_opcode) /* 0x64 */
ADD_HANDLER (adc_zeropage) /* 0x65 */
ADD_HANDLER (ror_zeropage) /* 0x66 */
ADD_HANDLER (invalid_opcode) /* 0x67 */
ADD_HANDLER (pla_implied) /* 0x68 */
ADD_HANDLER (adc_immediate) /* 0x69 */
ADD_HANDLER (ror_accumulator) /* 0x6a */
ADD_HANDLER (invalid_opcode) /* 0x6b */
ADD_HANDLER (jmp_indirect) /* 0x6c */
ADD_HANDLER (adc_absolute) /* 0x6d */
ADD_HANDLER (ror_absolute) /* 0x6e */
ADD_HANDLER (invalid_opcode) /* 0x6f */
ADD_HANDLER (bvs_relative) /* 0x70 */
ADD_HANDLER (adc_indirect_y) /* 0x71 */
ADD_HANDLER (invalid_opcode) /* 0x72 */
ADD_HANDLER (invalid_opcode) /* 0x73 */
ADD_HANDLER (invalid_opcode) /* 0x74 */
ADD_HANDLER (adc_zeropage_x) /* 0x75 */
ADD_HANDLER (ror_zeropage_x) /* 0x76 */
ADD_HANDLER (invalid_opcode) /* 0x77 */
ADD_HANDLER (sei_implied) /* 0x78 */
ADD_HANDLER (adc_absolute_y) /* 0x79 */
ADD_HANDLER (invalid_opcode) /* 0x7a */
ADD_HANDLER (invalid_opcode) /* 0x7b */
ADD_HANDLER (invalid_opcode) /* 0x7c */
ADD_HANDLER (adc_absolute_x) /* 0x7d */
ADD_HANDLER (ror_absolute_x) /* 0x7e */
ADD_HANDLER (invalid_opcode) /* 0x7f */
ADD_HANDLER (invalid_opcode) /* 0x80 */
ADD_HANDLER (sta_indirect_x) /* 0x81 */
ADD_HANDLER (invalid_opcode) /* 0x82 */
ADD_HANDLER (invalid_opcode) /* 0x83 */
ADD_HANDLER (sty_zeropage) /* 0x84 */
ADD_HANDLER (sta_zeropage) /* 0x85 */
ADD_HANDLER (stx_zeropage) /* 0x86 */
ADD_HANDLER (invalid_opcode) /* 0x87 */
ADD_HANDLER (dey_implied) /* 0x88 */
ADD_HANDLER (invalid_opcode) /* 0x89 */
ADD_HANDLER (txa_implied) /* 0x8a */
ADD_HANDLER (invalid_opcode) /* 0x8b */
ADD_HANDLER (sty_absolute) /* 0x8c */
ADD_HANDLER (sta_absolute) /* 0x8d */
ADD_HANDLER (stx_absolute) /* 0x8e */
ADD_HANDLER (invalid_opcode) /* 0x8f */
ADD_HANDLER (bcc_relative) /* 0x90 */
ADD_HANDLER (sta_indirect_y) /* 0x91 */
ADD_HANDLER (invalid_opcode) /* 0x92 */
ADD_HANDLER (invalid_opcode) /* 0x93 */
ADD_HANDLER (sty_zeropage_x) /* 0x94 */
ADD_HANDLER (sta_zeropage_x) /* 0x95 */
ADD_HANDLER (stx_zeropage_y) /* 0x96 */
ADD_HANDLER (invalid_opcode) /* 0x97 */
ADD_HANDLER (tya_implied) /* 0x98 */
ADD_HANDLER (sta_absolute_y) /* 0x99 */
ADD_HANDLER (txs_implied) /* 0x9a */
ADD_HANDLER (invalid_opcode) /* 0x9b */
ADD_HANDLER (invalid_opcode) /* 0x9c */
ADD_HANDLER (sta_absolute_x) /* 0x9d */
ADD_HANDLER (invalid_opcode) /* 0x9e */
ADD_HANDLER (invalid_opcode) /* 0x9f */
ADD_HANDLER (ldy_immediate) /* 0xa0 */
ADD_HANDLER (lda_indirect_x) /* 0xa1 */
ADD_HANDLER (ldx_immediate) /* 0xa2 */
ADD_HANDLER (invalid_opcode) /* 0xa3 */
ADD_HANDLER (ldy_zeropage) /* 0xa4 */
ADD_HANDLER (lda_zeropage) /* 0xa5 */
ADD_HANDLER (ldx_zeropage) /* 0xa6 */
ADD_HANDLER (invalid_opcode) /* 0xa7 */
ADD_HANDLER (tay_implied) /* 0xa8 */
ADD_HANDLER (lda_immediate) /* 0xa9 */
ADD_HANDLER (tax_implied) /* 0xaa */
ADD_HANDLER (invalid_opcode) /* 0xab */
ADD_HANDLER (ldy_absolute) /* 0xac */
ADD_HANDLER (lda_absolute) /* 0xad */
ADD_HANDLER (ldx_absolute) /* 0xae */
ADD_HANDLER (invalid_opcode) /* 0xaf */
ADD_HANDLER (bcs_relative) /* 0xb0 */
ADD_HANDLER (lda_indirect_y) /* 0xb1 */
ADD_HANDLER (invalid_opcode) /* 0xb2 */
ADD_HANDLER (invalid_opcode) /* 0xb3 */
ADD_HANDLER (ldy_zeropage_x) /* 0xb4 */
ADD_HANDLER (lda_zeropage_x) /* 0xb5 */
ADD_HANDLER (ldx_zeropage_y) /* 0xb6 */
ADD_HANDLER (invalid_opcode) /* 0xb7 */
ADD_HANDLER (clv_implied) /* 0xb8 */
ADD_HANDLER (lda_absolute_y) /* 0xb9 */
ADD_HANDLER (tsx_implied) /* 0xba */
ADD_HANDLER (invalid_opcode) /* 0xbb */
ADD_HANDLER (ldy_absolute_x) /* 0xbc */
ADD_HANDLER (lda_absolute_x) /* 0xbd */
ADD_HANDLER (ldx_absolute_y) /* 0xbe */
ADD_HANDLER (invalid_opcode) /* 0xbf */
ADD_HANDLER (cpy_immediate) /* 0xc0 */
ADD_HANDLER (cmp_indirect_x) /* 0xc1 */
ADD_HANDLER (invalid_opcode) /* 0xc2 */
ADD_HANDLER (invalid_opcode) /* 0xc3 */
ADD_HANDLER (cpy_zeropage) /* 0xc4 */
ADD_HANDLER (cmp_zeropage) /* 0xc5 */
ADD_HANDLER (dec_zeropage) /* 0xc6 */
ADD_HANDLER (invalid_opcode) /* 0xc7 */
ADD_HANDLER (iny_implied) /* 0xc8 */
ADD_HANDLER (cmp_immediate) /* 0xc9 */
ADD_HANDLER (dex_implied) /* 0xca */
ADD_HANDLER (invalid_opcode) /* 0xcb */
ADD_HANDLER (cpy_absolute) /* 0xcc */
ADD_HANDLER (cmp_absolute) /* 0xcd */
ADD_HANDLER (dec_absolute) /* 0xce */
ADD_HANDLER (invalid_opcode) /* 0xcf */
ADD_HANDLER (bne_relative) /* 0xd0 */
ADD_HANDLER (cmp_indirect_y) /* 0xd1 */
ADD_HANDLER (invalid_opcode) /* 0xd2 */
ADD_HANDLER (invalid_opcode) /* 0xd3 */
ADD_HANDLER (invalid_opcode) /* 0xd4 */
ADD_HANDLER (cmp_zeropage_x) /* 0xd5 */
ADD_HANDLER (dec_zeropage_x) /* 0xd6 */
ADD_HANDLER (invalid_opcode) /* 0xd7 */
ADD_HANDLER (cld_implied) /* 0xd8 */
ADD_HANDLER (cmp_absolute_y) /* 0xd9 */
ADD_HANDLER (invalid_opcode) /* 0xda */
ADD_HANDLER (invalid_opcode) /* 0xdb */
ADD_HANDLER (invalid_opcode) /* 0xdc */
ADD_HANDLER (cmp_absolute_x) /* 0xdd */
ADD_HANDLER (dec_absolute_x) /* 0xde */
ADD_HANDLER (invalid_opcode) /* 0xdf */
ADD_HANDLER (cpx_immediate) /* 0xe0 */
ADD_HANDLER (sbc_indirect_x) /* 0xe1 */
ADD_HANDLER (invalid_opcode) /* 0xe2 */
ADD_HANDLER (invalid_opcode) /* 0xe3 */
ADD_HANDLER (cpx_zeropage) /* 0xe4 */
ADD_HANDLER (sbc_zeropage) /* 0xe5 */
ADD_HANDLER (inc_zeropage) /* 0xe6 */
ADD_HANDLER (invalid_opcode) /* 0xe7 */
ADD_HANDLER (inx_implied) /* 0xe8 */
ADD_HANDLER (sbc_immediate) /* 0xe9 */
ADD_HANDLER (nop_implied) /* 0xea */
ADD_HANDLER (invalid_opcode) /* 0xeb */
ADD_HANDLER (cpx_absolute) /* 0xec */
ADD_HANDLER (sbc_absolute) /* 0xed */
ADD_HANDLER (inc_absolute) /* 0xee */
ADD_HANDLER (invalid_opcode) /* 0xef */
ADD_HANDLER (beq_relative) /* 0xf0 */
ADD_HANDLER (sbc_indirect_y) /* 0xf1 */
ADD_HANDLER (invalid_opcode) /* 0xf2 */
ADD_HANDLER (invalid_opcode) /* 0xf3 */
ADD_HANDLER (invalid_opcode) /* 0xf4 */
ADD_HANDLER (sbc_zeropage_x) /* 0xf5 */
ADD_HANDLER (inc_zeropage_x) /* 0xf6 */
ADD_HANDLER (invalid_opcode) /* 0xf7 */
ADD_HANDLER (sed_implied) /* 0xf8 */
ADD_HANDLER (sbc_absolute_y) /* 0xf9 */
ADD_HANDLER (invalid_opcode) /* 0xfa */
ADD_HANDLER (invalid_opcode) /* 0xfb */
ADD_HANDLER (invalid_opcode) /* 0xfc */
ADD_HANDLER (sbc_absolute_x) /* 0xfd */
ADD_HANDLER (inc_absolute_x) /* 0xfe */
ADD_HANDLER (invalid_opcode) /* 0xff */
END_DEFINE_STATIC_FUNC ()
pnes_handler = nes_handler;
is_init_global_func = 1;
}
Я просто начал работать и не думал о том, насколько может быть скучен проект. Это меня стимулировало. Во второй и третий день я работал по 16 часов, отдыхая только каждые 10 минут. Потом двое суток отладки. Для отладки я использовал fceux. В нем я находил область памяти и смотрел какие регистры будут после выполнения ряда операций. В своем эмуляторе я делал сначала как последовательный отладчик, а потом поменял на другой способ, я стал просто дожидаться выполнения до определенной строки адреса и выключение программы с дампом памяти и значениями регистров. Работа оказалось интересной, мне нравилось работать в таком русле. Даже несколько раз начинала болеть голова и приходилось на пол часика отвлекаться.
Отдельно у меня был проект программа, которая использует эту библиотеку с применениями SDL2. Вообще моя любимая тема, это дать эмулятору на выполнение например команд 10, а потом вернуть его в среду игры, но в тот раз я пренебрёг этим и заметил очень долгую работу библиотеки. Я совсем забыл о том, что после каждого возвращения из библиотеки, управление отдается SDL_PollEvent, которая обрабатывает накопившиеся события. Поэтому во втором дне разработки я очень долго ждал запуска игры, так как там было много инициализации по затиранию каждой ячейки памяти нулями.
Проект строиться на Makefile, и если нужно собрать для Linux, то можно выполнить make -f Makefile.linux. Позже, как пойму что нужно делать, чтобы заработало на электронике, например arduino, то сделаю реализацию и для неё, если конечно меня всё ещё будет интересовать проект. Для Linux собирается с Opengl. Можно поменять стиль, чтобы рисовалось во фреймбуфер. В таком случае можно в своей игре сделать экран старого телевизора, куда будет рисовать экран игры.
Из статьи здешней с Хабра, которая является переводом, я узнал, что первые игры, такие как mario, были без мапперов, поэтому я взял эту игру за основу.
И вот, два дня отладки, реверса, столько просиженных часов и я добился начальной картинки.
Правда она одного цвета была, а в реальной игре их несколько. Всё из-за того, что я не сделал обработку палитры по тайлам для фонов. Наблюдались также проблемы с рисованием, например такое меню рисовалось только через секунду, если ставить раньше, то ничего не рисуется. Эмуляция реального железа та ещё работёнка. Нужно научиться считать правильные тайминги, всё есть в документации и я научусь её правильно читать, чтобы была польза.
Как я организовал код в обработчиках. Обработчики у меня в виде макросов. Вот примеры как выглядят функции.
#define ADC_ACTS(_flags, reg, calc, eq, cycles, is_off, _bytes) { \
struct CPUNes *cpu = &emu->cpu; \
uint8_t flags = _flags; \
uint8_t ret = 0; \
ret = calc; \
uint8_t carry = 0; \
if (cpu->P & STATUS_FLAG_CF) carry = 1; \
cpu->P &= ~(flags); \
CHECK_FLAGS (flags, reg, ret); \
reg eq ret; \
reg += carry; \
wait_cycles (emu, cycles); \
emu->cpu.PC += _bytes; \
}
#define REPETITIVE_ACTS(_flags, reg, calc, eq, cycles, is_off, _bytes) { \
struct CPUNes *cpu = &emu->cpu; \
uint8_t flags = _flags; \
uint8_t ret = 0; \
ret = calc; \
uint8_t carry = 0; \
if (cpu->P & STATUS_FLAG_CF) carry = 1; \
cpu->P &= ~(flags); \
CHECK_FLAGS (flags, reg, ret); \
reg eq ret; \
wait_cycles (emu, cycles); \
emu->cpu.PC += _bytes; \
void sta_zeropage (struct NESEmu *emu)
{
ST_ACTS(cpu->A, zeropage (emu), 3, 0, 2);
}
void stx_zeropage (struct NESEmu *emu)
{
ST_ACTS(cpu->X, zeropage (emu), 3, 0, 2);
}
void dey_implied (struct NESEmu *emu)
{
REPETITIVE_ACTS (STATUS_FLAG_NF|STATUS_FLAG_ZF, cpu->Y, --cpu->Y, =, 2, 0, 1);
}
void ror_absolute_x (struct NESEmu *emu)
{
uint16_t addr = absolute_x (emu);
if (addr < 0x800) {
ROR_ACTS (STATUS_FLAG_NF|STATUS_FLAG_ZF|STATUS_FLAG_CF, emu->ram[absolute_x (emu)], 7, 0, 3);
} else {
ROR_ACTS (STATUS_FLAG_NF|STATUS_FLAG_ZF|STATUS_FLAG_CF, emu->mem[absolute_x (emu)], 7, 0, 3);
}
}
Сама структура эмулятора пока большая. Я хотел обойтись от выделения памяти, внутри структуры, поэтому, если кто-то будет использовать в своих классах, то можно просто выделить одну память структуре и всё, памяти всё-таки она поглощает тоже много. Я пока пытаюсь связать разные уровни уровней памяти и бывало, что делал багованный код.
По этому эмулятору я решил выпускать ряд коротких статей, чтобы читатель смог дочитать до конца и возможно тоже вдохновиться какой-то работой. Я не претендую на экспертность в разработке эмуляторов, так как это мой второй эмулятор, который по всей видимости мне удастся закончить, первый был мой вымышленный эмулятор, но для реального железа делать намного труднее, тут уже нет места творчеству, творчеству остается только часть с оптимизацией работы кода. :-)
В следующей статье я уже постараюсь показать эмуляцию игры, в которой компьютер играет сам, в этой игре это происходит во время заставки.
Проект пушиться в две репозитория, один на github, другой на gitverse.
Если мне удастся сделать эту библиотеку, то я приступлю к компилятору для NES, а потом можно дополнить мою старую разработку, где можно пока что только рисовать для NES.
https://flathub.org/apps/io.github.xverizex.RetroSpriteEditor
Далее я хочу сделать полноценную студию разработки, где можно будет и рисовать, и писать код и отлаживать его. Надеюсь, что мне будет в радость заниматься этим.
nzeemin
Несколько мыслей / предложений / рекомендаций:
Советую сделать автоматический "тестовый стенд", куда подставляешь очередную версию "ядра эмуляции" (библиотеку в вашем случае), прогоняется ряд тестов, которыми определяется, что вы не получили регрессии. Например, для нескольких своих эмуляторов я делал тесты вида "загрузить вот это, подождать, нажать клавишу, подождать, сделать скриншот и проверить его совпадение с эталоном". Постепенно накапливать базу таких тестов.
Подумать про трейсинг -- задаём например число uint32 под маску что трассировать: команды CPU, обращения к внешним устройствам итд. Собрать трассу бывает весьма полезно при отладке.
Подумать про API для отладчика. Это может быть набор вызовов. И/или это может быть полноценный GDB stub - позволит отлаживаться по GDB протоколу, для него есть UI инструменты.
Делать отладчик, например, в виде консольного диалога. Отдельными командами можно запустить выполнение, управлять точками останова, смотреть регистры и память, сохранить/загрузить полное состояние эмулятора. Отладчик поможет в сложных случаях. Можно проходить инструкции "параллельной отладкой" в двух эмуляторах, сравнивая результаты.
Реализовать сохранение состояний и их загрузку, убедиться что всё состояние эмулятора точно сохраняется и восстанавливается.
Подумать про кросс-платформенность, чтобы библиотека собиралась и одинаково полноценно работала под Linux/Mac/Windows, а возможно и под ARM или Wasm.
Пока вы используете SDL для UI, как я понял. Посмотрите в сторону ImGui, им можно делать крутые инструменты визуализации и отладки.
xverizex Автор
Спасибо за такой содержательный совет. Очень интересно будет всё это реализовать.
nzeemin
Ну и стоит конечно посматривать как другие люди подобные эмуляторы пишут, например:
https://github.com/punesemu/puNES
https://github.com/eariassoto/dear_nes
https://github.com/gordnzhou/imnes-emulator
https://github.com/LIJI32/SameBoy
Стоит также глянуть, как в проекте MESS организовано разделение устройств и систем, подход к исчислению времени.