Содержание статьи

В браузере пока нельзя эмулировать PlayStation 2, но это лишь вопрос времени: Linux туда уже загружают, и он работает. Это кажется чудом, но никакого чуда нет: внутри такие эмуляторы очень просты.

 

Вступление

Эмуляция при помощи JavaScript стала возможна по двум причинам. Во-первых, тег Canvas. Скажи спасибо Apple: десять лет назад именно эта компания разработала и встроила в WebKit технологию, которая позволяет при помощи JavaScript манипулировать отдельными пикселями на HTML-странице. Сейчас Canvas поддерживают все распространенные браузеры.

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

JSLinux эмулирует в браузере компьютер, способный загрузить полноценный Linux
JSLinux эмулирует в браузере компьютер, способный загрузить полноценный Linux

Исключение составляет JSLinux, нашумевший эмулятор ПК. Его в 2011 году разработал известный французский программист Фабрис Беллар — создатель FFmpeg, популярного средства кодирования и декодирования видео, которое используют VLC, MPlayer и YouTube, и универсального эмулятора аппаратного обеспечения QEMU. ПК и Linux — это вовсе не ретро, так ведь?

Почти. Это уже не восьмидесятые, но и не сегодняшний день. Эмулятор Беллара представляет собой модель тридцатидвухразрядного процессора, напоминающего Intel 80486, к которому подключены контроллер прерываний, таймер, блок управления памятью, интерфейс IDE для взаимодействия с виртуальным жестким диском и последовательный порт. Автор выкинул из процессора все, без чего можно обойтись, в том числе поддержку вычислений с плавающей точкой, а вместо графического дисплея использовал алфавитно-цифровой терминал — так проще.

Тем, кто желает разобраться, как устроен JSLinux, можно порекомендовать аннотированную версию исходников проекта, выложенную на GitHub. В отличие от оригинала, они снабжены подробными комментариями, которые поясняют, какой именно функциональности x86-совместимого процессора или других элементов ПК соответствует тот или иной фрагмент кода.

Как и в любом другом эмуляторе, центральную роль в JSLinux играет объект, моделирующий работу процессора. В данном случае соответствующий класс называется CPU_X86 и содержит переменные, в которых хранятся значения всех регистров и флагов, а также ссылки на «оперативную память» виртуального компьютера и методы для работы с ней. В принципе, ОЗУ можно было бы представить и в виде обычного целочисленного массива (так устроены многие другие эмуляторы), но Беллар нашел более эффективный вариант: он использовал типизированные массивы (их добавили в JavaScript относительно недавно для работы с бинарными данными в WebGL). Кроме процессора, в эмуляторе имеются отдельные объекты, имитирующие работу программируемого контроллера прерываний, последовательного порта и таймеров.


Взаимодействие с внешним миром происходит при помощи виртуального последовательного порта: на вход поступает информация о нажатиях клавиш, а на выход направляются последовательности символов, предназначенные для вывода на алфавитно-цифровой консоли. По сути дела, терминал играет роль шлюза между двумя программными интерфейсами: по одну сторону находится браузер с событиями DOM, по другую — Linux, который получает данные через последовательный порт.

При запуске эмулятор Беллара создает объект PCEmulator, содержащий объекты процессора и прочих компонентов компьютера, и выделяет 32 Мбайт памяти. Затем он инициализирует объекты устройств ввода-вывода и загружает в память образы ядра Linux и содержимого виртуальной «файловой системы» (они находятся в файлах vmlinux26.bin, root.bin и linuxstart.bin). После этого в регистр EIP (счетчик команд) помещается адрес, по которому оказалось содержимое vmlinux26.bin, в регистр EAX попадает величина виртуального ОЗУ в байтах, а в EBX — размер файла root.bin. Эмулятор готов к работе.

