Imran Nazar: Эмуляция GameBoy на JavaScript: Память

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

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

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

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

Рис. 1: Карта памяти адресной шины GameBoy

Рассмотрим области памяти более детально:

  • [0000-3FFF] ROM картриджа, банк 0: Первые 16,384 байта программы картриджа всегда доступны в этом месте карты памяти. Существуют особые случаи:
    • [0000-00FF] BIOS: Когда стартует процессор, значение PC начинается с
      1
      0000h

      , что является началом 256-байтного кода GameBoy BIOS. Как только BIOS запустился, он удаляется из карты памяти, и эта область ROM (Read Only Memory. Память, которая доступна только для чтения. прим. перев.) картриджа становится доступной для адресации.

    • [0100-014F] Заголовок картриджа: Эта область картриджа содержит данные о его имени и производителе. Должна быть записана в определённом формате.
  • [4000-7FFF] ROM картриджа, другие банки: В этой области памяти можно по очереди получить доступ к любым последующим 16k «банкам». Как правило, переключением банков и обеспечением доступа к ним управляет чип на картридже. Программы, которые имеют размер 32 килобайта не нуждаются в таком чипе.
  • [8000-9FFF] Графическая RAM: Здесь содержатся данные необходимые для фоновых изображений и спрайтов, используемых графической подсистемой. Они могут быть изменены программой. Эта область будет рассмотрена более подробно в третьей статье.
  • [A000-BFFF] RAM картриджа (Внешняя): В GameBoy для записи доступно не очень много памяти. Если игра требует больше, то дополнительные 8-ми килобайтные куски могут быть адресованы здесь.
  • [C000-DFFF] Рабочая RAM: 8 килобайт внутренней памяти GameBoy. Процессор имеет возможность читать из них или записывать в них.
  • [E000-FDFF] Рабочая RAM (теневая): Из-за особенностей устройства GameBoy в следующих 8-ми килобайтных областях памяти доступна точная копия рабочей памяти. Она доступна вплоть до последних 512 байт, где в дело вступают другие области.
  • [FE00-FE9F] Графика: информация о спрайтах: Здесь содержатся данные о спрайтах отрисовываемых графическим чипом, включая позиции и атрибуты спрайтов.
  • [FF00-FF7F] Ввод/вывод отображаемый в память: Каждая подсистема GameBoy (графика, звук, и т.д.) имеет контрольные значения (наверное, имелись в виду порты. прим. перев.), для того, чтобы у программ была возможность создавать эффекты и использовать оборудование. В этой области контрольные значения доступны процессору напрямую.
  • [FF80-FFFF] Zero-page RAM: На верхушке памяти доступна высокоскоростная область 128-ми байтной RAM. Как ни странно, хотя это 255-ая «страница» памяти, она упоминается как нулевая страница, так как большая часть взаимодействия между программой и аппаратным обеспечением GameBoy проходит через неё.

Взаимодействие с процессором

Для того, чтобы эмулируемый процессор обращался с каждой областью памяти по-своему, обращения к ним должны разделяться в модуле управления памятью. Эта часть кода была упомянута в предыдущей статье, и был описан базовый интерфейс объекта MMU. Конкретизация интерфейса проста как выражение

1
switch

.

MMU.js: Отображаемое чтение

MMU = {
    // Флаг, который показывает отображается BIOS в память или нет.
    // Отображение BIOS отключается с первой инструкцией после 0x00FF
    _inbios: 1, 

    // Области памяти (инициализируются во время перезагрузки)
    _bios: [],
    _rom: [],
    _wram: [],
    _eram: [],
    _zram: [], 

    // Читает байт из памяти
    rb: function(addr)
    {
    switch(addr & 0xF000)
    {
        // BIOS (256b)/ROM0
        case 0x0000:
            if(MMU._inbios)
        {
            if(addr < 0x0100)
                return MMU._bios[addr];
            else if(Z80._r.pc == 0x0100)
                MMU._inbios = 0;
        } 

        return MMU._rom[addr]; 

        // ROM0
        case 0x1000:
        case 0x2000:
        case 0x3000:
            return MMU._rom[addr]; 

        // ROM1 (unbanked) (16k)
        case 0x4000:
        case 0x5000:
        case 0x6000:
        case 0x7000:
            return MMU._rom[addr]; 

        // Graphics: VRAM (8k)
        case 0x8000:
        case 0x9000:
            return GPU._vram[addr & 0x1FFF];

        // External RAM (8k)
        case 0xA000:
        case 0xB000:
            return MMU._eram[addr & 0x1FFF];

        // Рабочая RAM (8k)
        case 0xC000:
        case 0xD000:
            return MMU._wram[addr & 0x1FFF];

        // Теневая рабочая RAM
        case 0xE000:
            return MMU._wram[addr & 0x1FFF];

        // Теневая рабочая RAM, Ввод/Вывод, Zero-page RAM
        case 0xF000:
            switch(addr & 0x0F00)
        {
            // Теневая рабочая Working RAM
            case 0x000: case 0x100: case 0x200: case 0x300:
            case 0x400: case 0x500: case 0x600: case 0x700:
            case 0x800: case 0x900: case 0xA00: case 0xB00:
            case 0xC00: case 0xD00:
                return MMU._wram[addr & 0x1FFF];

            // Графика: object attribute memory (память атрибутов объекта)
            // Размер OAM 160 байт. Оставшиеся байты читаются как 0
            case 0xE00:
                if(addr < 0xFEA0)
                return GPU._oam[addr & 0xFF];
            else
                return 0;

            // Zero-page
            case 0xF00:
                if(addr >= 0xFF80)
            {
                return MMU._zram[addr & 0x7F];
            }
            else
            {
                // Обработка Ввода/Вывода
                // На данный момент отсутствует
                return 0;
            }
        }
    }
    }, 

    // Читает 16-битное слово
    rw: function(addr)
    {
        return MMU.rb(addr) + (MMU.rb(addr+1) << 8);
    }
};

Обратите внимание, что область памяти между

1
0xFF00

и

1
0xFF7F

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

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

1
wb

.

Загрузка ROM

Так же как эмуляция процессора бесполезна без поддержки доступа к памяти, графики и т.п., возможность чтения программы из памяти бесполезна без загруженной программы. Существует два основных способа поместить программу в эмулятор: захардкодить (простите за столь вольный термин. прим. перев.) её или разрешить загружать ROM (в сленге эмуляторщиков образ картриджа с игрой называется ROM. прим. перев.) из определённого места. Очевидный недостаток первого варианта в том, что программу нельзя легко изменить.

В нашем JavaScript эмуляторе GameBoy BIOS вшит в MMU по причине того, что он не подлежит изменению. А вот ROM’ы, после инициализации эмулятора, асинхронно загружается с сервера. Это может быть сделано через XMLHTTP, при помощи кода для чтения двоичных файлов, например, Andy Na’s BinFileReader. В результате у нас будет строка содержащая ROM.

MMU.js: Загрузка ROM

MMU.load = function(file)
{
    var b = new BinFileReader(file);
    MMU._rom = b.readString(b.getFileSize(), 0);
};

Поскольку ROM хранится не как массив чисел, а как строка, функции

1
rb

и

1
wb

должны быть изменены для доступа к содержимому строки по индексу:

MMU.js: Доступ по индексу к ROM

        case 0x1000:
        case 0x2000:
        case 0x3000:
            return MMU._rom.charCodeAt(addr);

Следующие шаги

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

Как и в первой части, исходный код статьи доступен здесь:http://imrannazar.com/content/files/jsgb.mmu.js.

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