HiDPI и другие нововведения

В Dagon значительно обновляется система конфигурации. Многие опции из settings.conf я вынес в отдельные конфиги — render.conf и audio.conf. Также наконец-то появилась поддержка переопределения опций в конфигах на разных уровнях фиртуальной файловой системы — то есть, если создать settings.conf в APPDATA/<game>, то в нем можно переопределить опции, заданные в корневом settings.conf в папке с приложением. Корневой конфиг теперь загружается на ранней стадии инициализации, что позволяет задать в нем настройки логгера и вообще переопределить практически все доступные параметры приложения. Можно даже задать кастомные пути к SDL2, SDL2_Image и FreeType, что полезно под Linux.

Список новых опций settings.conf: log.enabled, log.level, log.toStdout, log.file, log.timestampTags, log.levelTags, appDataFolder, window.resizable, window.x, window.y, window.highDPI, vsync, stepFrequency, gl.debugOutput, font.sans, font.monospace, font.size, SDL2.path, SDL2Image.path, FreeType.path. Последние три поддерживают ОС-постфиксы (SDL2.path.windows, SDL2.path.linux), чтобы задавать разные пути в зависимости от платформы. Опции windowWidth, windowHeight, windowTitle заменены на window.width, window.height и window.title.

В dagon:audio внесены исправления для корректной поддержки многоканальных профилей (5.1, 7.1). Количество каналов можно указать в конфиге audio.conf, например для 5.1:

channels: 6;

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

window.highDPI: 1;

Фактический размер области рисования окна можно узнать через свойства Application.drawableWidth и Application.drawableHeight, а отношение физических пикселей экрана к логическим — через Application.pixelRatio.

На картинке ниже показано, как отличается вывод одного и того же спрайта в обычном режиме (слева) и в HiDPI-режиме (справа).

Большая часть движка уже поддерживает HiDPI из коробки, за исключением HUDRenderer.

Dagon 0.32.0

Очередной релиз Dagon содержит значительное изменение — переход на OpenGL 4.3. Это не должно сильно ударить по поддержке видеокарт (движок работает на всех десктопных видеокартах, выпущенных за последние лет 10), но дает много новых возможностей, в особенности — доступ к вычислительным шейдерам. Соответственно, появился новый класс ComputeShader, о работе с которым я писал ранее. Добавлен один встроенный вычислительный шейдер — dagon.compute.resample, реализация ресэмплинга текстур. Еще одна фича OpenGL 4.3 — кэширование шейдерных бинарников. В теории это должно ускорять их загрузку, хотя в Dagon встроенных шейдеров не так уж и много, и кэширование пока большого значения не имеет. По умолчанию оно отключено — можно включить свойством Application.shaderCache.enabled, либо опцией enableShaderCache в settings.conf. Бинарники сохраняются в папку data/__internal/shader_cache.

Добавлен арена-аллокатор (dagon.core.arena) — я о нем тоже писал отдельный пост. Пока он, впрочем, не используется встроенными классами движка, но пригодится в будущем для интеграции GScript.

Новый модуль dagon.core.dialogs — минимальное кроссплатформенное решение для открытия системного диалога открытия/сохранения файла. Поддерживает Windows и Linux, зависимости при линковке не тянет. Под GNOME работает на основе Zenity, под KDE — на основе kdialog.

Добавлена поддержка так называемого «оконного полноэкранного режима»: если в конфиге заданы нулевые windowWidth и windowHeight, то движок создает безрамочное окно размером с весь экран. Этот режим удобен для мультимониторных конфигураций.

Наконец, добавлено новое расширение dagon:audio для воспроизведения звука через библиотеку SoLoud. Сама библиотека, как обычно, предоставляется в готовом виде при компиляции под 64-битные Windows и Linux. Под Windows также предоставляется libopenmpt для поддержки трекерной музыки.

dagon:audio

