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 отлично подходит в качестве учебной архитектуры — она довольно простая, можно на ее примере изучить работу процессора и то, как абстрактные логико-математические концепции отображаются на реальное железо. Опыт работы на таком низком уровне незаменим. Так что, надеюсь, мои наработки кому-нибудь пригодятся, хотя бы как референс.

dlib 1.8.0

Очередное крупное обновление dlib. Главной фичей релиза является поддержка декодирования прогрессивных JPEG — жуткий долгострой, который я не мог довести до ума чуть ли не все последнее десятилетие. dlib.image.io.jpeg — это теперь пакет, где все части декодера разнесены в отдельные модули. Также теперь поддерживаются одноканальные JPEG.

Добавлен новый модуль dlib.random.ziggurat и функция gaussian, которая генерирует псевдослучайные числа с нормальным распределением по алгоритму Зиккурат.

Добавлена новая функция rsqrt в dlib.math.base. Это тот самый легендарный быстрый обратный корень Джона Кармака с оригинальными комментариями из кода Quake 3 — я решил добавить эту функцию в dlib как своего рода пасхалку.

Все математические константы в dlib.math.base приведены к единой точности (количеству знаков после запятой).

Исправлена ошибка в методе Array.removeKey.

dlib теперь использует Coveralls вместо Codecov для отслеживания покрытия кода.

Новый план по dlib2

В связи с выходом RTLD, я слегка изменил планы на вторую версию dlib. dlib2 будет по большей части независима от Phobos и основана на RTLD. Это будет по-прежнему библиотека для Windows и POSIX, но появится также поддержка AArch64. Большинство модулей будут вынесены в RTLD, сохраняя, насколько это возможно, обратную совместимость.

Что будет вынесено в RTLD без изменений в первую очередь:

  • Почти все из dlib.core
  • Контейнеры
  • Аллокаторы, арена
  • Базовая математика
  • Пул потоков (возможно, с некоторыми улучшениями)
  • Функции для работы с текстом

Разграничение функций между RTLD и dlib будет не по степени независимости от Phobos, а по степени фундаментальности. Не все функции dlib имеет смысл выносить в RTLD (вследствие спорного дизайна API или ограниченной области применения). Что, скорее всего, так и останется в dlib:

  • Линейная алгебра и вычислительная геометрия (нужны далеко не во всех приложениях)
  • Абстрактные потоки ввода-вывода (архитектурно тяжелые, нужны не всегда)
  • Интерфейс файловой системы (имеет специфический, не очень удобный API; в RTLD будет свой)
  • Работа с изображениями и звуком (специфическая область применения).

Новые фичи dlib2:

  • dlib2.image. Новый API для работы с изображениями, без SuperImage. Старый API будет реализован в пакете dlib поверх нового. Все декодеры графических форматов пойдут в dlib2, также появится декодер DDS. Основная задача пакета — максимальное упрощение прослойки, необходимой для хранения загруженных изображений в памяти и передачи в другие API, а также поддержка сжатых GPU-форматов. Будет использоваться структура, аналогичная TextureBuffer в Dagon. Этот же пакет будет включать функции-хелперы для создания текстур OpenGL ES.
  • Встроенная поддержка OpenGL ES в RTLD позволит создать новый движок холста для рисования векторной графики — dlib2.canvas. Будут реализованы растеризация сплайнов, функции плоттера, вывод изображений, простого текста и т.д. Это будет не полноценный графический движок, но, как минимум, инструмент для визуализации данных, полезный для научных исследований и прототипирования.

RTLD как замена druntime

Я задумал этот проект как простой способ скомпилировать хоть что-то под AArch64, но очень быстро понял, что можно не ограничиваться полумерами и сделать продвинутый рантайм вместо набора заглушек. Знал бы я изначально, какой портал в ад тем самым открываю… Когда я портировал криптографию с C на D, ощущения, как я уже писал, были как будто полез в трансформаторную будку, а тут — как будто изучаешь ядерный реактор!

