Портируем игры на практике

в 14:05, , рубрики: bodyawm_ништячки, timeweb_статьи, wolfenstein3d, атол, балдеж, гаджеты, девайсы, кассы, портирование, Программирование
Портируем игры на практике - 1

Дисклеймер: употребляемые слова ‭вроде «портируем‭», ‭«хакаем‭» и ‭«реверсим‭» совсем не значат, что статья предназначена исключительно для гиков! Я стараюсь писать так, чтобы было понятно и интересно абсолютно всем!

Наверняка многие мои читатели так или иначе слышали новости о том, что известные игры были портированы на самые разные платформы. В какой-то момент к такой же идее пришёл и я, однако мне хотелось портировать игры и эмуляторы на довольно диковинные промышленные девайсы, которые работают на платформе Windows CE. Как я портировал Wolfenstein и эмулятор NES на бравого, но списанного в утиль трудягу склада и зачем? Читайте в сегодняшней подробнейшей статье!

❯ Как, почему и зачем?

Мои давние читатели знают, что я прожженный энтузиаст, когда дело доходит до оживления самых разных ретро-девайсов. Помимо стандартных x86-компьютеров, многие из которых до сих пор в целом могут выполнять полезные задачи, я очень сильно интересуюсь ЭВМ и на довольно необычных архитектурах: ранние ARM-чипсеты, MIPS и, конечно же, SH3.

Портируем игры на практике - 2

Моя цель заключается в том, чтобы как можно меньше девайсов попадали в переработку чермета, если им можно найти интересное применение и в наше время. Ведь многие устройста, которые по началу кажутся бесполезными на манер промышленных ТСД или, например, кассовых аппаратов, на самом деле таковыми не являются и им можно находить самое разное крутейшее применение. Однако сценарий накатывания Putty и превращения компактной машинки в портативный терминал, или превращения девайса в часы с погодой не такой интересный, как превращение всего, у чего есть процессоры в игровые консоли!

Портируем игры на практике - 3

Потребительское общество уже забыло, что первые TV-боксы на Android'е буквально за час превращаются в игровые консоли путем накатывания эмуляторов или RetroArch, на смартфонах можно хостить сайты также легко, как и на одноплатном компьютере, а на PlayStation... можно накатить Linux. Но не всегда всё даётся так легко: иногда платформа настолько узкоспециализирована, что под неё нет ни эмуляторов, ни портов каких-то игр и поэтому нужно брать волю в свой кулак, о чём я вам и расскажу в сегодняшнем материале!

Портируем игры на практике - 4

Прямо сейчас, мой дорогой читатель, рядом со мной лежит ничто иное, как списанный терминал для сбора данных M3 Green. По началу кажется, что ТСД — очень узконаправленное устройство и ему место на складе/в‭ «магните‭», однако, если погрузиться в детали, выясняется что это очень нехилый портативный компьютер:

  • Процессор: ARMv5 Intel PXA272 на частоте 624МГц + Wireless MMX. Однако у процессора есть и слабое место: нет аппаратного деления (особенность ISA ARM) и нет FPU (сопроцессора для чисел с плавающей точкой).

  • ОЗУ: 128 мегабайт SDRAM-памяти. Кажется немного? Не забывайте, что Windows CE потребляет всего около 8-16 мегабайт памяти для своих нужд. По итогу у нас остаётся целых 100 мегабайт для себя. К примеру, современные версии Windows требуют ~1Гб ОЗУ как минимум без учётов кэша для I/O-операций!

  • Дисплей: встроенная 3-дюймовая матрица с разрешением 240x320. Кажется немного... но для КПК норма! Есть, конечно, и резистивный тачскрин.

  • Коммуникации: одна из самых сильных сторон такого девайса — наличие аппаратного USB-хоста (в док-станции), возможности синхронизации с ПК и конечно же Wi-Fi!

  • Клавиатура: ну, тут все очевидно :) Даже F-кнопки есть!

Короче, исходные данные простые офигенные! Учитывая, что девайс бронированный, его можно считать достаточно портативным устройством, который можно использовать и в несколько более экстремальных условиях.

❯ Эмулятор NES

