0x00 - Описание того, что сделано в первое время
0x01 - Процесс поиска бага - https://habr.com/ru/articles/877372/
Добро пожаловать дорогой читатель. Сегодня я расскажу о проекте, который я разрабатываю некоторое время. Это эмулятор 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
Далее я хочу сделать полноценную студию разработки, где можно будет и рисовать, и писать код и отлаживать его. Надеюсь, что мне будет в радость заниматься этим.
Продолжение https://habr.com/ru/articles/877372/
Комментарии (17)
iShrimp
26.01.2025 22:50Как достичь максимальной производительности байткод-машины? Недавно на эту тему здесь была жаркая дискуссия (рекомендую читать по порядку: раз, два, три).
kmatveev
26.01.2025 22:50Вообще не в тему комментарий. Эмулятор конкретного железа не является байткод-машиной, потому что должен повторять потактово время выполнения инструкций эмулируемого железа, нет никакой необходимости добиваться некоей "максимальной" производительности.
checkpoint
26.01.2025 22:50Прошу прояснить чем эмуляция "чужой" системы команд принципиально отличается от вирутальной байт-код машины ?
По сути, автор уже почти реализовал то же самое, что обсуждалось "в горячей дискуссии".
xverizex Автор
26.01.2025 22:50Прошу прояснить чем эмуляция "чужой" системы команд принципиально отличается от вирутальной байт-код машины ?
Дискуссию не читал, но могу ответить как понимаю данный вопрос. Эмуляция NES к примеру требует соблюдения ждать определенное время, пока такты циклов пройдут, а это значит, что нужно ждать какое-то время. Если сделать так, чтобы выполнялось как можно быстрее код, то игра будет очень быстро играться и мы увидим, что игра ведет себя не так как на реальных консолях. В эмуляции байткода, например как в java, там уже не нужно ждать циклы процессора, там просто надо как можно быстрее выполнить код и поэтому затрачиваются средства на то, чтобы код выполнялся как можно быстрее. Я так думаю.
checkpoint
26.01.2025 22:50А если рассматривать задержку по времени как еще один опкод для VM который будет вставляться (вызываться неявно) после исполнения каждого опкода эмулируемой системы команд ?
На мой взгляд switch/case это очень плохое решение независимо от задачи. Таблица указателей более вреное решение - с ней проще работать, добавлять/удалять имплементации, экспериментировать с вариантами исполнения. В случае с длинным case код быстро превратится в спагетти перемешанный с временно закоментированными кусками, ifdef-ами и прочей шелухой.
kmatveev
26.01.2025 22:50Самый простой способ написать эмулятор и виртуальную машину - это написать интерпретатор, с центральным бесконечным циклом, внутри которого будет происходить выборка команд с последующим switch-ем по опкодам. В случае эмулятора нет смысла делать что-то другое (типа threaded code), потому что после каждой команды нужно проделать некоторые общие вещи: посчитать число тактов, проверить, не возникло ли прерывание, проэмулировать другие компоненты, помимо CPU. Кроме того, архитектура реальных машин такова, что код хранится в памяти, то есть может быть изменён в процессе выполнения, и стек тоже находится в памяти. Это значит, что JIT/AOT применять не получится. А виртуальная машина - она "виртуальная", потому что у неё, как правило, отдельно память данных, отдельно хранилище кода, отдельно локальные переменные, отдельно стек возвратов, отдельно стек операций. То есть высокоуровневые вещи, ограничивающие всякие трюки в коде, но позволяющие JIT/AOT и прочие оптимизации.
То есть для эмуляторов отказываться от цикла со switch-ем - это, с одной стороны, усложнение кода, а с другой стороны - вообще не нужно, всё равно скорость и задержки надо эмулировать.
beeruser
26.01.2025 22:50Тогда я думал так, в switch выборка идет по бинарному поиску, но я не был уверен в том, будет ли такой же вестись поиск, если числа будут перемешаны в case.
А чем это было обосновано?
Может вы заглянули в дизассемблер, или хотя бы проверили производительность?
То, что вы сделали отдельные функции не позволяет компилятору оптимизировать код, расположив нужные данные в регистры. Конечно, если всё в одном файле, то компилятор может попробовать инлайнить ваш код, чтобы сделать аналог первоначального switch-case.
https://godbolt.org/z/9x5GTd5vv
Хотя, конечно, для потактового эмулятора древних консолей это может быть непринципиально.
xverizex Автор
26.01.2025 22:50Может вы заглянули в дизассемблер, или хотя бы проверили производительность?
То, что вы сделали отдельные функции не позволяет компилятору оптимизировать код, расположив нужные данные в регистры. Конечно, если всё в одном файле, то компилятор может попробовать инлайнить ваш код, чтобы сделать аналог первоначального 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 уже точно не буду. На тот момент мне показалось, что идея с массивом функций будет работать быстрее. Тогда код был проще. И опять же я не смотрел на оптимизацию со стороны компилятора.
Seenkao
26.01.2025 22:50256 операций в свитче не так много, в своём большинстве всё влезет в один кэш (и уж точно в любой кэш новых процессоров).
Я тоже думал "оптимизировать" подобным образом, но оставил свитч, понимая, что лучшая оптимизация будет на ассемблере. Остальная "оптимизация" на ЯВУ даже ни в какое сравнение не пойдёт.
По статье, извиняюсь, но я ожидал большей выкладки информации, а так получается статья просто рассказать о том что у тебя получилось эмулировать Nes. Молодец, но лично я ожидал большего.
xverizex Автор
26.01.2025 22:50Следующая статья может вам тоже не понравиться, там вся статья с описанием как я искал баг, вот, только что выпустил.
https://habr.com/ru/articles/877372/
Но я пока не могу о технических моментах тоже рассказывать, так как меняю иногда реализацию. Это цикл статей как я пишу эмулятор. Да, извиняюсь, мало технических аспектов, но и кода тоже мало. Как только сделаю уже готовый продукт, то можно написать хорошую статью о том, что и как устроено.
beeruser
26.01.2025 22:50Я же смотрел на код, который был всегда без оптимизации
А зачем его смотреть? Код без оптимизации нужен ТОЛЬКО для отладки.
Он раздутый и очень медленный.
Я хотел сам оптимизировать код, без компилятора.
Вы разорвали мне шаблон.
Эффективная кодгенерация никак у вас не отбирает возможность оптимизации.
Компилятор за вас практически ничего не сделает, но может сэкономить время.
Я сейчас борюсь с тем, чтобы сделать код как-то компактнее и не сильно смотрю на оптимизирующий компилятор.
Вы делаете совершенно противоположные этому вещи.
Такая оптимизация начинается с ключа -Os
axe_chita
26.01.2025 22:50В тему разработки для 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.
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.
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 организовано разделение устройств и систем, подход к исчислению времени.