Imran Nazar: Эмуляция GameBoy на JavaScript: Интеграция

Перевод пятой статьи про эмуляцию GameBoy на JavaScript. Оригинал здесь. Далее авторский текст.

Это пятая часть серии статей об эмуляции в JavaScript. Десять частей уже доступно, позже будут ещё.


В предыдущей части, после детального исследования графической подсистемы GameBoy, мы получили общую схему будущего эмулятора. Однако, графическая система не может быть использована без отображаемых в память регистров GPU. Как только это будет реализовано, эмулятор станет практически готов для простого использования. Вместе с описанными ниже дополнениями, которые добавляют регистры GPU и простым пользовательским интерфейсом для контроля эмулятора, результат выглядит вот так (В Google Chrome подглюкивает. прим. перев.).

Рис. 1: Реализация jsGB с графикой

Регистры GPU

Графический чип GameBoy имеет ряд регистров которые отображаются в I/O (Input/Output, Ввод/Вывод. прим. перев.) пространство памяти. Для того чтобы получить работающую эмуляцию с выводом фонового изображения, GPU необходимы следующие регистры (в GPU существуют и другие регистры, они будут рассмотрены в следующих статьях).

Адрес Регистр Статус
0xFF40 LCD and GPU control Чтение/запись
0xFF42 Scroll-Y Чтение/запись
0xFF43 Scroll-X Чтение/запись
0xFF44 Current scan line Только чтение
0xFF47 Background palette Только запись

Таблица 1: Базовые регистры GPU

Регистр палитры фона упоминался ранее и состоит из четырёх двухбитовых записей. Регистр прокрутки и счётчик линий занимают байт. Последний регистр это управляющий регистр LCD, который состоит из 8 отдельных флагов контролирующих отделы GPU.

Бит Функция При 0 При 1
0 Фон: вкл./выкл. Выкл. Вкл.
1 Спрайты: вкл./выкл. Выкл. Вкл.
2 Спрайты: размер (в пикселах) 8×8 8×16
3 Фон: карта тайлов #0 #1
4 Фон: набор тайлов #0 #1
5 Окно: вкл./выкл. Выкл. Вкл.
6 Окно: карта тайлов #0 #1
7 Экран: вкл./выкл. Выкл. Вкл.

Таблица 2: Управляющий регистр GPU

В таблице появились дополнительные возможности GPU: слой «окна», который появляется над фоном и спрайты, которые могут быть перемещены относительно фона и окна. Эти дополнительные возможности будут описаны по мере их надобности. В то же время, флаги фона наиболее важны для основных функций рендеринга. В частности, мы увидим, как фоновая карта тайлов и набор тайлов могут быть изменены простым инвертированием битов в регистре 0xFF40.

Реализация: регистры GPU

Согласно базовому устройству регистров GPU мы можем реализовать их эмуляцию простым добавлением обработчиков их адресов в MMU. Можно захардкодить обновления GPU в MMU, или определить диапазон регистров по которым GPU будет вызываться из MMU для выполнения обработки. В интересах модульности использован последний подход.

MMU.js: I/O нулевой страницы: GPU

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
    rb: function(addr)
    {
    switch(addr & 0xF000)
    {
        ...
        case 0xF000:
            switch(addr & 0x0F00)
        {
            ...
            // Нулевая страница
            case 0xF00:
                if(addr >= 0xFF80)
            {
                return MMU._zram[addr & 0x7F];
            }
            else
            {
                // обработка I/O
                switch(addr & 0x00F0)
                {
                    // GPU (64 регистра)
                    case 0x40: case 0x50: case 0x60: case 0x70:
                    return GPU.rb(addr);
                }
                return 0;
            }
        }
    }
    },

    wb: function(addr, val)
    {
    switch(addr & 0xF000)
    {
        ...
        case 0xF000:
            switch(addr & 0x0F00)
        {
            ...
            // Нулевая страница
            case 0xF00:
                if(addr >= 0xFF80)
            {
                MMU._zram[addr & 0x7F] = val;
            }
            else
            {
                // I/O
                switch(addr & 0x00F0)
                {
                    // GPU
                    case 0x40: case 0x50: case 0x60: case 0x70:
                    GPU.wb(addr, val);
                    break;
                }
            }
            break;
        }
        break;
    }
    }