В новой версии Dagon будет встроенный звуковой движок на основе SoLoud — расширение dagon:audio. Поясню для тех, кто не знает: SoLoud — это библиотека-микшер с поддержкой 3D-звука и базовых потоковых форматов (WAV, MP3, OGG, FLAC), а также трекерных форматов через OpenMPT. Есть частотные фильтры и встроенные синтезаторы — в частности, текстово-речевой преобразователь, SFXR, эмулятор VIC-20 и генератор разных видов шума. SoLoud поддерживает множество бэкендов, и по дефолту работает на основе SDL2, как и сам Dagon (пользователь, однако, может выбрать конкретный бэкенд через опцию audio.backend в settings.conf). Короче говоря, это своего рода «FMOD от мира OpenSource». Особо отмечу удобный API и, более того, прямо из коробки — обертка для D, позволяющая писать в объектно-ориентированном стиле.

SoLoud можно было и раньше использовать в приложениях Dagon с использованием BindBC-SoLoud, но dagon:audio упрощает работу со звуком в играх еще больше, предоставляя удобный механизм привязки 3D-слушателя и пространственных звуков SoLoud к объектам движка. Для этого я ввел менеджер AudioManager и компонент SoundComponent:

class MyGame: Game
{
    AudioManager audioManager;

    this(uint windowWidth, uint windowHeight, bool fullscreen, 
        string title, string[] args)
    {
        super(windowWidth, windowHeight, fullscreen, title, args);
        audioManager = New!AudioManager(this);
        currentScene = New!TestScene(this);
    }
}

class TestScene: Scene
{
    AudioManager audio;
    SoundComponent cubeSpeaker;
    WavStream music;

    this(MyGame game)
    {
        super(game);
        this.audio = game.audioManager;
    }

    override void afterLoad()
    {
        Camera camera = addCamera();
        camera.position = Vector3f(0.0f, 1.8f, 5.0f);
        game.renderer.activeCamera = camera;

        audio.listener = camera;

        auto eCube = addEntity();
        eCube.position = Vector3f(0, 1, 0);

        cubeSpeaker = audio.addSoundTo(eCube);
        music = audio.loadMusic("data/music/song.mp3");
        cubeSpeaker.play(music);
    }

    override void onUpdate(Time t)
    {
        audio.update(t);
    }
}

Позиция и ориентация слушателя и позиция источника звука обновляются автоматически в update.

Есть также класс PlaylistPlayer для управления плейлистами. Он упрощает загрузку множества аудиофайлов из папки и переключение между ними — полезно для автоматического воспроизведения фоновых мелодий одну за другой.

PlaylistPlayer pl = audio.addPlaylistPlayer();
pl.addTracksFromDirectory("data/music");
pl.play(0);

SoundComponent и PlaylistPlayer совместимы между собой:

cubeSpeaker.playlistPlayer = pl;
cubeSpeaker.play(0);

Арена-аллокатор в Dagon

Помимо встроенных в dlib механизмов управления памятью (New/Delete, ownership), Dagon теперь поддерживает арены (dagon.core.arena) — специально для случаев, когда ownership не подходит (в основном, это операции над строками). Арена — это аллокатор, который запрашивает память у системы одним большим блоком и размещает в нем объекты последовательно, пока он не заполнится. После этого запрашивает еще один блок и т.д. В моей реализации нет возможности удалять объекты индивидуально — память арены высвобождается только при удалении самой арены. Однако ее можно сбросить без высвобождения и переиспользовать.

Arena arena = New!Arena(10 * 1024); // первый блок в 10 Кб

// Записываем строку в арену:
string s1 = arena.store("This string is now stored in arena!");

// Можно создавать массивы, экземпляры классов и структур:
int[] arr = arena.create!(int[])(100);
Foo foo = arena.create!Foo(20);
MyStruct* stru = arena.create!MyStruct();

// Операции над строками - конкатенация, разбивка, объединение:
string s2 = arena.cat("Hello, ", "world", "!");
string[] parts = arena.split("one,two,three,four", ',');
string joined = arena.join(parts, " + ");

// Форматирование строк:
string s3 = arena.format("The answer is %d", 42);

// Замена:
string r = arena.replace("the quick brown fox jumps over the lazy dog", "the", "a");

Arena — это owned-объект, она может иметь владельца и автоматически удаляться, когда удаляется владелец. Это делает ее удобным хранилищем для временных данных, которые существуют лишь в рамках какого-то алгоритма и не нужны в общем контексте приложения (например, можно хранить в арене приватные данные worker-потока). А еще ее можно использовать как быструю фрейм-локальную память, очищаемую в конце каждого шага игрового цикла.

