Форум программистов, компьютерный форум, киберфорум
Наши страницы
loothood
Войти
Регистрация
Восстановить пароль
Оценить эту запись

Пишем CHIP-8 эмулятор с использованием Rust и WebAssembly

Запись от loothood размещена 25.12.2017 в 02:04

(Исходную статью я разбил на две части. Перевод второй части будет позже. Оригинал)
Последние несколько месяцев я исследовал потенциал Webassembly. Особое внимание я уделил параметрам компиляции и производительности, недавно обратил внимание на D3 Force Layout в WebAssembly с использованием AssemblyScript.

Я хотел попробовать свои силы в создании сложного приложения WebAssembly, поэтому я проводил вечера, работая над эмулятором CHIP-8 ... и изучая Rust!

Вы можете найти код завершенного эмулятора на GitHub. Так же, эмулятор размещен онлайн, если вы хотите поиграть с ним(я бы порекомендовал Kraftwerk’s Computer Love как музыку на фоне).


Почему Rust?
На данный момент, есть несколько языков с поддержкой WebAssembly. Поддержка их несколько сыра. Сейчас, в WebAssembly нет сборщика мусора(GC), в результате языки, которые не требуют GC, имеют лучшую поддержку. Самая зрелая поддержка C/C++ через Emscripten, но, если быть абсолютно честным, мне не очень нравится C++ ... или Emscripten!

Rust - относительно новый язык (2010), который, будучи синтаксически подобным C++, разработан, чтобы быть более «безопасным». Он также имеет множество языковых особенностей, общих с другими современными языками программирования, такими как Swift. Все вышеперечисленное, в сочетании с активным сообществом, внесло свой вклад в достижение титула «Самый любимый язык».

Итак, для моего эмулятора CHIP-8 мне нужен язык с относительно зрелой поддержкой WebAssembly, который не создает раздутые двоичные файлы (т.е. нет GC и минимальное время выполнения).

Rust был очевидным выбором.

Я не буду подробно рассказывать о своей реализации CHIP-8, однако я выделил несколько областей, в которых были отмечены особенности языка Rust.

Но сначала, что такое CHIP-8?


CHIP-8
Должен признаться, эмулятор давно был в моем списке "сделать" (я бы написал "todo список", но не был уверен что будет понятно - примечание переводчика). Я начинал программировать на 8-битных микрокомпьютерах, и мне всегда нравилось писать низкоуровневый код. Моя первоначальная идея заключалась в том, чтобы написать эмулятор Atari 2600 VCS, потому что его архитектура просто невероятна. Однако, чтобы эмулировать данную архитектуру, надо немало постараться. Возможно, стоит начать с чего попроще?

Немного почитав, я узнал о CHIP-8. Его архитектура является достаточно простой для эмуляции из-за относительно простого набора команд. Интересно, что CHIP-8 - это виртуальная машина, которая была реализована на ряде компьютеров 1970х и калькуляторах 1980х. Вы можете почитать побольше о CHIP-8 и преимуществе интерпретаторов в этом блоге.

Вкратце, у CHIP-8 4Кб оперативной памяти. Первые 512 байт используются интерпретаторами машинного перевода (да, всего 512 байт!). Это 16 8-битовых регистров и один 16-ти разрядный программный регистр. В архитектуре есть стек, который может хранить до 16 адресов, что позволяет поддерживать операции «call», а также «jump». CPU поддерживает 35 различных кодов операций (то есть инструкций), которые имеют ширину всего 2 байта. Для периферийных устройств CHIP-8 имеет простой дисплей размером 64x32 пикселя, базовую поддержку звука и клавиатуру с 16 клавишами.

Помимо прочего, CHIP-8 является относительно простым, похоже, не является проприетарным, и, как результат, технические справочные материалы легко доступны.

Если вы заинтересовались CHIP-8, я настоятельно рекомендую статьи Лоуренса Мюллера, в которых в деталях рассказано о многих аспектах CHIP-8. В данной же статье, я хотел бы сделать упор на Rust и WebAssembly.


Rust и WebAssembly
Поддержка Rust для WebAssembly быстро развилась за последние несколько месяцев. Первоначально поддержка зависела от Emscripten и «fastcomp» fork LLVM, однако это приводит к довольно долгому времени выполнения готовой программы и большому размеру бинарных файлов.

Совсем недавно в Rust была добавлена поддержка wasm-unknown-unknown, благодаря этому стало возможно убрать поддержку Emscripten, используя LLVM как бэкэнд напрямую. Это позволяет скомпилировать очень легковесные бинарники. Но нет пределов совершенству, потому команда Rust создала wasm-gc для обработки сообщений wasm-файлов для удаления неиспользуемого кода.