GPU.js: Обработка регистров

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
    rb: function(addr)
    {
        switch(addr)
    {
        // Управление LCD
        case 0xFF40:
            return (GPU._switchbg  ? 0x01 : 0x00) |
               (GPU._bgmap     ? 0x08 : 0x00) |
               (GPU._bgtile    ? 0x10 : 0x00) |
               (GPU._switchlcd ? 0x80 : 0x00);

        // Прокрутка Y
        case 0xFF42:
            return GPU._scy;

        // Прокрутка X
        case 0xFF43:
            return GPU._scx;

        // Текущая линия
        case 0xFF44:
            return GPU._line;
    }
    },

    wb: function(addr, val)
    {
        switch(addr)
    {
        // Управление LCD
        case 0xFF40:
            GPU._switchbg  = (val & 0x01) ? 1 : 0;
        GPU._bgmap     = (val & 0x08) ? 1 : 0;
        GPU._bgtile    = (val & 0x10) ? 1 : 0;
        GPU._switchlcd = (val & 0x80) ? 1 : 0;
        break;

        // Прокрутка Y
        case 0xFF42:
            GPU._scy = val;
        break;

        // Прокрутка X
        case 0xFF43:
            GPU._scx = val;
        break;

        // Палитра фона
        case 0xFF47:
            for(var i = 0; i < 4; i++)
        {
            switch((val >> (i * 2)) & 3)
            {
                case 0: GPU._pal[i] = [255,255,255,255]; break;
            case 1: GPU._pal[i] = [192,192,192,255]; break;
            case 2: GPU._pal[i] = [ 96, 96, 96,255]; break;
            case 3: GPU._pal[i] = [  0,  0,  0,255]; break;
            }
        }
        break;
    }
    }

Эмуляция одного кадра

В настоящее время цикл диспетчеризации CPU работает вечно, без паузы. Самый простой пользовательский интерфейс должен дать нам возможность поставить эмулятор на паузу или перезагрузить его. Для реализации этого необходимо определить базовую единицу для отсчёта времени эмуляции. Существует три подхода:

  • Инструкция: Предоставляет возможность включить паузу после каждой инструкции CPU. Это вызывает много накладных расходов, т.к. функция диспетчирезации должна вызываться каждый шаг CPU. При частоте 4.19MHz для достижения существенно количества должно произойти множество шагов.
  • Линия: Дать возможность включить паузу после рендеринга каждой линии. Это производит меньше накладных расходов, но диспетчер всё ещё должен вызываться несколько тысяч раз в секунду. Также, эмуляция может быть приостановлена в таком состоянии когда состояние холста не соответствует текущей строке развёртки.
  • Кадр: Разрешает включить паузу после того как кадр эмулирован, отрендерен и отображён на холсте. Предоставляет наилучший компромисс между точностью тайминга и оптимальной скоростью, при этом обеспечивая согласованиея холста и состояния GPU.

Так как кадр состоит из 144 линий прямого хода развёртки строки и 10 обратного хода развёртки кадра и обработка каждой линии требует 456 тактов процессора, то стоимость кадра составляет 70224 тактов. После реализации функции перезагрузки, которая инициализирует каждую подсистему при старте эмуляции, можно запускать эмулятор с примитивным интерфейсом.

index.html: Интерфейс эмулятора

1
2
<canvas id="screen" width="160" height="144"></canvas>
<a id="reset">Reset</a> | <a id="run">Run</a>

jsGB.js: Сброс и диспетчеризация

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
jsGB = {
    reset: function()
    {
        GPU.reset();
    MMU.reset();
    Z80.reset();

    MMU.load('test.gb');
    },

    frame: function()
    {
        var fclk = Z80._clock.t + 70224;
    do
    {
        Z80._map[MMU.rb(Z80._r.pc++)]();
        Z80._r.pc &= 65535;
        Z80._clock.m += Z80._r.m;
        Z80._clock.t += Z80._r.t;
        GPU.step();
    } while(Z80._clock.t < fclk);
    },

    _interval: null,

    run: function()
    {
        if(!jsGB._interval)
    {
        jsGB._interval = setTimeout(jsGB.frame, 1);
        document.getElementById('run').innerHTML = 'Pause';
    }
    else
    {
        clearInterval(jsGB._interval);
        jsGB._interval = null;
        document.getElementById('run').innerHTML = 'Run';
    }
    }
};

window.onload = function()
{
    document.getElementById('reset').onclick = jsGB.reset;
    document.getElementById('run').onclick = jsGB.run;
    jsGB.reset();
};

Тестирование

То что вы видели на Рис. 1 — результат объединения всего написанного кода: эмулятор способный загружать и запускать графическое демо. Используемый ROM это тест скроллинга написанный Doug Lanford: фон начнёт скроллится при нажатии на стрелочки. Однако, сейчас отображается только статичный фон по причине того, что эмуляция кейпада (джойстик GameBoy. прим. перев.) ещё не реализована.

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

Imran Nazar , Сентябрь 2010.
Статья датирована: 5 сентября 2010