Как стать автором
Обновить

Эмулятор Chip-8 для GTK+ на практике

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

Теги:
Хабы:
Всего голосов 8: ↑8 и ↓0+8
Комментарии0

Публикации