Вычислительные шейдеры в Dagon

С переходом на OpenGL 4.3 в движке появляется возможность использовать вычислительные шейдеры. Они довольно удобно легли поверх уже существующией шейдерной системы — добавились всего два новых класса, ComputeProgram (аналог ShaderProgram) и ComputeShader, наследующий от Shader. Механизм привязки параметров целиком остается тот же самый, никаких изменений, за исключением способа привязки текстур — для этого есть метод ComputeShader.bindImageTexture. Привязываются обычные текстуры Dagon, как-то специально их готовить не нужно. Чтобы создать свой шейдер, нужно наследовать от ComputeShader, создать программу и параметры, а в bindParameters, как обычно, задать им значения:

class TestComputeShader: ComputeShader
{
   protected:
    String cs;
    ShaderParameter!Color4f _fillColor;

   public:
    Texture outputTexture;
    Color4f fillColor = Color4f(1.0f, 1.0f, 1.0f, 1.0f);

    this(Owner owner)
    {
        cs = Shader.load("data/test.comp.glsl");
        ComputeProgram p = New!ComputeProgram(cs, this);
        super(p, owner);

        _fillColor = createParameter!Color4f("fillColor");
    }

    ~this()
    {
        cs.free();
    }

    override void bindParameters(GraphicsState* state)
    {
        _fillColor = fillColor;

        if (outputTexture)
            bindImageTexture(0, outputTexture, TextureAccessMode.Write);

        super.bindParameters(state);
    }

    void run()
    {
        if (outputTexture)
            super.run(outputTexture.width, outputTexture.height);
    }
}

TestComputeShader cs = New!TestComputeShader(assetManager);
cs.outputTexture = myTexture;
cs.fillColor = Color4f(0.0f, 0.5f, 1.0f, 1.0f);
cs.run();

Пример шейдера, который заполняет текстуру «синусной плазмой» заданного цвета:

#version 430
layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in;

layout(rgba8, binding = 0) writeonly uniform image2D outputTexture;

uniform vec4 fillColor;

void main()
{
    ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
    const float scale = 0.25;
    float value = 0.5f + 
        0.25f * cos(coord.x * scale) + 
        0.25f * cos(coord.y * scale);
    vec3 color = fillColor.rgb * value;
    imageStore(outputTexture, coord, vec4(color, 1.0));
}

ComputeShader.run уже ставит барьер памяти, в приложении это делать не нужно. Есть методы dispatch и barrier для запуска вручную.

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 расскажу в ближайшее время — я решил актуализировать этот старый проект и уже сделал много интересного.

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

(далее…)

Обновления

Dagon 0.29.0

В очередной версии Dagon добавлены эффекты ретро-рендеринга — снаппинг вершин (SimpleRenderPass.retroVertexSnapping) и пикселизация (PresentRenderer.pixelization, PresentRenderer.pixelSize). SimpleRenderer теперь поддерживает свечение, туман и фоновые объекты. Исправлен баг с неправильным рендерингом упрощенной тени в SimpleRenderer.

В модуль dagon.graphics.texture добавлены функции конвертации текстурных форматов между различными API: dxgiFormatToGLFormat, vkFormatToGLFormat, glFormatToVkFormat.

Исправлены некоторые важные баги, в том числе интерполяция кадров в GLTFPose и GLTFBlendedPose, а также, благодаря переходу на новую версию dlib, очень неприятный баг с повреждением памяти в загрузчике glTF.

dlib 1.3.3

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

PN Reloaded 2.7.0

К этой версии редактора прилагается плагин PNScript, реализующий поддержку скриптов на JavaScript (на основе Node.js). Скрипты работают как текстовые фильтры, с их помощью можно реализовать форматирование, умный поиск, деобфускацию, различные алгоритмы анализа данных и т.д. Можно устанавливать NPM-пакеты, что дает практически неограниченные возможности обработки текстов. Автоматизация самого редактора (создание макросов) пока не поддерживается.

Также добавлена опция «Load ASCII files as UTF-8».

Сборку можно скачать на странице релиза.