Imran Nazar: Эмуляция GameBoy на JavaScript: Тайминги GPU

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

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

Исходные коды эмулятора, о котором идёт речь в статьях, находятся здесь: http://github.com/Two9A/jsGB

В предыдущих частях была рассмотрена структура эмулятора GameBoy. Появилась возможность загрузить ROM с игрой и пошагово исполнить программу на эмулируемом процессоре. Теперь, учитывая, что у нас есть процессор соединённый с памятью, возможно добавить к системе периферию. Как и в любой другой консоли графический процессор (GPU) — одно из важнейших периферийных устройств. Для консоли это главный способ вывода и большая часть работы центрального процессора направлена на генерацию графики для GPU.

Эмуляция экрана

Внутреннее имя GameBoy в Nintendo — «Dot Matrix Game». LCD дисплей консоли имеет разрешение 160×144 пикселей. Если каждый пиксель экрана принять за пиксель на HTML5 <canvas>, то можно сделать прямое отображение на canvas (я не стал переводить этот термин во избежание разногласий. прим. перев.) размером 160 на 144. Для того чтобы непосредственно адресовать каждый пиксель на экране, содержимым канвы можно манипулировать как «экранным буфером»: единым блоком памяти, представляющим данные в виде последовательности 4-байтных RGBA значений (т.е. на каждый цвет по байту и байт на альфа-канал, другими словами каждый компонент может изменяться в пределах от 0 до 255. прим. перев.).

index.html: Тег canvas

<canvas id="screen" width="160" height="144"></canvas>

GPU.js: Инициализация canvas

GPU = {
    _canvas: {},
    _scrn: {}, 

    reset: function()
    {
        var c = document.getElementById('screen');
    if(c && c.getContext)
    {
        GPU._canvas = c.getContext('2d');
        if(GPU._canvas)
        {
        if(GPU._canvas.createImageData)
            GPU._scrn = GPU._canvas.createImageData(160, 144);

        else if(GPU._canvas.getImageData)
            GPU._scrn = GPU._canvas.getImageData(0,0, 160,144); 

        else
            GPU._scrn = {
                'width': 160,
            'height': 144,
            'data': new Array(160*144*4)
            }; 

        // Заполнить canvas белым цветом
        for(var i=0; i<160*144*4; i++)
            GPU._scrn.data[i] = 255; 

        GPU._canvas.putImageData(GPU._scrn, 0, 0);
        }
    }
    }
}

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

1
y * 160 + x

.

Растеризация графики

Canvas готова, и следующим шагом для получения графического вывода GameBoy будет эмуляция отображения графики. Оригинальное “железо” GameBoy эмулирует тайминги катодно-лучевой трубки (CRT). В CRT кадр построчно развёртывается пучком электронов, который возвращается к верхней части дисплея после выполнения прохода.

Рис. 1: Периоды прямого (scanline) и обратного (blanking) хода развёртки

На рисунке видно, что развёртка строки в CRT отнимает больше времени чем просто проход над ней. В это время входит период обратного хода развёртки строки (HBlank, horizontal blanking), который нужен для того, чтобы пучок электронов переместился от конца одной строки до начала следующей. Аналогично, конец кадра означает начало периода обратного хода развёртки кадра (VBlank, vertical blanking), во время которого пучок перемещается в верхний-левый угол экрана. Период обратного хода развёртки кадра обычно отнимает больше времени чем период обратного хода развёртки строки, потому что в первом случае лучу необходимо пройти существенно большее расстояние.

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

Период Номер режима GPU Затраченное время (в тактах)
Прямой ход развёртки строки (доступ к памяти атрибутов объектов (OAM)) 2 80
Прямой ход развёртки строки (доступ к видеопамяти (VRAM)) 3 172
Обратный ход развёртки строки 0 204
Одна строка (прямой и обратный ход) 456
Обратный ход развёртки кадра 1 4560 (10 строк)
Целый кадр (прямые и обратные ходы развёртки) 70224

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

Z80.js: Диспетчер

while(true)
{
    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();
}

GPU.js: Шаг процессорного таймера

    _mode: 0,
    _modeclock: 0,
    _line: 0, 

    step: function()
    {
        GPU._modeclock += Z80._r.t; 

    switch(GPU._mode)
    {
        // Режим чтения памяти атрибутов объектов (OAM), прямой ход развёртки строки
        case 2:
            if(GPU._modeclock >= 80)
        {
            // Enter scanline mode 3
            GPU._modeclock = 0;
            GPU._mode = 3;
        }
        break; 

        // Режим чтения видео памяти (VRAM), прямой ход развёртки строки
        // Завершение третьего режима расценивается как завершение прямого
            // хода развёртки строки
        case 3:
            if(GPU._modeclock >= 172)
        {
            // Начать обратный ход развёртки строки
            GPU._modeclock = 0;
            GPU._mode = 0; 

            // Записать развёрнутую строку в экранный буфер
            GPU.renderscan();
        }
        break; 

        // Обратный ход развёртки строки
        // После последнего обратного хода развёртки строки,
            // отправить экранные данные на canvas.
        case 0:
            if(GPU._modeclock >= 204)
        {
            GPU._modeclock = 0;
            GPU._line++; 

            if(GPU._line == 143)
            {
                // Начать обратный ход развёртки кадра
            GPU._mode = 1;
            GPU._canvas.putImageData(GPU._scrn, 0, 0);
            }
            else
            {
                GPU._mode = 2;
            }
        }
        break; 

        // Обратный ход развёртки кадра (10 строк)
        case 1:
            if(GPU._modeclock >= 456)
        {
            GPU._modeclock = 0;
            GPU._line++; 

            if(GPU._line > 153)
            {
                // Сбросить режимы развёртки
            GPU._mode = 2;
            GPU._line = 0;
            }
        }
        break;
    }
    }

В следующей части: фоны и палитры

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

1
renderscan

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

Imran Nazar <tf@imrannazar.com>, Август 2010.

  • Даешь следующию часть!!!!

    • Будет, конечно, скоро, просто сейчас немного загружен. :)