Начинаем с эмулятора всем известной ‭«Денди‭». Сейчас есть множество самых разных эмуляторов с открытым исходным кодом, бери любой и портируй. Из тех, что портируются проще всего, можно выделить InfoNES, который уже портирован на Windows CE, однако на многих современных машинках работает нестабильно и его нужно адаптировать под конкретный девайс. Затем я чуть покумекал и вспомнил, что видел в сэмплах к SDK порт довольно шустрого эмулятора NES на один из китайских телефонов, о которых я рассказывал в одной из своих статей. Единственный нюанс — в нём нет эмуляции звука, зато и работает шустро. Корни эмулятора мне выяснить не удалось, нет ни копирайтов, ничего. Не исключено, что этот эмулятор лёг в основу многих ранних китайских игровых консолей.

Изначально эмулятор был разработан под платформу MRP, что только упрощало задачу. По сути, все приложения для китайских телефонов — это 4 функции: инициализация, отрисовка, обработка событий и выход. Конечно есть ещё обработчики событий, например по таймеру, но в целом концепция предельно ясна. Эмулятор был прямо-таки ‭«захардкожен‭» на конкретные пути к файлу рома (образу картриджа):

int32 mrc_init(void)
{   
	
	keyinit();
	ROM_len=0;
    nesloop=0;
	rom_file=NULL;
    rom_file=(unsigned char *)mrc_readFileFromMrp("nes.nes",&ROM_len,0);
	
	if(rom_file!=NULL&&ROM_len>1024&&ROM_len<=43*1024)
	{
	  startopcodetable();
      setkey();
	  if(index==8)
	  { 
	    Start();
	  }
	  else
	  {
		drawTxt("设置按键: 右");
	  }
	}
	else
	{
	drawTxt("内存不足...");
	mrc_exit();
	}
	return MR_SUCCESS;
}

Всё усложнялось тем, что большинство переменных были глобальные и ни о каком едином стейте для эмулятора и речи не было, поэтому код нужно было рефакторить. Но сначала нам хоть-бы что-то запустить! Для этого минимально переписываем логику загрузки ROM'ов на stdio с учётом того, что в WinCE корень файловой системы начинается с (не '/', как в Unix):

    f = fopen("\rom.nes", "rb");
	if(!f)
		return 0;

	fseek(f, 0, SEEK_END);
	len = ftell(f);
	fseek(f, 0, SEEK_SET);
	ptr = (unsigned char*)malloc(len);
	fread(ptr, 1, len, f);
	ROM_len = len;
	fclose(f);

	rom_file = ptr;

Теперь эмулятор загружает ром, однако у нас всё ещё нет ни вывода на экран, ни обработки ввода. Состояние кнопок геймпада представлено глобальной переменной KEY, где в оригинале большой свич просто сопоставляет код клавиши телефона к коду аппаратной кнопки геймпада NES:

if(MR_KEY_PRESS == code)
	{
		switch(p0)
		{
		case MR_KEY_SOFTRIGHT:
			 mrc_exit();
			 break;
		case MR_KEY_SELECT:
			 KEY[4]=1;
			 break;
        case MR_KEY_SOFTLEFT:  
			 KEY[5]=1;
			 break;
		case MR_KEY_LEFT:
			 KEY[1]=1;
			 break;
		case MR_KEY_RIGHT:
			 KEY[0]=1;
			 break;
		case MR_KEY_UP:
             KEY[3]=1;
			 break;
        case MR_KEY_DOWN:
			 KEY[2]=1;
			 break;
		case MR_KEY_1:
			 KEY[6]=1;
			 break;
		case MR_KEY_2:
			 break;
		case MR_KEY_3:
		     KEY[7]=1;
			 break;
		}
	}

Этот участок мы переписывем таким образом, чтобы сопоставить каждой аппаратной кнопке устройства виртуальный код клавиши в ‭«винде‭» и затем иметь возможность переназначить их на любые другие. Для WinCE навигаторов, где кнопок почти нет, актуально реализовать и ввода с тачскрина (в репозитории его на данный момент нет):

void ProcessInput()
{
	KeyMapping mapping[] = {
		{ VK_RETURN, 4 },
		{ VK_LEFT, 1 },
		{ VK_RIGHT, 0},
		{ VK_UP, 3},
		{ VK_DOWN, 2},
		{ 1, 7 }
	};

	if(GetAsyncKeyState(VK_ESCAPE) & 0x8000)
		exit(0);
	
	for(int i = 0; i < sizeof(mapping) / sizeof(KeyMapping); i++)
		KEY[mapping[i].KeyCode] = GetAsyncKeyState(mapping[i].VirtualKeyCode) & 0x8000 ? 1 : 0;
}

