Нововведения в 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-макросы и библиотеки байт-кода. Они заслуживают отдельных статей.

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

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

(далее…)

Dagon 0.30.0 и 0.31.0

Выпустил подряд две версии движка. В ядро Dagon внесен фреймворк многопоточности и обмена сообщениями, о котором я подробно писал в предыдущем посте. EventManager.userEventQueue переименовано в EventManager.outboxEventQueue, EventManager.numUserEvents — в EventManager.numOutboxEvents. Также теперь рекомендуется использовать EventManager.queueEvent вместо EventManager.addUserEvent, EventManager.queueFileChangeEvent вместо EventManager.generateFileChangeEvent, EventManager.queueLogEvent вместо EventManager.asyncLog.

Заметно улучшен пакет dagon.collision, хотя он пока и далек от продакшн-уровня. Исправлены баги в модуле BVH, добавлена реализация GeomTriangle.boundingBox, а также экспериментальный алгоритм проверки столкновений GJK (dagon.collision.gjk). EPA пока не поддерживается, так что функция gjkTest не возвращает информацию о контакте — основным алгоритмом проверки столкновений остается MPR. Метод CollisionShape.supportPointGlobal теперь просто CollisionShape.supportPoint.

В Dagon 0.31.0 я продолжил улучшение пакета core. Добавлены свойства Application.path и Application.directory — соответственно, полный путь к исполняемому файлу и папка, в которой он лежит. Под Windows доступно свойство Application.hwnd для получения дескриптора окна игры. VFS теперь монтирует в качестве последнего источника данных папку, где хранится приложение, а не рабочую папку. Благодаря этому можно в командной строке запускать приложение не из текущей папки.

Экспериментальная фича: поддержка ввода с графических планшетов (пока только под Windows через Wintab). Абстрактный интерфейс InputDevice для добавления в EventManager кастомных устройств ввода. Новые типы событий EventType.PenMotion, EventType.JoystickAxisMotion, EventType.LocaleChange.

В deferred-рендер добавлена поддержка перспективных теневых карт (PSM) для конусных источников света.

Dagon 0.31.0 является последней версией, использующей OpenGL 4.0 — со следующей движок переходит на 4.3, что позволит добавить поддержку вычислительных шейдеров.

Конкурентное программирование в Dagon

Идея добавить в EventManager поддержку конкурентности/асинхронности не давала мне покоя еще со времен DGL, и вот, наконец, мне это удалось. Точнее, асинхронный слой удобно лег поверх стандартной событийной шины, и для этого не пришлось менять в ней практически ничего.

(далее…)

Обновление dlib.image

В dlib.image появилась возможность отслеживать прогресс во время работы фильтров. Для этого используется многопоточность – необходимо создать класс-враппер, наследующий от FilteringThread. Прогресс (от 0 до 1) считывается из свойства progress для SuperImage. В данном примере показано, как использовать эту функциональность для вывода прогресса свертки в консоль:

import std.stdio;
import dlib.image.image;
import dlib.image.io.png;
import dlib.image.filters.convolution;
import dlib.image.fthread;

class ConvolutionThread: FilteringThread
{
    float[] kernel;
    
    this(SuperImage img, float[] k)
    {
        super(img);
        kernel = k;
    }
    
    override void run()
    {
        output = image.convolve(kernel);
    }
    
    override void onRunning()
    {
        writef("Convolving %s%%", cast(uint)(image.progress * 100));
        write("r");
        stdout.flush();
    }
    
    override void onFinished()
    {
        writeln();
    }
}

void main()
{
    auto img = loadPNG("test.png");
    img = (new ConvolutionThread(img, Kernel.Emboss)).filtered;
    img.savePNG("output.png");
}