Под капотом D устроен весьма интересно и очень не просто. Это не C, где все лежит на поверхности. Компилятор D мало что знает о рантайм-семантике языка — он только генерирует машинный код, используя внешние вызовы для всех нетривиальных механизмов. Выделение памяти, проверка границ массивов, классы, RTTI — все это опирается на специальные хуки, которые реализованы в druntime. Следовательно, все это можно переопределить. Если выкинуть druntime и использовать свою библиотеку без GC, то вам придется написать свои реализации нужных вам хуков. Задача это, безусловно, сложная, системная, но я уже много лет занимаюсь подобными вещами, поэтому к созданию RTLD я подошел во всеоружии. Одно потянуло за собой другое, и вот я уже обнаруживаю себя пишущим собственную _d_newclassT, которая реализует встроенный оператор new для классов!

Это дает большую власть, но я все же не замахиваюсь на полное переизобретение druntime. Моя идея состоит в том, чтобы RTLD подменяла его ровно в той степени, чтобы получилось реализовать библиотеку общего назначения уровня dlib и писать соответствующий идиоматичный код. Иными словами, я не планирую ничем заменять GC, но большинство других возможностей языка будут доступны. На сегодняшний день RTLD уже обеспечивает следующее:

  • Классы, интерфейсы;
  • Литералы массивов, а также оператор new для массивов и классов (под капотом — глобальный аллокатор rtld.core.memory, аналогичный dlib.core.memory). Я его, впрочем, не рекомендую использовать, потому что это создает семантическую путаницу — он все равно требует Delete, а это несовместимо с new при компиляции с druntime;
  • Автоматическая проверка выхода за границы массивов;
  • Вывод некоторых распространенных рантайм-ошибок.

Как гласит название, библиотека пригодна для разработки приложений реального времени — в том смысле, что все функции работают за предсказуемое время (druntime опирается на сборку мусора, поэтому в общем случае нельзя полагаться, что вызов функций Phobos не приведет к глобальной блокировке). Если вы пишете обычные приложения, не ограниченные по таймингам и памяти, то вы сможете использовать RTLD ситуативно, для каких-то отдельных задач, не отказываясь в целом от Phobos — обе библиотеки могут благополучно сосуществовать в одной программе. Полный переход на RTLD целесообразен в особых случаях, из которых я выделяю два главных сценария:

  • Системное программирование: если вы пишете низкоуровневую библиотеку с интерфейсом C, которая будет использоваться другими программами — драйвер, сетевой протокол, графический или звуковой API и т.д.;
  • Программирование встраиваемых систем и маломощных компьютеров. Сюда относятся одноплатные ARM-компьютеры вроде Raspberry Pi или Khadas. RTLD имеет минимальный отпечаток памяти, что позволяет использовать ее на самых слабых машинах, имеющих операционную систему.

RTLD — низкоуровневый рантайм для D

Я начал разработку dlib в далеком 2011 году, когда писал на старом добром OpenGL 1.x под 32-битным Linux. Тогда я и не помышлял об альтернативных архитектурах и кросс-компиляции, мне даже в голову не приходило, что у D когда-нибудь появится поддержка такого количества разнообразных платформ. Но к началу 20-х уже стало очевидно, что грядет эпоха ARM, и старичку x86 придется потесниться, а сегодня AArch64 на устройствах мощнее телефона — уже не причуда и экзотика, а обыденная реальность. Стоит это принять и начать уже думать об ARM не как о мобильной, а как о второй десктопной архитектуре. Это необычное положение дел, потому что на десктопе x86 всегда царил монопольно. Но я вообще-то люблю изучать все новое, и для меня как графического разработчика освоение еще одной процессорной архитектуры — интересный вызов. Надо ли прямо сейчас всем кидаться портировать весь софт на ARM? Я думаю, нет, но какие-то базовые штуки, полезные библиотеки, фреймворки — желательно. Вот и я решил не оставаться в стороне.