Цикл работы процессора описан в методе timer_func класса PCEmulator. Он, если опустить детали, заключается в последовательном вызове метода exec_internal класса CPU_X86, который загружает и исполняет отдельные команды машинного кода. Начало метода отмечено лаконичным комментарием: The Beast («Чудовище»). И не зря. Длина exec_internal без учета комментариев составляет порядка шести тысяч строк кода — это примерно 85% всего эмулятора. Метод идентифицирует команды, извлекает их аргументы и изменяет состояние процессора в соответствии с ними.

Копаться в коде эмулятора Беллара трудно не столько из-за его сложности, сколько из-за размеров системы команд x86. Одно лишь перечисление регистров отнимет целую страницу. Но тот же принцип использует большинство других эмуляторов, написанных на JavaScript. Возьмем, к примеру, JSNES — эмулятор восьмибитной игровой приставки NES, которую выпускала компания Nintendo (в России эту приставку знают под названием «Денди»).

 

NES и Sega

Игровая приставка NES
Игровая приставка NES

NES построена на базе восьмиразрядного процессора Ricoh 2A03, использующего систему команд MOS 6502 — популярного чипа, разработанного в середине семидесятых. Он очень прост не только по сравнению с Intel 80486, но и по сравнению с его современником — восьмиразрядным Intel 8080. MOS 6502 имеет всего два регистра общего назначения (X и Y), аккумулятор для математических операций и три специальных регистра: регистр P, отдельные биты которого служат флагами процессора, указатель стека SP и счетчик команд PC. Все они, за исключением шестнадцатиразрядного PC, имеют длину восемь бит (старший байт указателя стека считается всегда равным 0x01).

Начинка игровой приставки NES
Начинка игровой приставки NES
Процессор Ricoh 2A03 под микроскопом
Процессор Ricoh 2A03 под микроскопом

Эмулировать такой процессор гораздо проще, чем Intel 80486. Как и в JSLinux, в JSNES есть объект, описывающий состояние процессора:

JSNES.CPU = function() {
    this.mem = null;
    this.REG_ACC = null;
    this.REG_X = null;
    this.REG_Y = null;
    this.REG_SP = null;
    this.REG_PC = null;
    // Пропустим долгое перечисление флагов процессора
    this.reset();
};

В методе reset выделяется память — целочисленный массив, состоящий из 65 536 элементов. Именно таково максимальное число элементов, к которым можно обращаться при помощи шестнадцатиразрядных адресов:

this.mem = new Array(0x10000);

Затем эмулятор инициализирует регистры процессора. Хотя в действительности в указателе стека MOS 6502 умещается не больше одного байта, в эмуляторе он хранит полный адрес. Проще позаботиться о верной обработке переполнения значения регистра SP, чем составлять нужный адрес из нескольких частей всякий раз, когда нужно обратиться к стеку:

this.REG_ACC = 0;
this.REG_X = 0;
this.REG_Y = 0;
this.REG_SP = 0x01FF;
this.REG_PC = 0x8000-1;

В JSNES, разумеется, есть свой аналог белларовского exec_internal — метод, исполняющий отдельные команды машинного кода. Он называется emulate и хоть и не мал, но и не столь монструозен. После обработки прерываний emulate достает содержимое ячейки памяти, на которую указывает счетчик команд, сдвигает счетчик команд на один байт вперед и приготавливается считать потраченные на работу такты:

var opinf = this.opdata[this.nes.mmap.load(this.REG_PC+1)];
var cycleCount = (opinf>>24);
var cycleAdd = 0;

var opaddr = this.REG_PC;
this.REG_PC += ((opinf >> 16) & 0xFF);

var addrMode = (opinf >> 8) & 0xFF;
var addr = 0;

Теперь ему необходимо определить, есть ли у команды аргумент, и вычислить его. В зависимости от значения переменной addrMode аргумент может находиться по адресу, на который указывает счетчик команд, содержаться в аккумуляторе или вычисляться без малого дюжиной различных способов. Наконец, он может просто отсутствовать. Значение аргумента сохраняется в переменной addr.

