Когда появился 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/
Добавить комментарий