Атрибуты, макросы и статический аллокатор в Retrolang

Retrolang похож на C, но сходство лишь базовое. В действительности я не собираюсь копировать C — не только потому что это фактически мне не нужно, но и потому что программирование под PSX требует весьма специфических возможностей языка, почти на уровне DSL (при этом я не стал их прибивать гвоздями к MIPS, они теоретически могут быть реализованы на системном уровне для любой архитектуры).

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

uchar data[] @("file.bin");

На PSX слишком мало памяти, чтобы использовать динамический аллокатор, как в современных ОС. Программа должна заранее распланировать память, которую она потребляет. Если доверить эту задачу компилятору, то он может все проверять и не допускать коллизий (в то время как ручная работа с диапазонами адресов чревата ошибками). Для этого в Retrolang используются все те же атрибуты:

macro Region = (0x80050000, 2048);
int someData[512] @Region;

Это статически выделит 512*4 байт в регионе MyRegion. Для этого в кодогенераторе предусмотрена специальная таблица регионов и аллокаций.

Преимущество такого подхода перед выделением статической памяти в секции данных в том, что бинарник остается маленьким, а это иногда критично. Кроме того, регионы поддерживают Scratchpad:

int someData[64] @SCRATCHPAD;

Выделенную память можно затем использовать в качестве арены для создания в рантайме более мелких объектов.

Во-вторых, AST-макросы. Это киллер-фича, которая делает ненужным препроцессор C. Препроцессор в Retrolang все равно есть, но возможности его ограничены, и я не планирую их развивать. Другое дело — макросы. Они позволяют переиспользовать практически любые выражения и утверждения языка, параметризуя их. Это похоже не шаблоны, но шаблон обычно параметризуется типами, а макрос — константами, идентификаторами, выражениями и другими макросами. Как и шаблоны, макросы — тоже разновидность метапрограммирования, но их главное назначение — явный инлайнинг кода. C инлайнит непредсказуемо, а Retrolang — язык с предсказуемой семантикой, в котором каждая операция транслируется в машинный код буквально. Поэтому в условиях, когда вызов лишней процедуры — дорогое удовольствие, очень важно переиспользовать код статически. Например, вместо того, чтобы создавать функцию с командами GTE, можно сделать соответствующий макрос, и команды будут вставлены непосредственно в вызывающий код без оверхеда на движение по стеку:

macro gte_upload_transform(tr)
{
    ctc2(GTE_TRX, tr->tx);
    ctc2(GTE_TRY, tr->ty);
    ctc2(GTE_TRZ, tr->tz);
    
    gte_set_matrix(tr->r);
    
    ctc2(GTE_H,   tr->h);
    ctc2(GTE_OFX, tr->ofx);
    ctc2(GTE_OFY, tr->ofy);
    ctc2(GTE_DQA, tr->dqa);
    ctc2(GTE_DQB, tr->dqb);
}

Макросы также удобны для создания удобочитаемых оберток над функциями BIOS.

Retrolang — язык для PlayStation 1

PS1 — наверное, одна из самых культовых консолей всех времен. Количество часов моего детства, проведенных за ней, бессчетно. Я иногда даже вставал пораньше, чтобы часок поиграть перед школой) Классические части Need for Speed и Final Fantasy, Tekken и Soul Blade, Twisted Metal, Driver, TOCA, Crash Bandicoot, Spyro the Dragon, Medievil, Sim Theme Park — список моих любимых игр с первой «соньки» можно продолжать долго. В некоторые кроссплатформенные игры типа GTA, Quake 2, Myst я тоже впервые поиграл именно на PS. 3D-графика в то время сносила крышу и казалась совершеннейшей фантастикой — я ей увлекся как раз благодаря любимой консоли. А с появлением компьютера и Интернета я быстро освоил эмуляцию, стал качать образы игр, которые было не найти в магазинах, и записывать свои диски.

