...

четверг, 14 ноября 2013 г.

[Из песочницы] Эмулятор Chip-8 для GTK+ на практике

Когда был в школе и работал/играл с советскими клонами Sinclair 48К, мечтал о соседском 8086.

Когда появился 486DX66, мечтал снова о Z80. Так и пронес свою любовь к ретрокомпьютерам в настоящее. И хотя сейчас пытаюсь в железе воплотить себя как “конструктора ПК”, и даже обладая некоторой коллекцией раритетных и не очень ЦПУ, всегда хотел сделать виртуальную версию сам. Но то знаний не хватало, то ещё чего-нибудь; чаще всего — времени. В итоге решил попробовать. Мечтой был запуск СВМ для ЕС ЭВМ, да и Elite снова увидеть на чем-то, сделанном самим. Но так как дом строят с фундамента, решил начать с начала.


Программировал я и в школе, на «Агатах», дома на «Микроше», потом на Java. Но потом забросил. Год с лишним назад по работе понадобилось автоматизировать один процесс, что-то попробовал и понеслась. Пытаюсь писать на С, работаю на Linux, и использую GTK+ (3.0) (хотя и под win пишу на нем же — привык. И да, я знаю что это извращение). Примеров реализации именно того, что я хотел на GTK+ не нашел, поэтому, может быть, данный пост пригодится таким же как я начинающим с GTK и эмуляцией.


Статей о принципах эмуляции, и конкретно Chip-8 – вагон и маленькая тележка, поэтому репостить то, что итак замечательно описано, например, тут,, тут и и тут, не буду.


Я не стал смотреть исходные коды ни одного эмулятора, перед попыткой написать свой. Кроме удовольствия от результата, преследовалась цель самообучения. Подсматривать в ответы всегда приводило к отсутствию запоминания. Посему хотелось «помучаться» самому, сначала. Использую я Glade. Поэтому весь интерфейс был нарисован в нем. Так как это тестовая попытка и никакого практического использования не планировалось, то некоторые вещи были упрощены. Что-то решил сделать уже в эмуляторе следующей системы. Заранее прошу прощения за стиль кода.


Итак, рисуем наше окном для эмулятора. Разрешение Chip-8 базовой версии — 64*32, размер пикселя я взял как 8*8. Поэтому выставляем соответствующие свойства GtkDrawingArea, где и будем рисовать.





Всё нутро виртуального ЦПУ лежит в структуре



typedef struct
{
uint64_t last_cycle;
uint64_t vsync;
gboolean pressed;
uint8_t last_key;
gboolean run;
uint8_t delay_timer;
uint8_t sound_timer;
uint8_t cycle;
uint8_t keypad[16];
uint8_t V[16];
uint16_t opcode;
uint16_t stack[16];
uint16_t sp;
uint16_t I;
uint16_t pc;
uint8_t video[SCREEN_X][SCREEN_Y];
uint8_t video_mirrored[SCREEN_X][SCREEN_Y];
uint8_t memory[RAM_SIZE];
}_CHIP8;
extern _CHIP8 SYS;


Возможно, видео память «выглядит» не очень натурально, но я хотел потом перенести на микроконтроллер с дисплеем 128*64, и хотелось избавиться от всех лишних умножений/делений, если это возможно. А потом так и осталось.


Дизассемблирование ПЗУ реализовано просто и примитивно.

SYS.opcode = SYS.memory[SYS.pc] << 8 | SYS.memory[SYS.pc + 1];

После этого идет «бинарная магия» в сравнительно большой функции со switch/case.

С микроконтроллерами я вожусь чуть дольше, но все равно бинарная арифметика была больше черным ящиком, чем понятным предметом. Работа с эмулятором за час-два мне привила и прожгла «в подкорке» все то, что нужно знать.

Опкодов немного, поэтому такое решение вполне себя оправдывает. Сами машинные коды составлены очень удобно, поэтому такая функция пишется очень быстро. Главное понимать И и ИЛИ, а так же помнить, что Chip-8 — big endian машина.


Главный цикл крутится в отдельном потоке, с частотой в 24Гц я планировал обновлять экран.

Проблема в том, что GTK требует, чтобы все манипуляции с ним производились из главного цикла. Поэтому раз в 1/24 сек видеопамять отзеркаливается и с помощью g_idle_add мы сообщаем основному циклу о том, что хотим вызвать refresh_screen. Функция будет вызвана сразу, как только освободятся ресурсы. Если этого не сделать и вызывать функции отрисовки из другого треда — работать будет почти наверняка. Может даже долго работать, пока либо не покрашится, либо не возникнут забавные и не очень артефакты/спецэффекты.



void *chip8_vcpu_pipeline(void *data)
{
[…...........]
g_idle_add((GSourceFunc) refresh_screen, NULL);
[…............]
return (0);
}


Для начала нужно сделать соответствующий callback для GtkDrawingArea. Всё рисование будет происходить в этой функции.



gboolean draw_cb(GtkWidget *widget, cairo_t *cr, gpointer data)
{
cr = gdk_cairo_create( gtk_widget_get_window (widget));
cairo_set_source_rgb(cr, 0, 0, 0);

cairo_paint(cr);
for ( int x = 0; x < SCREEN_X; x++ )
{
for ( int y = 0; y < SCREEN_Y; y++ )
{
SYS.video_mirrored[x][y] ? set_dot(cr, x, y) : clear_dot(cr, x, y);
}

}
cairo_destroy(cr);
return FALSE;
}


Ну и функции пикселя: поставить точку/ стереть оную



void set_dot(cairo_t *cr, int32_t cx, int32_t cy)
{
cairo_set_source_rgb(cr, 255, 255, 255);
cairo_set_line_width(cr, 2);
cairo_rectangle(cr, cx * 8, cy * 8, 8, 8);
cairo_fill(cr);
cairo_stroke(cr);
}

void clear_dot(cairo_t *cr, int32_t cx, int32_t cy)
{
cairo_set_source_rgb(cr, 0, 0, 0);
cairo_set_line_width(cr, 2);
cairo_rectangle(cr, cx * 8, cy * 8, 8, 8);
cairo_fill(cr);
cairo_stroke(cr);
}


Функцию draw_cb подключаем к эвенту draw GtkDrawingArea. Один кадр теперь мы отрисуем, но как обновить экран? Это и делается в refresh_screen, где GUI.screen — GtkDrawingArea.



gboolean refresh_screen(void)
{
gtk_widget_queue_draw_area(GTK_WIDGET(GUI.screen), 0, 0, 512, 256);
return FALSE;
}


Так как мы вызывали отрисовку через g_idle_add, возвращаем FALSE, чтобы отрисовка была однократной.


Теперь клавиатура. Пишем две функции



gboolean
on_key_press (GtkWidget *widget, GdkEventKey *event, gpointer user_data)
{
switch(event->keyval)
{
case GDK_KEY_1:
SYS.keypad[1]=1;
SYS.last_key = 1;
break;
case GDK_KEY_2:
SYS.keypad[2]=1;
SYS.last_key = 2;
break;
…........


И такую же для on_key_release и подключаем их к key-press-event и key-release-event соответственно.


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



This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers.


Комментариев нет:

Отправить комментарий