Теперь у нас есть обработка ввода... но всё ещё ничего нет на экране! И вот здесь начинается самое интересное. Дело в том, что как такового быстрого графического API в Windows CE нет. В Windows Mobile был GX, предназначенный для дисплеев 240x320, который предоставлял прямой доступ к фреймбуферу устройства, а также специальный вызов ExtScape, который позволял сделать тоже самое. Но ни тот, ни другой способ не поддерживаются на современных WinCE устройствах. Microsoft предлагала использовать DirectDraw, знакомый читателям по играм из 90-х, однако он не был реализован почти нигде, кроме КПК. Поэтому остаётся лишь 2D-подсистема GDI, которой рисуется окна и почти вся графика и в обычной Windows — медленный, тормозной способ который не позволяет выжать всю производительность с нашего девайса.

Начинаем с создания окна. Здесь всё стандартно:

    hwnd = CreateWindowW(L"static", L"Emulator", WS_VISIBLE | WS_SYSMENU, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, 0, 0);
	dc = GetDC(hwnd);

	SHFullScreen(hwnd, SHFS_HIDETASKBAR | SHFS_HIDESTARTICON | SHFS_HIDESIPBUTTON);

В эмуляторе, содержимое дисплея представлено переменной LCDBUF, которая содержит в себе RGB565-картинку разрешением 240x240 (чуточку усеченный). Поскольку устройства на Windows CE обычно тоже используют 16-битный цвет, то достаточно было бы просто скопировать их прямо в фреймбуфер дисплея по сканлайнам и получить изображение но... из-за GDI, система принимает только формат RGB5551, который затем снова конвертируется в RGB565 из-за чего получаем лаги на слабых устройствах.

Сначала заполняем структуру BITMAPINFO, описывающую формат изображения ‭«выхлопа‭» эмулятора:

    BITMAPINFO info;
	memset(&info, 0, sizeof(info));
	info.bmiHeader.biBitCount = 16;
	info.bmiHeader.biPlanes = 1;
	info.bmiHeader.biHeight = -240;
	info.bmiHeader.biWidth = 240;
	info.bmiHeader.biCompression = BI_RGB;
	info.bmiHeader.biSize = sizeof(info);

Затем в главном цикле, пока открыто окно, вызываем обработку ввода, следующего цикла NES и наконец, выводим всё на дисплей с помощью SetDIBitsToDevice:

    while(IsWindow(hwnd))
	{
		ProcessInput();

		NEStimer(2);
		SetDIBitsToDevice(dc, 0, 0, info.bmiHeader.biWidth, -info.bmiHeader.biHeight, 0, 0, 0, -info.bmiHeader.biHeight, LCDBUF, &info, DIB_RGB_COLORS);
	}

Результат: эмулятор вполне неплохо работает на шустрых устройствах с процессорами 400+ МГц, причем как на 240x320, так и на 480x800. Осталось лишь добавить ‭«мордашку‭»: окно выбора рома, диалог ремаппинга кнопок, читов (редактирование RAM-консоли) и управления игровым временем. Также очень желательно реализовать адекватный таймер с ограничением в 60 FPS, но... ни один из опробываемых мной девайсов не смог сэмулировать NES в FullSpeed без пропуска кадров. Но как сам факт, Proof of Concept, эмулятор NES у нас уже есть!

В случае с другими эмуляторами, обычно приходится отвязывать ещё и Platform-dependent часть с ‭«мордой‭», интерфейсом, конфигами и иными плюшками. Для портирования выгодно выделяются те эмуляторы, где ядро чётко разграничено с ‭«мордой‭» и где это самое ядро можно вытащить без каких-либо проблем!

❯ Wolfenstein 3D

Дальше я решил портировать небезызвестную игру Wolfenstein 3D. Среди ‭«больших‭» игр с открытым исходным кодом, она относительно нетребовательная (необходимо ~640Кб ОЗУ, то есть теоретически её можно портировать и на жирные микроконтроллеры). В данном случае, брать оригинальный код нет необходимости (в нём есть вставки на x86-ассемблере и совершенно ненужные в нашем случае драйвера для звуковых карт, обработчики аппаратного таймера и прочие особенности DOS-игр), можно начать с современного порта WolfSDL, который использует в качестве библиотеки для вывода графики и обработки ввода библиотеку SDL 1.2.

SDL сама по себе отлично абстрагирует особенности платформы и не особо сложно портируется, а под WinCE порт уже был — причем учитывающий особенности платформы с графикой и кнопками. Собирается SDL легко, с этим проблем не возникло — идём в папку VisualCE, и собираем в VS2005 библиотеку.

