Эмулятор 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, в итоге длину цикла выбирал на глаз. В любом случае, двигающаяся картинка на экране, да ещё и возможность поиграть в пинпонг очень хорошо мотивировало двигаться дальше.

ссылка на оригинал статьи http://habrahabr.ru/post/202104/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *