Нововведения в GScript3

Давненько не писал о GScript! Между тем, VM языка уже пригодна для использования в играх, а спецификация пополнилась множеством интересных фич, о которых я сейчас и расскажу.

Изоляция памяти потоков

О потоках в GScript3 я уже писал в отдельном посте. Разрабатывая модель памяти языка, я придумал совершенно новую парадигму, которую в существующих реализациях не видел. Каждый поток имеет свою изолированную арену для динамических аллокаций (с размером блока в 1024 байта). Ссылочные типы — объекты, массивы, динамически созданные строки — в контексте потока создаются в этой арене и по умолчанию помечаются как принадлежащие этому потоку (у каждой переменной GsDynamic есть поле владельца — owner). VM запрещает ссылке покидать изолированный контекст, проверяя владельца при операциях внешней передачи — например, при записи в глобальную переменную. Исключение делается только для главного потока, который имеет право писать куда угодно без ограничений.

Такой подход гарантирует, что данные, созданные потоком, не переживут его самого, и в памяти машины не будет висячих ссылок, что очень полезно для платформы, не имеющей сборщика мусора. Но это несколько ограничивает полезность потоков, поэтому я предусмотрел два вида исключений из этого правила. Если создать данные со спецификатором shared, они будут размещены в арене главного потока и, таким образом, будут валидны вплоть до завершения работы скрипта (в терминах GScript — будут разделяемыми):

const thread = spawn func
{
    const arr1 = shared [10, 5, 6];
    const arr2 = [10, 5, 6];

    global.arr = arr1; // ok
    global.arr = arr2; // error!
};

shared, строго говоря, применяется не к данным как таковым, а к операции конструирования. Например, его можно применить к конкатенации строк, чтобы результат был разделяемым:

const s = shared("a" ~ "b");

Помимо этого, есть ключевое слово escape. Оно, как и shared, применяется к результату выражения. Оно берет неразделяемые данные и делает их разделяемыми — то есть, якобы принадлежащими основному потоку. Это позволяет передать ссылку во внешний контекст, даже если она указывает на изолированную арену.

func test(value)
{
    print value;
}

const thread = spawn func
{
    const arr = [0, 1, 2];
    test(escape arr);
};

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

Каналы

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

const ch = global.channel();

const thread1 = spawn func
{
    print ch.receive();
    ch.send("world");
};

const thread2 = spawn func
{
    ch.send("hello");
    print ch.receive();
};

Приведенная программа выведет строку «hello», затем строку «world». Порядок гарантируется, потому что сначала будет запущен первый поток, который ждет сообщения «hello» от второго, а затем отправляет ему «world».

Проверка типов и обработка ошибок

Тип динамического значения можно сравнить с константой типа с помощью оператора :

const x = 10;
if (x: Number)
{
    // ...
}

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

if (type(x) != Null)
{
    // ...
}

if (type(x) == type(y))
{
    // ...
}

Константы типов в языке следующие:

Null = 0
Number = 1
String = 2
Array = 3
Object = 4
NativeMethod = 5
NativeFunction = 6
Error = 7
Function = 8
Vector = 9

Вы, наверное, заметили, что отдельной константой типа является Error — «ошибка». Ошибка в GScript3 действительно является отдельным типом переменной. Это позволяет не использовать пресловутый Null для обработки ошибок (хотя отказ от Null вообще — фича крайне спорная и даже безумная, я этого не стал делать) и одновременно — реализовать очень простую, не перегружающую синтаксис обработку ошибок на уровне VM.

Простейший случай — функция просто возвращает ошибку:

func myFunc()
{
    return error("BADBEAF");
}

Значением ошибки является строка, которую вызывающий код может вывести в консоль.

const thread = spawn myFunc;

let running = true;
while(running)
{
    running = thread.running;
    const result = await thread;
    if (result: Error)
    {
        print result;
    }
}

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