Далее начинается самое интересное — портирование самой игры! Сначала игра отказывалась собиратся из-за модуля звука, ведь порта SDL_mixer (плагин к SDL, выполняющий роль софтварного микшера звука) под Windows CE нет. Роль микшера может выполнять и сама Windows с помощью модуля waveout, однако на время портированию звук можно и ‭«выкинуть‭» :) Для этого просто убираем все вызовы функций SDL_mixer, ни к каким структурам и возвращаемым значениям библиотеки, звуковая подсистема игры не привязана.

    if(DigiChannel[which] != -1) return DigiChannel[which];

    int channel = Mix_GroupAvailable(1);
    if(channel == -1) channel = Mix_GroupOldest(1);
    if(channel == -1)           // All sounds stopped in the meantime?
        return Mix_GroupAvailable(1);
    return channel;

Далее игра отказывалась собираться из-за того, что Wolf4SDL использовал POSIX-вызовы типа stat и open/read/write/close. Сами вызовы легко оборачиваются в stdio-аналоги, а stat использовался лишь для проверки существования файла (используется в механизме обнаружения эпизодов игры):

int read(FILE* f, void* buf, int len)
{
	return fread(buf, len, 1, f);	
}

После этого, встал вопрос с обработкой ввода от игры. Игра полагалась на встроенный механизм биндинга кнопок в настройках, однако в случае устройств на WinCE не всегда-то даже DPAD есть, не говоря уже о целой клавиатуре. Поэтому пришлось мудрить и хардкодить часть кнопок для конкретного устройства:

            switch(LastScan)
			{
			case SDLK_LEFT:
				Keyboard[sc_LeftArrow] = 0;
				break;
			case SDLK_KP7:
				Keyboard[sc_RightArrow] = 0;
				break;
			case SDLK_UP:
				Keyboard[sc_UpArrow] = 0;
				break;
			case SDLK_DOWN:
				Keyboard[sc_DownArrow] = 0;
				break;
			case SDLK_SPACE:
				Keyboard[sc_Space] = 0;
				break;
			case SDLK_RETURN:
				Keyboard[sc_Return] = 0;
				Keyboard[sc_Enter] = 0;
				break;
			}

После фикса ещё некоторых мелких ошибок, устранения особенностей путей в WinCE (нет понятия «текущая директория»), игра наконец-то запустилась на эмуляторе!

Портируем игры на практике - 5

А с правкой кнопок и на самом ТСД!

Портируем игры на практике - 6

❯ Заключение

Вот такой интересный материал у нас сегодня с вами получился! Исходный код можно найти на моём гитхабе. Также проекты можно портировать и на GPS-навигаторы на Windows CE, путём добавления виртуальной клавиатуры (однако мультитача нет и не будет. Решением может стать подключение Bluetooth HID-клавиатуы), привнеся новую жизнь ещё и им!

Друзья! Если вас заинтересовал девайс из статьи, то купить его можно здесь за 500 рублей, с полным комплектом (коробочка, диск, блок питания, док-станция и сам девайс, иногда попадаются ревизии с GSM). Это списанные девайсы, но полностью рабочие, даже аккумуляторы отлично держат заряд. Просто у человека их больше 50 штук и он захотел проспонсировать розыгрыш, мало ли кто-то из читателей тоже заинтересуется таким интересным девайсом, как я. Кроме того, два таких красавца мы с вами разыграем в ближайшее время.

Портируем игры на практике - 7

Также у меня есть свой личный Telegram-канал "Клуб фанатов балдежа", куда я публикую посты о программировании, реверс-инжиниринге и просто показываю бэкстейдж статей вперемешку с небольшим щитпостом. Если интересно - подписывайтесь, там же будут опубликованы и условия конкурса. Конкурс начнём проводить как только выйдет видео-версия данной статьи. Ну а пока можете посмотреть мой недавний видос об оживлении ноутбука на 386'ом:

Кстати, если у кого-то из читателей есть ненужные устройства (в том числе с косяками) или дешевые китайские подделки на айфоны/айпады/макбуки и другие брендовые девайсы будучи нерабочими, тормозящими, или окирпиченными и вам не хотелось бы выкидывать их на свалку, а наоборот, отдать их в хорошие руки и увидеть про них статью — пишите мне в Telegram или в комментах! Готов в том числе и купить их. Особенно ищу донора дисплея на китайскую реплику iPhone 11 Pro Max: мой ударник, контроллер дисплея калится и изображения нет :(

Портируем игры на практике - 8

Автор: bodyawm

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js