Хорошая новость состоит в том, что D, в лице компилятора LDC, отлично поддерживает ARM. LDC — это неявный кросс-компилятор, он умеет генерировать код почти под все платформы, поддерживаемые LLVM, и это можно делать на любой хост-системе, не пересобирая тулчейн. ARM существует в разных аппаратно-программных вариантах, я под ним буду подразумевать aarch64-linux-gnu — то есть, платформу с процессором AArch64, ядром Linux и окружением GNU. Проще говоря, установив LDC, вы можете на x86 собирать приложения под десктоп или сервер на базе AArch64, о чем я уже писал в предыдущем посте.

Плохая новость состоит в том, что для компиляции под нестандартные таргеты нужны соответствующие версии Phobos и druntime. Я боюсь представить, что это за квест — собрать их, и не имею никакого понятия о статусе Phobos для AArch64. Мне это даже не очень интересно, потому что Phobos я считаю не самой удачной стандартной библиотекой, я ее почти не использую — она сильно перегружена, половину можно было бы оттуда вынести в дополнительные DUB-пакеты, многие модули откровенно устарели, некоторые функции работают странно, а шаблоны порой не компилируются. Для меня уже привычен хардкорный путь — игнорировать Phobos и писать все свое. К тому же, под маломощными одноплатниками с маленькой памятью вам все равно GC не друг, и лучше все делать на @nogc-компонентах.

Все эти измышления подвели меня к идее создания новой библиотеки общего назначения, более низкоуровневой, чем dlib. Я ее назвал RTLD — Real-Time Library for D. Она основана на модулях, которые я изначально писал для dlib2/dcore, а также на прямых портах из dlib 1.x. В отличие от dlib, это будет настоящая замена Phobos. RTLD предоставляет самую базовую функциональность, работая поверх libc, WinAPI, POSIX, системных вызовов Linux, а также динамически загружаемых функций — X11 и OpenGL ES. По сути, это библиотека для тех, кто хочет писать на D как на C, не используя ничего лишнего. Однако я намеренно не стал делать ее BetterC, в отличие от dcore. Взвесив все «за» и «против», я решил, что от BetterC на десктопе толку мало — от классов никуда не деться, они все равно потребуются на определенном уровне абстракции, поэтому зачем изначально себя их лишать?

https://github.com/gecko0307/rtld

На данный момент RTLD поддерживает Windows и Linux, компилируется под x86_64 и AArch64. Некоторые ее модули платформонезависимы, и их можно использовать в системной разработке, под bare metal.

Текущий список фич:

  • rtld.core — ключевые функции: работа с динамической памятью, загрузчик библиотек, потоки, мьютексы, создание окна
  • rtld.gl— биндинг к OpenGL ES 2/3 и создание контекста (поддерживаются Windows и Linux/X11)
  • rtld.libc — функции библиотеки C
  • rtld.math — элементарные математические функции
  • rtld.random — генератор псевдослучайных чисел
  • rtld.sys.windows — функции WinAPI
  • rtld.sys.posix — функции POSIX
  • rtld.sys.linux — функции Linux
  • rtld.text — текстовые кодировки
  • rtld.time — функции даты-времени, наносекундный таймер.

Закономерен теперь вопрос архитектуры будущей dlib2. Я склоняюсь к мысли отказаться от dcore в пользу RTLD — это будет базовый бэкенд dlib2, реализующий львиную долю функциональности, которая не зависит от GC. К сожалению, полный отказ от GC в dlib невозможен по ряду причин, но я надеюсь сильно сократить его присутствие.

Кросс-компиляция «Hello, World» под ARM

Я всю жизнь разрабатываю под x86 (за исключением экспериментов с MIPS-I/PSX, но это больше из исторического интереса), и мне всегда хотелось написать что-нибудь на D под вторую главную архитектуру современности — AArch64. Недавно я досконально изучил, как это правильно делать, причем самым, на мой вкус, безболезненным способом, который не требует WSL, Docker и монструозных тулчейнов — понадобится только обычный свежий LDC плюс несколько дополнительных бинарей, которые легко скачать в готовом виде.

Итак, программа (src/main.d):

