gameboy-sharp

Эмулятор Game Boy (DMG) на C#. Ядро без зависимости от фронтенда, поэтому вся машина работает и проверяется headless на стандартных тестовых ROM.

gameboy-sharp
TL;DR

Эмулятор Game Boy (модель DMG), написанный на C#. Ядро отделено от фронтенда и проверяется headless на стандартных тестовых ROM-ах, чтобы чужие игры шли бит в бит.

Обзор

gameboy-sharp - это эмулятор оригинального Game Boy, модели DMG, написанный на C#. Задача звучит просто: заставить игры, выпущенные тридцать лет назад, работать без изменения ни одного байта. Сложность кроется в слове "точно" - эмулятор должен изображать железо настолько верно, чтобы оригинальный код ничего не заметил.

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

Ошибки в эмуляторе почти никогда не падают с грохотом. Игра запускается, игра идёт - только звук чуть высоковат, или персонаж сдвигается на пиксель дальше. Нет исключения, нет краша, есть лишь тонко неправильное поведение, которое "на глаз" не отличить от верного.

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

Сердце машины: процессор

Сердце консоли - процессор Game Boy, разновидность Z80. Каждая инструкция - это чтение байта опкода и выполнение ровно того, что сделал бы оригинал, вместе с числом циклов, которое эта инструкция стоит. Тайминг - не деталь: именно он решает, синхронизированы ли звук и изображение так, как на настоящем железе.

Cpu.cs · csharp
byte opcode = ReadByte(PC++);

switch (opcode) {
    case 0x00: cycles += 4; break;
    case 0x3E: A = ReadByte(PC++); cycles += 8; break;
    case 0xC3: PC = ReadWord(); cycles += 16; break;
    default: cycles += Execute(opcode); break;
}

Инструкция и её стоимость в циклах

ОпкодИнструкцияЦиклы
0x00NOP4
0x3ELD A, d88
0xC3JP a1616
i
Примечание

Ядро ничего не знает ни об окне, ни о звуке - это чистая машина. Фронтенд получает только буфер изображения и аудиосэмплы. Благодаря этому всю консоль можно запустить без экрана и сравнить результат с ожидаемым, цикл за циклом.

Тесты с точностью до цикла

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

Именно поэтому ядро отрезали от фронтенда. Если бы машине нужно было окно, каждый тест пришлось бы прокликивать вручную. А раз это чистая библиотека, тестовые ROM-ы пролетают автоматически, и результат бинарный: сходится или нет.

Эффект: чужие игры бит в бит

На выходе - эмулятор, который запускает чужие игры и проходит стандартные тестовые ROM-ы, а не только показывает логотип Nintendo и встаёт. Звук там, где должен быть, персонаж сдвигается ровно на столько пикселей, на сколько должен, а правильность доказана тестами, а не впечатлением. Это проект, сделанный из любви к старому железу и доведённый до состояния, в котором это видно.

Больше проектов

Другие работы из той же категории - посмотрите, как мы решаем похожие задачи.

Есть похожий проект?

Напишите нам - смета бесплатна и приходит в течение часа.