Когда с этим покончено, приходит время выполнять команду. За это отвечает оператор switch(opinf&0xFF), за которым следует несколько десятков возможных значений кода команды. Вот, к примеру, команда JMP. Она имеет код 27 и вызывает безусловный переход по адресу, указанному в переменной addr:

case 27: {
    this.REG_PC = addr-1;
    break;
}

Команда вызова подпрограммы JSR отличается тем, что перед переходом текущее значение счетчика команд отправляется в стек. Метод сохранения в стеке (push) вызывается дважды, потому что каждая ячейка стека вмещает не более восьми разрядов. Шестнадцатиразрядный адрес приходится сохранять в два приема — сначала старшие восемь разрядов, затем младшие:

case 28:{
    this.push((this.REG_PC>>8)&255);
    this.push(this.REG_PC&255);
    this.REG_PC = addr-1;
    break;
}

При возврате из подпрограммы (команда RTS) половинки адреса добывают из стека в обратном порядке:

case 42:{
    this.REG_PC = this.pull();
    this.REG_PC += (this.pull()<<8);
    if (this.REG_PC==0xFFFF) return; // return from NSF play routine:
    break;
}

Некоторые команды отвечают за операции с содержимым регистров и памятью. Вот, например, команда STA, сохраняющая значение аккумулятора по адресу, указанному в аргументе команды:

case 47:{
    this.write(addr, this.REG_ACC);
    break;
}

Такой метод эмуляции называется интерпретацией. Он самый простой, самый распространенный и относительно неэффективный — но не настолько, чтобы это вызывало реальные затруднения. Справедливости ради нужно упомянуть два других метода: динамическую и статическую рекомпиляцию. В отличие от интерпретатора, рекомпилятор не исполняет команды по одной, а сперва перемалывает код в более удобную форму: например, в тот же JavaScript, который можно скормить JIT-компилятору.

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

Такой подход эксплуатирует jsSMS — эмулятор Sega Master System, восьмибитного предшественника «Мегадрайва». Разработчик jsSMS утверждает, что при помощи динамической рекомпиляции ему удалось ускорить работу программы в пять-шесть раз.

Игровая приставка Sega Master System
Игровая приставка Sega Master System

Все просто и понятно? Так и должно быть. JSLinux ничуть не сложнее. Да, он вынужден поддерживать больше команд, но сами команды почти столь же прямолинейны, как команды MOS 6502. В конечном счете, для того, чтобы разработать такую программу, нужно обладать только железным терпением и болезненной любовью к чтению скучных интеловских спецификаций. Нужное сочетание качеств встречается не так уж часто, но если оно есть, достаточно строго следовать правилам, и Linux заработает.

Sega Master System использовала процессор Z80 — такой же, как в ZX Spectrum
Sega Master System использовала процессор Z80 — такой же, как в ZX Spectrum

С эмуляцией NES дело обстоит несколько иначе. Смоделировать MOS 6502 совсем нетрудно, но это не конец, а только начало пути. Когда работаешь с ретрокомпьютерами, прямолинейность спецификаций быстро заканчивается, и тогда ты остаешься один на один с хтоническим неевклидовым безумием, которое царит за их пределами. Оно чуждо всему, что существует сейчас, но его нужно понять, а затем воспроизвести во всех нездоровых и пугающих деталях — иначе Марио не прыгать.

Суди сам: в исходниках эмулятора Беллара около семи тысяч строк, шесть из которых занимает описание процессора. Это программа, которая способна запустить полноценный современный Linux. JSNES тем временем эмулирует простенькую приставку тридцатилетней давности, основанную на примитивном процессоре с двумя регистрами. Здравый смысл подсказывает, что такой эмулятор обязан быть проще, но ретрокомпьютеры и здравый смысл — понятия несовместные. По величине кода JSNES почти не уступает JSLinux, и большая его часть не реализует спецификации — она борется с безумием.

 