func test()
{
    raise error("Something");
}

func threadFunc()
{
    test();
}

const thread = spawn threadFunc;

let running = true;
while(running)
{
    running = thread.running;
    const result = await thread;
    if (result: Error)
    {
        print result;
    }
}

Векторы

Свежее нововведение. Поскольку GScript3 создан с прицелом на игры, в языке не помешает векторный тип данных, и теперь он есть. Поддерживаются арифметические операции и встроенные функции (интринсики) для ускорения вычислений:

const v = vector(3, 2, 1) + vector(1, 2, 3);
print v;
print normalize(v);

Вектор является стековой структурой и хранится в стандартном GsDynamic.

VM реализует следующие интринсики для векторов: dot, cross, normalize, distance.

В этом посте я не затронул еще две важные темы — AST-макросы и библиотеки байт-кода. Они заслуживают отдельных статей.

Новости по Dagon 2

Разработка Dagon 2.0 началась в весьма бодром темпе. Deferred-рендер уже почти готов — остались только тени PSM и DPSM, поддержка локальных зондов освещения и forward-проход. Я добавил систему кэширования ресурсов, сжатие в BC7 (на основе компрессора bc7enc Рича Гелдриха) и проделал еще множество мелких улучшений на разных стадиях формирования кадра. Особое внимание я уделил тому, чтобы картинка соответствовала Eevee в Blender 5.

Что еще нового? Часть функциональности, которая в Dagon 1.0 реализована в качестве расширений, теперь входит в ядро — это физика на базе Jolt и загрузчик текстур в формате KTX/KTX2. Такое решение я принял исходя из полезности этих фич, простоты сборки Jolt и libktx из исходников и их автономности: они не имеют собственных зависимостей и отлично работают на всех платформах (в противовес тому же Newton, который имеет проблемы с работой некоторых функций под Linux). Наличие libktx «из коробки» дает серьезные преимущества и ставит Dagon 2 в авангард движкостроения; в будущем не исключен перевод текстурного кэша с DDS на KTX2.

Еще одним нововведением будет встроенная VM GScript3, которую я разработал в прошлом году. Движок при старте загружает скомпилированный байт-код скрипта и выполняет его, а скрипт, в свою очередь, навешивает обработчики событий, позволяя, таким образом, выполнять внешнюю логику без пересборки игры. Игра может экспонировать скриптовой системе свои данные и методы, что полезно для создания модов. Некоторые встроенные классы Dagon уже реализуют интерфейс GsObject и напрямую совместимы с GScript: это Entity, Scene, World, BaseGame.

Зарегистрирован пакет dagon2 в реестре DUB, так что начать пользоваться можно уже сейчас, несмотря на то, что разработка находится на ранней стадии.

Итоги 2025 года

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

  • Перенес почти все свои онлайн-ресурсы на новый сервер. Блог переехал на новый домен и был переименован в PixelPerfect Blog. Заодно я добавил некоторые оптимизации, за счет которых он теперь работает заметно быстрее;
  • Выпустил множество версий Dagon, от 0.19 до 0.36. Я перешел на новый итеративный принцип обновлений: теперь релизы выходят по мере добавления небольших наборов фич, вместо того, чтобы по полгода накапливать много изменений. В этом году движок был очень серьезно обновлен, перечислю тут лишь наиболее важное.
    • Состоялся переход на OpenGL 4.3;
    • Добавлена поддержка вычислительных шейдеров;
    • Появилась поддержка GPU-скиннинга и анимированных моделей glTF;
    • Реализована поддержка HiDPI;
    • Реализовано расширение для работы с устройствами виртуальной реальности посредством OpenVR;
    • В систему событий добавлена многопоточность с асинхронным обменом сообщениями между потоками;
    • Улучшена система загрузки текстур: изображения теперь загружаются через библиотеку SDL2_Image, что обеспечивает поддержку новых форматов — WebP, AVIF, SVG и многих других. Также реализована поддержка текстур в формате KTX/KTX2 с транскодированием из Basis Universal в различные профили аппаратного сжатия. Добавлена поддержка анизотропной фильтрации текстур;
    • Встроенный звуковой движок на основе SoLoud;
    • Поддержка видео на основе libVLC;
    • Поддержка теней для позиционных источников света (PSM и DPSM);
    • Объемное рассеивание для позиционных источников света;
    • Префильтрация кубических карт;
    • Бокс-проекция для зондов окружения;
    • Упрощенный рендер SimpleRenderer для казуальных игр и ретро-графики;
    • Шейдер океана;
    • Шейдер луж для ландшафта;
    • Поддержка Matcap-проекций для текстур;
    • Встроенная система проверки столкновений;
    • Логгер;
    • Простой GUI-тулкит;
    • Поддержка нескольких игровых контроллеров;
    • Ввод с Wintab-совместимых графических планшетов с учетом силы нажатия;
    • Поддержка локализации приложений;
    • Обновился сайт Dagon. Также у проекта наконец-то появилась онлайн-документация.
  • Выпустил dlib 1.3.1, 1.3.2, 1.3.3 и 1.4.0;
  • Написал биндинги к Vulkan, GLSLang, SPIRV-Cross, PhysFS. Все мои биндинги теперь живут в GitHub-организации DLangGamedev;
  • Значительно обновил Chillwave Drive, демку физики автомобиля;
  • Начал работу над GScript3, третьей версией моего скриптового языка;
  • Форкнул Programmer’s Notepad, мой любимый текстовый редактор под Windows, и выпустил три новые версии, 2.5, 2.6 и 2.7, с несколькими важными изменениями по сравнению с оригиналом;
  • Завершил работу над Xtreme3D 4.0.

Ну и, конечно, мой личный список интересных событий в мире D, Open Source и IT в целом:

  • D используется в NASA! В этом году стало известно, что ПО для сбора данных в рамках миссии TRACERS было написано на D;
  • D отметили в списке 10 самых быстрых языков программирования;
  • Выход демо-версии The Art of Reflection, новой инди-игры на D;
  • Выход в ранний доступ InZOI, next-gen симулятора жизни с реалистичной графикой — это первый в истории серьезный конкурент серии The Sims. Несмотря на сыроватый геймплей, игра поражает технологическим уровнем, особенно возможностями встроенных нейросетей для генерации моделей;
  • GreenSock Animation Platform, самый мощный фреймворк браузерной анимации, стал полностью бесплатным. Я использую GSAP во всех своих коммерческих браузерных играх, так что новость для меня весьма радостная.

Многопоточность в GScript3

Одна из ключевых новых фич GScript3, отличающих язык от предшественников — поддержка многопоточности. Если конкретнее, VM языка реализует гибридную кооперативно-вытесняющую многозадачность. В отличие от большинства языков, где многопоточность завязана на системные API, в GScript3 потоки — это встроенная фича VM. Они ближе к корутинам, но при этом управляются планировщиком, что делает их очень похожими на настоящие потоки, только без оверхеда ОС.

(далее…)

GScript3

В D все очень плохо со встраиваемыми скриптовыми языками. Перепробовал много вариантов — Lua, Python, AngelScript — везде боль, все делается через неимоверно сложные API, где элементарно привязать к скрипту свою функцию — это целый квест. Чтобы, например, использовать биндинг dangel, нужно патчить рантайм языка для поддержки соглашения вызовов функций D, без этого там ничего не будет работать. Под Lua-биндинги очень сложно найти нужную версию библиотеки, чтобы приложение не крашилось. Pyd, привязка Python — вообще какой-то фантастически запутанный фреймворк из compile-time костылей. Нативные языки, написанные на D, по большей части устаревшие, неподдерживаемые и тупо не компилируются.

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

(далее…)

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

Собственно сабж. Оказывается, Variant, стандартная реализация tagged union в Phobos, плоховато подходит для вычислений в реальном времени. Не знаю, что там наворотили, но бенчмарки, которые я сделал при разработке GScript3, показали ускорение в 900%, когда я заменил Variant на кастомный динамический тип. Я замерял выполнение скриптового счетчика от 0 до 100000000, и версия на Variant завершилась за 45 секунд, версия на моем GsDynamic — всего за 5!

О самом языке GScript3 расскажу в ближайшее время — я решил актуализировать этот старый проект и уже сделал много интересного.

Итоги года

Вот и пролетел еще один год — самое время подводить итоги по проделанной работе!

  • Вышли подряд нескольно новых версий dlib (0.3 и 0.4). Появилась поддержка абстрактных потоков ввода/вывода, а также платформонезависимый интерфейс файловой системы и его реализации для Windows и POSIX. Пакет обработки изображений теперь поддерживает JPEG, TGA и BMP, распараллеливание, HDRI. В пакете линейной алгебры состоялся серьезный рефакторинг матриц, появилась поддержка инверсии через LU-разложение.
  • Было выпущено 6 номеров электронно-познавательного журнала «FPS» (№№ 28, 29, 30, 31, 32, 33). Появился новый сайт проекта (http://fps-magazine.cf). Также «FPS» теперь доступен в качестве мобильного приложения для Android и iOS. Кстати, в феврале 2015 года журналу исполняется уже 7 лет!
  • Вышла игра 2048х2 — клон 2048 для двух игроков.
  • Улучшен физический движок dmech: реализован новый кэш контактов, добавлена поддержка составных тел, улучшена поддержка ограничений, добавлены статические тримеши, поддержка raycast и игровой кинематики.
  • Графический движок Atrium теперь развивается как самостоятельный проект — DGL. Это объектно-ориентированная надстройка над OpenGL, SDL и Freetype с собственной системой событий, виртуальной файловой системой с поддержкой ZIP-архивов, своим форматом хранения сцен, поддержкой шейдеров, мультитекстурирования, скелетной анимации, выводом текста в UTF-8, а также встроенными средствами интернационализации.
  • Разработан скриптовый язык GScript — минималистичный императивный язык с динамической типизацией, идейно близкий к D, JavaScript и Python. GScript можно будет использовать в качестве скриптовой системы в игровых движках.
  • Вышла новая версия системы сборки проектов Cook 2.0.1 — с новой системой аргументов командной строки, обновленным парсером импортов, поддержкой внешних зависимостей (в том числе из Git-репозиториев), улучшенной системой конфигурации.
  • Обновилась страница проекта Atrium.

GScript — скриптовый язык для D

В игровом движке трудно обойтись без какого-либо способа динамического задания логики и поведения объектов, поэтому я решил написать для Atrium скриптовый язык. Это очень простой императивный язык с динамической типизацией и (пока) всего одним внутренним типом – float.

Что уже реализовано:

  • Модульная система, как в D;
  • Функции (есть поддержка рекурсии);
  • Локальные переменные;
  • Передача аргументов по значению и по ссылке. Что интересно, передача по ссылке возможна в любую функцию, так как ссылочный тип указывается при конкретном вызове функции, а не при ее объявлении;
  • Условный переход if…else;
  • Цикл while;
  • Возможность расширять язык собственными функциями на D.

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

Пример кода на GScript:

import myPackage.myModule;

func main()
{
    var x = 10;
    var a, b;

    a = x * 2 + 1;

    while(a > 0)
    {
        a = a - 1;
        b = b + 1;
    }

    writeln(x, a, b);
}

Исходники проекта доступны на GitHub:
https://github.com/gecko0307/gscript
Примеры скриптов

Приветствуются предлолжения и пожелания – какую функциональность вы бы хотели видеть в языке (оговорка: поддержка ООП в ближайшее время не планируется).