module main;

extern(C) nothrow @nogc
{
    int printf(const(char)* fmt, ...);
}

extern(C) int main(int argc, char** argv) nothrow @nogc
{
    printf("Hello, World!\n");
    return 0;
}

Для начала нужно убедиться, что ваша версия LDC собрана с поддержкой aarch64: ldc2 --version.

Важный вопрос: в каком режиме компилировать — BetterC или нет? По моему мнению, на большинстве популярных платформ BetterC дает ноль преимуществ, отнимая важные фичи языка вроде классов. Он хорош для микроконтроллеров, для WASM, для всякой экзотики, но точно не для привычного GNU userspace, который наличествует на любой железке мощнее калькулятора. Так что я предлагаю сразу идти нормальным путем и заменить те функции druntime, которые вам нужны, самописными реализациями. Для простейшего примера, впрочем, это не требуется.

Моя команда сборки выглядит следующим образом:

ldc2 -Isrc -i --vgc
    -defaultlib= ^
    -debuglib= ^
    -mtriple=aarch64-linux-gnu ^
    -linker=lld ^
    -Xcc=--sysroot=sysroot/aarch64-linux-gnu/libc ^
    -Xcc=--gcc-toolchain=sysroot ^
    -Xcc=-unwindlib=none ^
    src/main.d ^
    -of=test

Она предполагает, что в папке sysroot находится минимальное системное окружение, необходимое для компиляции linux-приложений. Я его взял отсюда: https://github.com/ClickHouse/sysroot. Заберите папку linux-aarch64 из репозитория со всем содержимым и переименуйте в sysroot. Это же окружение понадобится для тестирования.

При желании можно оформить то же самое для DUB:

{
    "name": "test",

    "targetType": "executable",
    "targetName": "test",

    "sourcePaths": ["src"],
    "importPaths": ["src"],

    "dflags-ldc": [
        "--vgc",
        "-defaultlib=",
        "-debuglib=",
        "-mtriple=aarch64-linux-gnu",
        "-linker=lld",
        "-Xcc=--sysroot=sysroot/aarch64-linux-gnu/libc",
        "-Xcc=--gcc-toolchain=sysroot",
        "-Xcc=-unwindlib=none"
    ],
    "postBuildCommands-windows": [
        "copy /Y test.exe C:\\Users\\gecko\\Documents\\vbshared\\arm\\test",
        "del test.exe"
    ]
}

Последнее нужно под Windows, чтобы на выходе получился бинарник без расширения «.exe».

Если все хорошо, то вы получите файл test, который можно сразу проверить:

file ./test

Команда выведет что-то вроде

./test: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV),
dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for
GNU/Linux 3.7.0, with debug_info, not stripped

Если есть ARM-компьютер, то можно запустить приложение на нем (я тестировал на Khadas VIM2), но для тестов гораздо практичнее настроить виртуальное окружение. Если у вас Linux, то рекомендую https://github.com/multiarch/qemu-user-static — нужно скачать qemu-aarch64-static на странице релизов. Это эмулятор пользовательского режима QEMU, который позволяет запускать бинарники AArch64 непосредственно на хост-системе.

Использую я его так:

./qemu-aarch64-static -L ./sysroot/aarch64-linux-gnu/libc/ ./test

Параметр -L передает эмулятору путь к корневой папке окружения, там должны быть динамический линкер /lib/ld-linux-aarch64.so.1 и папка lib64 с библиотеками C/POSIX — проверьте. Важно убедиться, что вместо симлинков не лежат пустые файлы, под Windows часто так получается.

Для Windows я, к сожалению, не нашел эмулятор юзерспейса linux-aarch64 (может, его и нет вовсе). Ручной запуск ARM-машины QEMU — это жуткий геморрой. Самый беспроблемный способ, который не заставляет постоянно запускать и останавливать тяжелые сессии эмуляции (равно как и вводить много команд вручную) — тупо установить обычный десктопный Linux в VirtualBox и там гонять qemu-aarch64-static, не выключая систему. Это немного костыльно, но жить можно, особенно если держать весь проект сразу в shared-папке хоста. Мне лично это удобно тем, что я и так использую Linux на виртуалке для других тестов.