Atari

Послушай историю. В сентябре 1977 года компания Atari выпустила домашний компьютер под названием Atari 2600. Краткого знакомства с этим легендарным прибором достаточно для того, чтобы осознать весь ужас ситуации, в которой находятся разработчики эмуляторов ретрокомпьютеров.

Домашний компьютер Atari 2600
Домашний компьютер Atari 2600

Atari 2600 использовала урезанную версию уже знакомого нам процессора MOS 6502 и обладала оперативной памятью величиной 128 байт (этот абзац там не уместился бы — он в три раза длиннее). Кроме того, к устройству можно было подключать картриджи с ПЗУ объемом четыре килобайта, а его видеочип позволял отображать на телеэкране изображение с разрешением 160 на 190 пикселей и 128 цветами на пиксель.

А теперь самое важное: у Atari 2600 не было видеопамяти. 160 на 190 пикселей. 128 цветов. И ни единого байта для того, чтобы их хранить. Как это возможно?

Внутренности Atari 2600
Внутренности Atari 2600

Сейчас 2015 год. Те, кто читает эту статью, скорее всего, не видели телевизора с электронно-лучевой трубкой уже лет десять — а некоторые, вполне возможно, и никогда. Это странно, но, боюсь, для того, чтобы объяснить, как работало видео Atari 2600, нужно начинать с самого начала — с магнитов.

Изображение на экране телевизора с электронно-лучевой трубкой — это иллюзия. В действительности телевизоры двадцатого века могли заставить светиться лишь крохотный участок экрана. Они делали это при помощи пучка электронов, который сфокусирован и направлен в нужную точку при помощи мощных магнитов. Чтобы построить изображение, электронный пучок с огромной скоростью бежал по экрану: от верхнего левого к верхнему правому углу, затем возвращался налево, опускался чуть ниже, повторял путь к противоположному краю, и так до тех пор, пока не достигал самого низа. От того, какой сигнал поступает в телевизор, зависела интенсивность пучка и, соответственно, яркость «пикселей», которых он касается.

В большинстве компьютеров, появившихся после 1980 года, программист надежно отделен от электронных пучков и магнитов несколькими слоями абстракций. Но Atari 2600 так примитивна, что не может позволить себе такой роскоши. Луч, бегущий по люминофору, — это главный герой любой программы для этой платформы. Разработчикам приходилось непрерывно следить за тем, где именно находится луч именно сейчас, чтобы в нужный момент дать команду и заставить его зажечь на экране несколько пикселей (на самом деле их задача была еще сложнее, но не будем отвлекаться на детали).

Процессор Atari 2600 не отличался производительностью, поэтому каждое действие приходилось отмерять с точностью до такта. Стоит ошибиться на долю секунды, и картинки не получится. Это создавало трудновообразимые сейчас затруднения и ограничения. Вот пример: на пару команд, загружающих из памяти значение, а затем устанавливающих регистр, который задает «цвет» луча, уходит пять тактов. За пять тактов луч успевает окрасить пятнадцать пикселей. Следовательно, менять цвет можно не чаще одиннадцати раз на каждой строке — и это в том случае, если программа не будет делать ничего, кроме изменения цветов.

Еще не страшно? Сейчас будет: различные телевизионные стандарты подразумевают разную скорость развертки и разное количество кадров в секунду. У других платформ об этой разнице заботится видеоадаптер, но с Atari 2600 так не выйдет. Особенности PAL, SECAM и NTSC рушат тщательно подсчитанные по продолжительности комбинации команд и заметно влияют на функциональность программ. Апокалиптическую картину довершают разработчики игр, которые быстро научились применять скудные возможности Atari 2600 не по назначению, выжимая из приставки то, на что она по всем формальным признакам неспособна.