Благодаря этому обновлению, создание wasm модуля выглядит очень просто (сюда если вам нужен детальный гайд). Следующий вызов cargo создаст wasm модуль:
Bash
1
cargo +nightly build --release --target wasm32-unknown-unknown
Заметка: cargo в Rust это менеджер пакетов и инструмент сборки. cargo позволяет подключать зависимости(crates) и запускать билды или тесты.


Структура эмулятора
Ядром эмулятора является процессор, который реализуется как структура. Поля этой структуры почти полностью совпадают с архитектурой CHIP-8, состоящей из памяти, регистров, счетчика программ и т.д.
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pub struct Cpu {
    // index register
    pub i: u16,
    // program counter
    pub pc: u16,
    // memory
    pub memory: [u8; 4096],
    // registers
    pub v: [u8; 16],
    // peripherals
    pub keypad: Keypad,
    pub display: Display,
    // stack
    pub stack: [u16; 16],
    // stack pointer
    pub sp: u8,
    // delay timer
    pub dt: u8
}
Клавиатура и дисплей представляют собой отдельные структуры, которые управляют этими периферийными устройствами.

Цикл выполнения процессора CHIP-8 довольно прост.

В моей реализации CPU состоит из открытого метода execute_cycle, который считывает код операции из текущей ячейки памяти и обрабатывает ее:
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn read_word(memory: [u8; 4096], index: u16) -> u16 {
    (memory[index as usize] as u16) << 8
        | (memory[(index + 1) as usize] as u16)
}
 
impl Cpu {
    pub fn execute_cycle(&mut self) {
        let opcode: u16 = read_word(self.memory, self.pc);
        self.process_opcode(opcode);
    }
 
    fn process_opcode(&mut self, opcode: u16) {
      ...
    }
}
Каждая инструкция, которая представляет собой два байта, интерпретируется методом process_opcode, о котором будет речь ниже.

На основе кода операции, CPU может обновлять регистры, рисовать на дисплее, читать ввод с клавиатуры, выполнять другие операции.

Ядро этой реализации, которая состоит из CPU, Display и Keypad, совпадает с большинством других реализаций Rust, которые вы найдете на GitHub и в других местах. Здесь нет ничего особенного, завязанного на WebAssembly!

Мы хотим, чтобы жизненный цикл CPU был таким же, как у модуля WebAssembly, поэтому имеет смысл задать его глобальным статическим значением:
C
1
2
3
static mut CPU: Cpu = Cpu {
   // Default field values added here
};
Чтобы позволить хостинговому JavaScript-коду взаимодействовать с этим статическим экземпляром, определим ряд общедоступных глобальных функций:
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#[no_mangle]
pub fn execute_cycle() {
    unsafe {
        CPU.execute_cycle();
    }
}
 
#[no_mangle]
pub fn get_memory() -> &'static [u8; 4096] {
    unsafe {
        &CPU.memory
    }
}
 
#[no_mangle]
pub fn get_display() -> &'static [u8; 2048] {
    unsafe {
        &CPU.display.memory
    }
}
Атрибут no_mangle отключает редактирование имен, гарантируя, что эти функции сохранят свои имена и могут быть вызваны извне.

Эти глобальные функции создают простой API, который позволяет JavaScript-коду взаимодействовать с модулем WebAssembly. После того, как модуль WebAssembly извлечен, скомпилирован и создан, цикл requestAnimationFrame (rAF) используется для многократного вызова функции execute_cycle:
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const res = await fetch("chip8.wasm");
const buffer = await res.arrayBuffer();
const module = await WebAssembly.compile(buffer);
const instance = await WebAssembly.instantiate(module);
const exports = instance.exports;
 
const runloop = () => {
    for (var i = 0; i < 10; i++) {
      exports.execute_cycle();
    }
  }
  exports.decrement_timers();
  updateUI();
  window.requestAnimationFrame(runloop);
};
window.requestAnimationFrame(runloop);
Заметьте, что в каждом rAF цикле, функция execute_cycle вызывается 10 раз.

Эмулятор CHIP-8 имеет тактовую частоту около 500 Гц, поэтому при вызове runloop со скоростью около 60 кадров в секунду это дает правильную скорость. Цикл также уменьшает таймер задержки CHIP-8, который работает на частоте 60 Гц.
Размещено в Без категории
Просмотров 285 Комментарии 0
Всего комментариев 0
Комментарии
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2018, vBulletin Solutions, Inc.
Рейтинг@Mail.ru