Рендеринг волос в Dagon 2

Добавил анизотропную BRDF Kajiya-Kay для рендеринга волос:

Текстура волос может иметь альфа-канал, но вместо альфа-смешивания используется упрощенная прозрачность на основе дизеринга Байера.

dlib 1.7

В рамках Dagon 2.0 скопилось множество улучшений для dlib, и данный релиз подытоживает эту работу.

Добавлен модуль dlib.math.base — замена std.math, которая заменяет некоторые часто используемые функции быстрыми интринсиками LLVM при компиляции LDC (подробнее об этом тут). Ускорены следующие элементарные функции: fabs, sqrt, sin, cos, tan, ceil, floor, round, trunc, rint, nearbyint, pow, exp, exp2, log, log2, log10, fmax, fmin, fma, copysign. В среднем они вдвое быстрее, чем в Phobos. Единственный нюанс, который следует помнить — лучше не импортировать оба модуля одновременно, иначе будет конфликт, и вам придется использовать квалифицированные имена при вызовах.

Добавлена встроенная SIMD-оптимизация для перемножения матриц 4×4 (Matrix4x4f). Это ускоряет оператор в 3-4 раза. В связи с этим удален модуль dlib.math.sse как устаревший, неподдерживаемый и, как выяснилось, нерабочий под x86_64.

Хэш-таблица FlatHashMap из Dagon 2.0 добавлена в библиотеку как dlib.container.hashmap. Реализация xxHash64, соответственно, — как dlib.hash.xxhash64 в новом пакете dlib.hash. Напомню, что это очень быстрая реализация ассоциативных массивов без использования сборщика мусора, которая в десятки раз эффективнее, чем dlib.container.dict и в 1,5-2 раза — чем встроенные ассоциативные массивы D.

Полностью переработан dlib.random — пакет теперь работает на основе перемешанного конгруэнтного генератора (PCG), реализованного для 32- и 64-битных чисел. Это современный эффективный генератор псевдослучайных чисел, который превосходит как Вихрь Мерсенна (std.random), так и линейный конгруэнтный генератор (rand в стандартной библиотеке C). Добавлены новые функции choice (случайный выбор из заданного множества вариантов) and rollDice (случайное целое число от 0 до заданного порога). Функция seed, генерирующая зерно для инициализации генератора из системной энтропии, теперь использует функцию смешивания fmix64 из MurmurHash3 вместо mix Боба Дженкинса.

Не используйте std.random!

Стандартная библиотека использует классический Вихрь Мерсенна (MT19937). Недавно я, вдохновившись эффективностью PCG для шейдерных вычислений, добавил соответствующий модуль в dlib, заменив им rand в dlib.random. Оказалось, что моя функция random для выборки равномерного распределения в диапазоне 0..1 аж в 4 раза быстрее, чем std.random.uniform!

Бенчмарк для 10000000 вызовов:

std.random.uniform: 77 ms, 38 μs, and 3 hnsecs total
dlib.random.random: 19 ms, 599 μs, and 3 hnsecs total

Стандартное отклонение у обеих функций примерно одинаковое:

std.random.uniform: 0.544σ
dlib.random.random: 0.413σ

Статья по теории рендеринга в Dagon

Наконец-то дошли руки написать теоретическую статью о модели освещения, которую я использую в движке, со всеми формулами и ссылками на научную литературу. Это GGX/Trowbridge-Reitz, на которую опирается освещение в Unreal Engine 4 и многих других движках — физически обоснованная модель зеркально отраженного света для изотропных шероховатых поверхностей. Ее многие сейчас используют, одни и те же уравнения можно встретить в самых разных движках, но мало кто задумывается, откуда они взялись.

Поскольку в тексте много математики, я оформил статью в виде PDF — можно скачать файл или читать при помощи встроенного в браузер просмотрщика.