Эмулятор Atari 2600 должен учитывать все. Он должен знать, с какой скоростью бежит луч по экрану телевизора. Он должен эмулировать мельчайшие особенности телевизионных стандартов. Он должен точно синхронизировать движение луча (которого вообще-то нет) с продолжительностью исполнения различных команд и задержками, которые вносит электроника Atari 2600, — о них, разумеется, в документации ни слова, но игры отлаживались на реальном железе и, конечно, развалятся на части, если где-то не хватит микросекунды. Наконец, эмулятор должен точно имитировать все недостатки и ошибки устройства, на которые привыкли полагаться разработчики приложений.

Игры для Atari 2600

Pitfall
Pitfall

Pete Rose Basebal
Pete Rose Basebal

Pigs In Space
Pigs In Space

Pole Position
Pole Position

Pompeii
Pompeii

Q*bert
Q*bert

Это парадокс: загрузить Linux при помощи JavaScript проще, чем заставить работать примитивные доисторические игры. По крайней мере, интерфейс IDE и алфавитно-цифровой терминал предсказуемы. Они не зависят от фазы Луны, длины бороды программиста и тысячи других факторов. Когда имеешь дело с железом, которое придумано больше тридцати лет назад, все наоборот. Нужно думать о бесчисленных недокументированных особенностях железа, связанных с синхронизацией, графикой и звуком. Именно в этих особенностях — главная трудность.

Atari 2600 — это экстремальный случай, но и в NES хватает сюрпризов. Один из вечных источников проблем — картриджи. Дело в том, что картриджи NES часто содержали не только ПЗУ с кодом игр, но и нестандартное железо, которое могло делать все что угодно. Например, если в картридже нужно уместить больше информации, чем позволяет выделенное под него адресное пространство, в него встраивали специальный чип для переключения банков данных. Другие картриджи снабжались энергонезависимыми запоминающими устройствами для сохранения отгрузок, а порой — и специальными сопроцессорами. Существовали десятки разновидностей картриджей. Каждая из них работала по-своему, и, если эмулятор не поддерживает нужную, игра не пойдет.

Другая трудность связана опять-таки с графикой. За нее в NES отвечал особый сопроцессор, работающий с тактовой частотой, которая втрое превышала тактовую частоту центрального процессора. Он собирал отображаемое на экране изображение из фона и 64 спрайтов величиной 8 на 8 или 8 на 16 пикселей. Программист определял, где хранится спрайт, в какой части экрана его следует вывести, нужно ли сдвинуть фон и если нужно, то как. Остальное делал сопроцессор.

На первый взгляд, все просто и удобно — никакого сравнения с видеоадом Atari 2600. Но, как известно, свинья грязь найдет. Разработчиков игр не устраивали недостатки NES. Видеопроцессор приставки начинал сбоить, когда на одной строке оказывалось больше восьми спрайтов, и не умел скроллить различные части экрана независимо друг от друга. Постепенно программисты научились обходить эти ограничения. Оказалось, если подменять видеоданные, пока видеопроцессор отрисовывает кадр, от него можно добиться большего. Беда в том, что эта чудесная идея возвращает нас в мрачное царство телеразвертки и подсчета тактов.

О преодолении подобных трудностей можно рассуждать вечно. Мы не будем это делать, а сосредоточимся на более практических материях: эмуляции видеовывода приставки в браузере. JSNES использует для формирования изображения тег Canvas:

self.root = $('<div></div>');
self.screen = $('<canvas class="nes-screen" width="256" height="240"></canvas>').appendTo(self.root);
self.canvasContext = self.screen[0].getContext('2d');
self.canvasImageData = self.canvasContext.getImageData(0, 0, 256, 240);

Переменная canvasImageData позволяет обращаться к отдельным пикселям Canvas. Каждый пиксель описывается четырьмя целыми числами: по одному на каждый из трех цветовых компонентов и один — на прозрачность (его можно игнорировать).

