В прошлый раз мы написали интерпретатор CHIP-8, который способен выполнять все операции за исключением одной — Dxyn
(DRW Vx, Vy, nibble
). Ради упрощения реализации этой инструкции мы инкапсулируем графическую память и код в классе Image
. Кадр размером 64×32 пикселя будет представлен в виде единого фрагмента данных в памяти. Каждому пикселю будет соответствовать один байт:
0x000:|--------------------------------------------------------------|
0x040:| |
0x080:| |
0x0C0:| |
...
0x7C0:|--------------------------------------------------------------|
Для описания этой памяти нам понадобится три значения: количество строк, количество столбцов и начальный адрес (нам его даёт malloc). Если у нас есть этот адрес, указывающий на элемент графической памяти, находящийся в верхнем левом углу вышеприведённой схемы, обращение к отдельным пикселям будет выполняться очень просто. Вот несколько примеров:
img[col=0, row=0] = img[0]
img[col=0, row=1] = img[width]
img[col=1, row=3] = img[3*width+1]
Теперь, когда у нас есть эти сведения, мы готовы к тому, чтобы создать соответствующий заголовочный файл:
// image.h
class Image {
public:
// Выделение и освобождение памяти в ctor и dtor.
Image(int cols, int rows);
~Image();
uint8_t* Row(int r);
// Возвращает пиксель, который может быть изменён.
uint8_t& At(int c, int r);
void SetAll(uint8_t value);
private:
int cols_;
int rows_;
uint8_t* data_;
};
Тут надо обратить внимание на то, что мы динамически выделяем память, владельцем которой будет этот класс. В более крупной системе мы могли бы решить воспользоваться std::unique_ptr
вместе с особой функцией для выделения памяти. Но тут мы просто используем malloc
в конструкторе и free
в деструкторе класса.
// image.cpp
Image::Image(int cols, int rows) {
data_ = static_cast<uint8_t*>(malloc(cols * rows * sizeof(uint8_t)));
cols_ = cols;
rows_ = rows;
}
Image::~Image() {
free(data_);
}
uint8_t* Image::Row(int r) {
return &data_[r * cols_];
}
uint8_t& Image::At(int c, int r) {
return Row(r)[c];
}
void Image::SetAll(uint8_t value) {
std::memset(data_, value, rows_ * cols_);
}
void Image::DrawToStdout() {
for (int r = 0; r < rows_; r++) {
for (int c = 0; c < cols_; c++) {
if (At(c,r) > 0) {
std::cout << "X";
} else {
std::cout << " ";
}
}
std::cout << std::endl;
}
std::cout << std::endl;
}
Здесь мне удобнее пользоваться такими именами переменных, как rows_
(строки) и cols_
(столбцы), а не x
и y
. Дело в том, что имя переменной «rows» чётко ассоциируется у меня со «строками», а вот о том, что такое «x», я вполне могу забыть. Функция At
возвращает uint8_t&
, что даёт нам возможность и получать значения отдельных пикселей, и устанавливать эти значения. Это плохо с точки зрения инкапсуляции, но такой приём часто используется в графических API. Мы, кроме того, предусмотрели тут удобную функцию DrawToStdout
, которая позволяет выводить в консоль то, что должно быть отображено на экране, делая это даже тогда, когда подсистема графического вывода эмулятора ещё не реализована. Сейчас мы можем добавить в класс CpuChip8
поле frame_
типа Image
и поработать над реализацией соответствующих механизмов.
// cpu_chip8.h
class CpuChip8 {
public:
constexpr innt kFrameWidth = 64;
constexpr innt kFrameHeight = 32;
CpuChip8() : frame_(kFrameWidth, kFrameHeight) {}
...
private:
...
Image frame_;
};
Теперь давайте поговорим о том, как CHIP-8 выполняет вывод графических данных. А именно, «рисование» спрайта в текущем (и единственном) кадровом буфере выполняется по принципам, используемым в операции XOR
. Все спрайты описываются в виде изображений с глубиной цвета в 1 бит (каждый пиксель может быть либо «включен», либо «выключен»). Ширина спрайта равняется 8 битам, высота может меняться. Ограничение на ширину спрайта применяется из-за того, что каждый пиксель спрайта представлен единственным битом. Посмотрим на описание набора шрифтов, присутствующее в предыдущем материале, и попробуем «расшифровать» одну из цифр.
0xF0, 0x90, 0x90, 0x90, 0xF0, // 0
0xF0 это 1111 0000 -> XXXX
0x90 это 1001 0000 -> X X
0x90 это 1001 0000 -> X X
0x90 это 1001 0000 -> X X
0xF0 это 1111 0000 -> XXXX
Замечательно! Помните, я говорил о том, что вывод графики основан на операции XOR? Так вот, это значит, что единственный способ убрать спрайт с экрана заключается в том, чтобы вывести ещё один спрайт поверх него (фактически — тот же самый спрайт), так как 1 ⊕ 1 даёт 0. Именно поэтому при работе с CHIP-8-программами часто заметно мерцание, так как спрайты постоянно выводятся на экран и стираются с него для вывода движущихся объектов.
Итак, мы готовы к тому, чтобы создать функцию для вывода спрайтов. Нам понадобится начальная точка и сам спрайт (область памяти). Так как спрайты могут иметь переменную высоту, мы получаем и соответствующий параметр, описывающий её. Отмечу, что одна особенность интерпретатора CHIP-8 потребовала некоторого времени на её отладку. Она заключается в том, что интерпретатор поддерживает вывод графики за пределами экрана. Когда спрайт выходит за границы экрана, его рисование продолжается на другой стороне экрана. Это поведение проявляется и при указании стартовых координат спрайта (то есть — вывод 15 строк в координате 255,255 — это совершенно нормально). Кроме того, интерпретатору нужно сообщать о том, был ли при выводе спрайта стёрт какой-нибудь пиксель (это часто используется для обнаружения столкновений объектов, выводимых на экран).
// image.cpp
// Возвращает true в том случае, если новое значение стирает пиксель.
bool Image::XOR(int c, int r, uint8_t val) {
uint8_t& current_val = At(c, r);
uint8_t prev_val = current_val;
current_val ^= val;
return current_val == 0 && prev_val > 0;
}
bool Image::XORSprite(int c, int r, int height, uint8_t* sprite) {
// Переход на другую сторону экрана при выводе спрайта.
bool pixel_was_disabled = false;
for (int y = 0; y < height; y++) {
int current_r = r + y;
while (current_r >= rows_) { current_r -= rows_; }
uint8_t sprite_byte = sprite[y];
for (int x = 0; x < 8; x++) {
int current_c = c + x;
while (current_c >= cols_) { current_c -= cols_; }
// Обратите внимание: Сканирование выполняется от MSbit до LSbit
uint8_t sprite_val = (sprite_byte & (0x80 >> x)) >> (7-x);
pixel_was_disabled |= XOR(current_c, current_r, sprite_val);
}
}
return pixel_was_disabled;
}
Нам нужно позаботиться о том, чтобы извлекать биты, представляя их значениями 1
или 0
. Так как класс Image
поддерживает [0..255]
, операции XOR, без этого ограничения, могут наделать много беспорядка. Когда же применяется это ограничение, соответствующая инструкция нашего CPU получается очень простой — нужно всего лишь извлечь параметры, необходимые для вызова XORSprite
.
CpuChip8::Instruction CpuChip8::GenDRAW(uint8_t reg_x, uint8_t reg_y, uint8_t n_rows) {
return [this, reg_x, reg_y, n_rows]() {
uint8_t x_coord = v_registers_[reg_x];
uint8_t y_coord = v_registers_[reg_y];
bool pixels_unset = frame_.XORSprite(x_coord, y_coord, n_rows,
memory_ + index_register_);
v_registers_[0xF] = pixels_unset;
NEXT;
};
}
Если вы дошли до этого момента — у вас уже должна появиться возможность запускать некоторые ROMы! Поставьте вызов DrawToStdout
после цикла выполнения кода и понаблюдайте за тем, что попадает в консоль. Правда, пока на нашем интерпретаторе можно запускать только программы, не ожидающие пользовательского ввода.
В следующем материале из этой серии мы подключим к проекту библиотеку SDL, что позволит выводить графику на экран.
Если бы вы писали собственный интерпретатор CHIP-8 — каким языком программирования вы бы пользовались?
Apple возобновила переговоры с OpenAI о возможности внедрения ИИ-технологий в iOS 18, на основе данной операционной системы будут работать новые…
Конкурсный управляющий российской «дочки» Google подготовил 23 иска к участникам рекламного рынка. Общая сумма исков составляет 16 млрд рублей –…
Google завершил обновление основного алгоритма March 2024 Core Update. Раскатка обновлений была завершена 19 апреля, но сообщил об этом поисковик…
У частных продавцов на Авито появилась возможность составлять текст объявлений с помощью нейросети. Новый функционал доступен в категории «Обувь, одежда,…
24 апреля 2024 года в Москве состоялась церемония вручения наград международного конкурса Workspace Digital Awards. В этом году участниками стали…
27 июня Яндекс проведет гик-фестиваль Young Con для студентов и молодых специалистов, которые интересуются технологиями и хотят работать в IT.…