Надо ли говорить, что мне всегда хотелось научиться программировать под PS. Я редко пишу об этом, но этой темой я периодически занимаюсь с конца нулевых, и с тех пор скопил много информации, инструментов и исходников. Началось все с того, что я научился компилировать проги при помощи утекшего в Сеть старого официального SDK PsyQ, который использует C89. Именно на нем было создано большинство хитов. С ним работать довольно удобно, несмотря на то, что это древний софт, написанный еще в эпоху Windows 95. Но мне показалось, что как-то нехорошо зависеть от пиратского ПО, пусть и заброшенного. К тому же, он закрытый, его невозможно модифицировать. Поэтому я взялся писать собственную графическую библиотеку PSXLib. Дело пошло, я довольно глубоко погрузился в детали на уровне ассемблера, но мне ни один существующий ассемблер для консоли не понравился по ряду причин, о которых сейчас излишне рассказывать. Я решил сделать свой собственный, который назвал Retrolang, попутно изучив архитектуру MIPS-I и протокол загрузки PlayStation во всех деталях.

Недавно я взял этот проект и ради прикола скормил его Claude, чтобы он сгенерировал под это дело компилятор языка чуть ниже уровнем, чем C. Получилось лучше, чем я ожидал! Я добавил простейший препроцессор (#define, #include) плюс еще кое-какие расширения, и получилась вполне рабочая штука для низкоуровневых экспериментов. Недавно я допилил все это до состояния альфа-версии и выложил на GitHub:

https://github.com/gecko0307/retrolang

Язык во многом похож на C, но поддерживает не все его фичи. Используется стандартный для 32-битной архитектуры набор целочисленных типов (char/uchar, short/ushort, int/uint). 64-битных чисел нет, как нет и float’ов. Поддерживаются указатели, массивы, строковые литералы, структуры (в том числе вложенные). Вызовы функций соответствуют ABI O32, за исключением аргументов на стеке — они пока не поддерживаются, поэтому функции могут принимать только по 4 аргумента.

В данный момент можно выводить текст в последовательный порт (местный аналог TTY), определять нажатия кнопок геймпада и рисовать простейшую графику, причем все это получается более-менее читаемо. Например, вот как выглядит Hello, World:

void main()
{
    bios_a(0x13, "Hello, World!\n");
}

Вызовы BIOS являются интринсиками компилятора — это фича, которой не было ни в PsyQ, ни где-либо еще, такие вызовы раньше всегда делались через ассемблерные вставки.

Есть возможность встраивать файлы в бинарник — очень не хватало такого в C. Я для этого ввел специальный синтаксис атрибутов для глобальных переменных:

char* timFile @("assets/img.tim");

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

И, конечно, есть ассемблер, с которого проект и начался. Ассемблерные вставки добавляются в программу при помощи таких же атрибутов:

void asmFunc() @("asmFunc.s");

Код тела такой функции реализуется во внешнем ассемблерном листинге. При этом asmFunc — обычная функция с прологом и эпилогом.

Я работаю над стандартной библиотекой с собственным драйвером GPU, который отчасти основан на коде из PSXLib. Реализован как прямой рендеринг через GP0, так и непрямой через командные буферы и DMA. Есть начальная поддержка 3D-графики — трансформация вершин в экранное пространство и Z-упорядочивание. Правда, 3D там намного сложнее, чем 2D, и требует самых изощренных хаков, с которыми я сталкивался в своей жизни как графический разработчик. Это вам не в Unity кнопочки тыкать. Перед людьми, которые придумали, как рендерить 3D-модели на платформе с 1 мегабайтом видеопамяти и без Z-буфера, я снимаю шляпу (хотя имен их не знаю).

Трудно сказать, для чего все это сегодня нужно — эпоха 32-битных консолей давно в прошлом, и оценить подобные штуки могут лишь единичные любители ретрокомпьютинга и системного программирования. Это, конечно, в основном хобби для души. С другой стороны, MIPS отлично подходит в качестве учебной архитектуры — она довольно простая, можно на ее примере изучить работу процессора и то, как абстрактные логико-математические концепции отображаются на реальное железо. Опыт работы на таком низком уровне незаменим. Так что, надеюсь, мои наработки кому-нибудь пригодятся, хотя бы как референс.