Игры для NES

Игра Castlevania для NES
Игра Castlevania для NES

Chip’n’Dale: Rescue Rangers 2
Chip’n’Dale: Rescue Rangers 2

Duck Hunt
Duck Hunt

Super Mario Bros. 3
Super Mario Bros. 3

Teenage Mutant Ninja Turtles III
Teenage Mutant Ninja Turtles III

За вывод изображения отвечает метод writeFrame. Он получает на входе два видеобуфера, один из которых содержит текущий кадр, а другой — прошлый. Оба буфера — это обычные массивы JavaScript, но изображение в них хранится в несколько другом формате, чем в canvasImageData. Если в canvasImageData пиксель состоит из четырех элементов массива, то в buffer и prevBuffer каждый пиксель — это одно целое число. Цвета закодированы в трех байтах этого числа и извлекаются при помощи операции побитового сдвига. Для экономии времени это делается только для тех элементов buffer, которые изменились по сравнению с prevBuffer. Метод writeFrame заканчивается командой putImageData, отображающей сформированное изображение в браузере:

writeFrame: function(buffer, prevBuffer) {
    var imageData = this.canvasImageData.data;
    var pixel, i, j;

    for (i=0; i<256*240; i++) {
        pixel = buffer[i];
        if (pixel != prevBuffer[i]) {
            j = i*4;
            imageData[j] = pixel & 0xFF;
            imageData[j+1] = (pixel >> 8) & 0xFF;
            imageData[j+2] = (pixel >> 16) & 0xFF;
            prevBuffer[i] = pixel;
        }
    }
    this.canvasContext.putImageData(this.canvasImageData, 0, 0);
}

Вот, в принципе, и все. Кроме разбора исходников JSNES, интересующимся эмуляцией ретрокомпьютеров при помощи JavaScript можно порекомендовать серию статей о разработке джаваскриптового эмулятора GameBoy, которую написал британский программист Имран Назар. После этого можно вооружаться информацией с описанием интересующей платформы (энтузиасты, как правило, давно во всем разобрались) и браться за дело. Если страшно, можно начать с простого: написать эмулятор какой-нибудь виртуальной машины — скажем, байт-кода Z machine. Для того чтобы сделать первый шаг, самое то.

Где искать эмуляторы

JBacteria

JBacteria эмулирует ZX Spectrum и представляет собой джаваскриптовый порт эмулятора Bacteria, который интересен своими крохотными размерами: всего четыре килобайта. На сайте JBacteria выложены десятки игр для «Спектрума», которые можно тут же открыть в браузере.

Игра Head Over Heels в эмуляторе JBacteria
Игра Head Over Heels в эмуляторе JBacteria

JS-VBA-M

Чтобы запустить этот эмулятор GameBoy Advance, придется запастись «ромами» игр с пиратских сайтов. Без них JS-VBA-M работать не будет. Подобной тактики придерживаются многие авторы эмуляторов. Они надеются, что таким образом им удастся избежать внимания юристов.

Одна из версий игры Pokemon в эмуляторе JS-VBA-M
Одна из версий игры Pokemon в эмуляторе JS-VBA-M

Радио 86-РК

Этот радиолюбительский компьютер, разработанный в СССР около тридцати лет назад, построен на базе восьмиразрядного микропроцессора КР580ВМ80А — советского клона Intel 8080. На сайте эмулятора выложены многочисленные игры для РК — например, незабвенный «Клад» (Lode Runner).

Игра «Клад» в эмуляторе Радио 86-РК
Игра «Клад» в эмуляторе Радио 86-РК

Emulators written in JavaScript

По этой ссылке располагается полнейший каталог ретроэмуляторов, написанных на JavaScript. Кроме предсказуемых игровых приставок, есть и экзотика: эмуляторы PDP-11 и Burroughs B5500, машин, относящихся к более ранним поколениям вычислительной техники.

Оставить мнение