Статус dlib2

В последнее время редко пишу о dlib, но хочу заверить: разработка библиотеки на месте не стоит, в данный момент я разрабатываю dlib2, а точнее — ее низкоуровневый компонент dcore. dcore — это процедурный API, совместимый с betterC и минимально заменяющий стандартную библиотеку. Он полностью автономный, не зависит от Phobos, поддерживает компиляцию в машинный код под bare metal (то есть, без операционной системы) и в WebAssembly. Он предназначен для создания компактных приложений в стиле C, а также для написания высокоуровневых частей библиотеки на его основе.

Может возникнуть вопрос — для чего это нужно, если есть libc? dcore, конечно, использует стандартные функции C при компиляции в таргеты, которые поддерживают libc. Но все упирается в WebAssembly: своего Emscripten на D не существует. Поэтому, для упрощения написания WASM-модулей, я решил сделать минимальный набор абстракций, которые скрывают платформозависимые реализации самых важных компонентов приложения под универсальным низкоуровневым API. dcore элементарно позволяет использовать printf, malloc и т.д., не задумываясь, доступны ли они на целевой платформе.

Еще один важный момент — математика. Я уже публиковал пост о бенчмарках различных реализаций синуса и косинуса — dcore поддерживает все стандартные математические функции (sin, cos, tan, asin, acos, sqrt, cbrt и др.), предоставляя на каждой платформе оптимальные реализации: LLVM-интринсики, функции C, функции Phobos и кастомные, без зависимостей, предназначенные для нестандартных таргетов.

Из нового:

  • dcore.time — удобный враппер над C-шными localtime и gmtime. Позволяет получать текущую системную время-дату, в текущем часовом поясе или по Гринвичу;
  • dcore.sys — информация о системе: архитектура CPU, количество ядер, объем памяти, название и версия ОС. Модуль поддерживает x86/x64, ARM, ARM64, IA64, MIPS, SPARC, PPC. Информация об ОС поддерживается для всех версий Windows и двух больших семейств Unix — Linux/Solaris/AIX (через sysconf) и BSD/OSX (через sysctl);
  • dcore.process — позволяет кроссплатформенно получить PID.

Мини-проект: радиоуправляемая платформа на Arduino

Пост не имеет отношения к D, но я решил опубликовать его тут, так как тематика относительно близкая.

Arduino я начал изучать год назад. Для непосвященных: это созданный итальянскими инженерами недорогой 8-битный микроконтроллер с распиновкой, очень простой для понимания начинающими, в комплекте с работающим из коробки C-тулчейном, юзер-френдли IDE и инструментарием для прошивки платы по COM-порту. Вообще электрическими схемами я увлекаюсь с самого раннего детства — всегда любил что-то делать своими руками из железного конструктора, проводов и лампочек =) С появлением компьютера это хобби не угасло, хотя до недавних пор и отошло на второй план. Мечту сделать игрушечный радиоуправляемый аппарат я не оставлял много лет — и вот, наконец, сумел воплотить ее в жизнь!

За основу я взял китайский клон Arduino Uno R3 (благо они очень дешевые, можно не жалеть для экспериментов). Я использовал, в основном, прилагавшиеся к плате детали, но пришлось также докупить электромоторы и драйвер L298N. Что удобно, моторы можно купить сразу с колесом — таким образом, остается только укрепить ходовую часть на шасси.

Шасси, за неимением лучшего материала, я сделал из Лего. На этом этапе, как ни странно, я застрял надолго, так как электронные компоненты оказалось очень непросто надежно прикрепить к лего-деталям. Для удобства я докупил лего-совместимый корпус для Arduino, однако толкового способа закрепить моторы я так, к сожалению, и не придумал — в итоге ходовая часть получилась немного расхлябанная, но тем не менее работоспособная.

Отдельной проблемой стало питание. Сначала я думал запитать плату от USB-пауэрбанка, но ничего не вышло: Ардуино работает пару секунд и выключается. Пришлось добавить батарейный отсек на 6 элементов AA (9В). Драйвер моторов запитал параллельно с платой, от разъема VIN (который, в отличие от обычных 5-вольтных разъемов питания, напрямую отдает 9В от адаптера, что очень удобно). Теоретически, для большей надежности можно было бы запитать драйвер полностью независимо от Ардуино, добавив клемму между DC-штекером и платой, но я не стал так заморачиваться.

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

#include "IRremote.h"

#define IR_RECEIVE_PIN 7

// IN1, IN2 - motor A
// IN3, IN4 - motor B
#define IN1 2
#define IN2 3
#define IN3 4
#define IN4 5

#define ENA A0
#define ENB A1

int receivedValue;
int buttonPressed = 0;

void setup()
{
    pinMode(IN1, OUTPUT);  //motor A
    pinMode(IN2, OUTPUT);  //motor A
    pinMode(IN3, OUTPUT);  //motor B
    pinMode(IN4, OUTPUT);  //motor B
  
    IrReceiver.begin(IR_RECEIVE_PIN, ENABLE_LED_FEEDBACK);
  
    buttonPressed = 0;
}

void loop()
{
    receivedValue = 0;
    
    if (IrReceiver.decode())
    {
        receivedValue = IrReceiver.decodedIRData.command;
        IrReceiver.resume();
    }
  
    if (receivedValue == 28) // forward
    {
        digitalWrite(IN1, HIGH);
        digitalWrite(IN2, LOW);
        digitalWrite(IN3, LOW);
        digitalWrite(IN4, HIGH);
    }
    else if (receivedValue == 82) // back
    {
        digitalWrite(IN1, LOW);
        digitalWrite(IN2, HIGH);
        digitalWrite(IN3, HIGH);
        digitalWrite(IN4, LOW);
    }
    else if (receivedValue == 8) // left
    {
        digitalWrite(IN1, HIGH);
        digitalWrite(IN2, LOW);
        digitalWrite(IN3, HIGH);
        digitalWrite(IN4, LOW);
    }
    else if (receivedValue == 90) // right
    {
        digitalWrite(IN1, LOW);
        digitalWrite(IN2, HIGH);
        digitalWrite(IN3, LOW);
        digitalWrite(IN4, HIGH);
    }
    else // stop
    {
        digitalWrite(IN1, LOW);
        digitalWrite(IN2, LOW);
        digitalWrite(IN3, LOW);
        digitalWrite(IN4, LOW);
    }
    
    delay(100);
}

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

Dagon 0.19.0

Вышла новая версия движка! Релиз очень большой, все изменения детально описывать в этом посте не буду, перечислю только самое основное. Подробности — на странице релиза.

  • Добавлен модуль dagon.core.persistent — простая БД типа «ключ-значение», которую можно использовать для управления пользовательскими данными игры, такими как настройки и сохранения;
  • InputManager теперь поддерживает разделенные пробелами имена кодов клавиш в привязках. Вместо пробела используется символ плюса, например, kb_left+ctrl для левого Ctrl;
  • Dagon-приложение теперь устанавливает кодировку консоли на UTF-8 в Windows, чтобы нормально выводился нелатинский текст;
  • Добавлен SimpleRenderer, о котором я уже писал ранее — облегченный рендерер для казуальной и стилизованной графики;
  • Добавлена поддержка бокс-проекции для световых зондов. Ее можно включить с помощью свойства Entity.probeUseBoxProjection. Зонды теперь используют альфа-спад для плавного смешивания с данными в G-буфере — благодаря этой фиче сглаживается граница между интерьерным и уличным освещением. Для управления этим эффектом введено свойство Entity.probeFalloffMargin;
  • Добавлена поддержка ортогональных проекций: свойство RenderView.projection теперь принимает константы Perspective, Ortho и OrthoScreen. RenderView.orthoScale управляет масштабом проекции в режиме Ortho;
  • Добавлен новый примитив — цилиндр ShapeCylinder, а также Billboard (прямоугольник, который всегда направлен в сторону камеры);
  • Материалы теперь поддерживают произвольные преобразования текстур (аффинные матрицы 3×3) благодаря свойству Material.textureTransformation. Метод Material.setSprite(Vector2f uvSize, Vector2f uvPosition) можно использовать для наложения на квады фрагментов текстуры — полезно для рендеринга спрайтов и покадровой анимации;
  • Добавлен новый пакет dagon.collision — базовая система обнаружения столкновений;
  • В контроллере персонажей NewtonCharacterComponent появилась поддержка приседания (метод crouch) и улучшенная проверка земли.

Обновления

Улучшения в Dagon

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

dmech, мой старый физический движок, включает продвинутые алгоритмы проверки столкновений, которые до недавнего времени пропадали зря (в частности, Minkowski Portal Refinement и солвер Джонсона) — я решил исправить этот недочет и добавить их в Dagon в качестве пакета dagon.collision. В данный момент он поддерживает обнаружение пересечений между любыми выпуклыми телами и проверку пересечения выпуклого тела с лучом.

Продолжаю работу над упрощенным рендером, о котором уже писал ранее. Появились билборды и универсальная ортографическая проекция: теперь можно, к примеру, легко сделать изометрическое 3D со спрайтовой графикой, как на скриншоте. Среди прочих улучшений — рефакторинг компонента FreeviewComponent и улучшенный контроллер персонажа Newton (появилась поддержка приседания). Оптимизирован deferred-рендер, сокращено количество переключений кадровых буферов. Добавлен новый встроенный примитив — цилиндр.

BindBC-GLSLang

Написал биндинг к glslang, референсному компилятору GLSL от Khronos — можно генерировать модули SPIR-V для WebGPU-приложений прямо в D, без использования внешнего ПО.

https://github.com/gecko0307/bindbc-glslang

Раздел со статьями

Добавил раздел «Статьи» для быстрого доступа к ним. Там, в основном, написанное за последние 10 лет и доступное онлайн, так как более ранние мои материалы публиковались еще в PDF-версии журнала «FPS» и бумажных изданиях — ссылки на них поставить затруднительно.

std140: статическая валидация структур

std140 — самый платформонезависимый лейаут uniform-блоков. Если вы передаете параметры в шейдер структурами, а не по одному, то лучше использовать именно его. Конечно, у переносимости есть своя цена: стандарт предусматривает несколько ограничений, самое известное из которых — выравнивание по 16 байтам. То есть, все поля структуры должны быть по размеру кратны 16 байтам. Например, если это вектор — то vec4, если матрица — то mat4. Некратные 16 байтам типы (float, int и др.) также можно использовать, но их необходимо выравнивать вручную — то есть, одиночный float пойдет все равно как vec4. Использование невыровненных данных приводит к тому, что в шейдере считываются неправильные значения, и это вызывает недоумение у начинающих.

Благодаря CTFE и статической интроспекции в D можно проверять структуры на соответствие этому правилу на этапе компиляции — все необходимое есть в std.traits:

import std.traits;

bool fieldsAligned(T, alias numBytes)()
{
    static if (is(T == struct))
    {
        alias FieldTypes = Fields!T;
        
        static foreach(FT; FieldTypes)
        {
            static if (FT.sizeof % numBytes != 0)
                return false;
        }
        
        return true;
    }
    else return false;
}

alias Std140Compliant(T) = fieldsAligned!(T, 16);

Теперь можно делать так:

import dlib.math.vector;
import dlib.math.matrix;

struct Uniforms
{
    Matrix4x4f modelViewMatrix;
    Matrix4x4f normalMatrix;
    Vector4f worldPosition;
    float someParameter; // Will not compile, use Vector4f instead
}

static assert(Std140Compliant!Uniforms,
    Uniforms.stringof ~ " does not conform to std140 layout");

Возникает вопрос, почему бы просто не делать align(16)? Можно!

struct AlignedUniforms
{
  align(16):
    float a;
    Vector3f b;
    Matrix4x4f c;
}

Преимуществом такого подхода является право использовать любые типы для полей, но это неэффктивно: например, вместо того, чтобы впустую расходовать три байта после a, можно было бы упаковать a вместе с b в один 16-байтный вектор. Поэтому я лично не фанат автоматического компиляторного выравнивания — лучше это делать вручную, защитившись от ошибок необходимыми статическими проверками.

Обновления

bindbc-newton 0.3.2

Небольшой исправляющий релиз, который фиксит список путей к Newton под Linux.

https://github.com/gecko0307/bindbc-newton/releases/tag/v0.3.2

dlib2 компилируется под WASM

Поддержка Web Assembly в LDC существует достаточно давно, и я когда-то уже делал экспериментальный мини-проект — приложение с треугольником, рисующимся через OpenGL ES, которое можно скомпилировать как под десктоп, так и в WASM-модуль. Для этой работы пришлось написать минимальную замену стандартной библиотеке, поскольку поддержка WASM в Phobos чуть более, чем никакая. Надеюсь изменить эту ситуацию в dlib2 — dcore уже сейчас можно собрать под WASM!

Например, вот такой «Hello, World» на десктопе печатает в стандартный вывод, а в браузере — в консоль, причем с поддержкой UTF-8:

module main;

import dcore.stdio;

extern(C):

void main()
{
    // Minimal cross-platform betterC application.
    // Will print to stdout on desktop and to the console in browser.
    
    printf("Hello from D! Привет из D!n");
}

В ближайшее время напишу пост по статусу dlib2 и всем деталям планируемых фич.

Багфиксы и улучшения в MiniGL

Актуализировал и чуть доработал MiniGL, мой программный растеризатор. Исправил альфа-смешивание, а также теперь можно отключить запись и чтение z-буфера для отрисовки экранных спрайтов. Обновил также демонстрационное приложение — теперь это почти готовый движок для коридорных ретро-шутеров: есть проверка столкновений со стенами, стрельба магией и спрайт оружия.

Упрощенный рендеринг

По мере усложнения стандартного рендер-движка Dagon, повышаются и системные требования — в данный момент он требует довольно мощную видеокарту геймерского класса (желательно NVIDIA, желательно не ниже Turing). Но, поскольку далеко не все игры обязаны иметь топовую графику, неплохо предусмотреть в движке некий облегченный режим, оптимальный для казуальных жанров и стилизации под ретро, где не нужен сложный пайплайн с реалистичным освещением и PBR. В Dagon рендер уже давно структурно вынесен в отдельную систему, которую можно модифицировать и даже полностью заменять, не трогая остальной код движка — модель данных сцены и менеджер ресурсов в Dagon полностью независимы от рендера. Это позволило без особых сложностей добавить упрощенный рендер-движок SimpleRenderer, который вы можете создать в вашем классе игры (на базе Game или Application) и заменить им стандартный DeferredRenderer.

SimpleRenderer полностью переопределяет рендеринг объектов. Здесь по умолчанию нет физически обоснованных источников света, карт окружения и т.д. — за освещение отвечает простейшая модель Блинна-Фонга, которая в данный момент работает с одним глобальным направленным источником света (environment.sun текущей сцены). Нет normal mapping’а и прочих эффектов материала — учитывается только baseColorFactor/baseColorTexture. Но зато вы можете назначить вашим материалам любой шейдер с любыми эффектами — в DeferredRenderer такой возможности нет (все объекты с пользовательскими шейдерами трактуются как forward и рендерятся отдельно, после всех deferred-проходов). Также в этой системе поддерживаются слои, что позволяет явным образом задавать порядок рендеринга группам объектов — например, можно занести все прозрачные объекты в отдельный слой, который рисуется поверх дефолтного.

SimpleRenderer отлично подойдет для создания игр для low-end железа, он будет работать даже на самых слабых системах.

Бокс-проекция

Добавил в Dagon поддержку бокс-проекции для световых зондов окружения (EnvironmentProbe). Техника старая, но никем не отмененная — а главное, хорошо сочетается с deferred-рендером!

Стандартный environment mapping предполагает, что стенки виртуальной среды, с которой считывается освещение, бесконечно удалены от объектов сцены. Это допущение работает для открытого пространства, но не годится для интерьера. Бокс-проекция корректирует сэмплинг из карты окружения так, что результат выровнен по сторонам бокса заданного размера, благодаря чему минимумом ресурсов достигается сносного качества непрямое освещение в интерьере (если, конечно, карта окружения 1 в 1 совпадает с моделью комнаты). Это эффективный способ аппроксимировать локальный GI в ограниченном пространстве: окружение интерьера, с которого рендерилась карта, статичное, но любые другие объекты могут быть динамическими. При трансформации камеры или объекта внутри комнаты, отражения соответствующим образом меняются, причем как зеркальные, так и диффузные.

Получается даже имитировать объемные источники света! На скриншотах ниже нет ничего, кроме параллельного источника света для солнца и двух статичных карт окружения — для улицы и для интерьера. Отражения окон и светящегося блока на полу получаются автоматически:

Главным минусом техники является то, что создать под нее правильную карту не очень просто — нужно учитывать различия в координатных системах.

Сжатие текстур, часть IV. Мобильные форматы: ETC и PVRTC

Продолжение серии постов о сжатых текстурных форматах. Предыдущие части: часть I, часть II, часть III.

Ситуация со сжатием текстур на мобильных платформах довольно запутанная, так как 3D-ускорители там существенно отличаются от десктопных. К сожалению, ни iOS, ни Android не поддерживают S3TC (и, тем более, BPTC). В мобильных системах используются свои специализированные форматы сжатия — ETC на Android и PVRTC на iOS.

ETC1 и ETC2

Ericsson Texture Compression / iPACKMAN

Формат сжатия от Ericsson. Поддерживается как в мобильных устройствах, так и в современных браузерах (кроме Firefox).

ETC1 поддерживается практически на всех android-устройствах и является стандартным форматом сжатия в OpenGL ES 2.0. Не поддерживает прозрачность. Блок 4×4 преобразуется в 64-битное представление. Блок разбивается на два субблока (4×2 или 2×4), им присваиваются базовые цвета — либо каждому RGB 4:4:4, либо одному 5:5:5, а второму смещение 3:3:3 относительно первого. Пиксели в субблоке представляются в виде суммы базового цвета и одного из четырех смещений — так называемых модификаторов: pixelColor = baseColor + RGB(modifier, modifier, modifier). Модификаторы, определяемые спецификацией, представляют собой 4×8 таблицу констант — целочисленных значений со знаком. Индексы ряда в таблице [0, 7] хранятся как два 3-битных значения (по одному на субблок), индексы столбца [0, 3] — как 16 2-битных значений (по одному на каждый пиксель). Оставшиеся два бита определяют ориентацию субблоков (flip-бит) и тип хранения базовых цветов (diff-бит). Результат суммирования нормализуется в 8 бит на канал.

ETC2 является стандартным форматом сжатия в OpenGL ES 3.0. Поддерживает прозрачность. ETC2 — это обратно-совместимое надмножество ETC1. Альфа-канал кодируется по такому же принципу, что и цвет: значение прозрачности для пикселя — это сумма базовой альфы и модификатора из таблицы констант 8×16. Блоку 4×4 присваиваются дополнительные 64 бита: 8-битное базовое значение альфа, 4-битный индекс ряда в таблице модификаторов, 4-битный множитель и 16 3-битных индексов столбцов.

Также в ETC2 есть отдельный режим punch-through (GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2), аналогичный ETC1, но в котором diff-бит заменяется битом прозрачности — таким образом, блок интерпретируется либо как полностью прозрачный, либо как полностью непрозрачный.

PVRTC1 и PVRTC2

PowerVR Texture Compression

Используется в видеоускорителях PowerVR (iPhone и другие устройства Apple). Основной целью разработчики формата поставили устранение разрывов цвета вдоль границ блоков, которая присуща S3TC. PVRTC поддерживает альфа-канал.

Имеет две разновидности — 4bpp и 2bpp. На блок всегда выделяется 64 бита информации, поэтому в режиме 4bpp блок имеет размер 4×4 пикселя, в режиме 2bpp — 8×4. В некоторых аппаратных реализациях блоки расположены в памяти не в порядке сканирования (снизу вверх, слева направо), а в Z-последовательности для увеличения пространственной локальности и, как следствие, более эффективного кэширования.

В каждом блоке хранится шесть переменных: для PVRTC1 — данные модуляции (32 бит), флаг punch-through alpha (1 бит), цвет A (15 бит), флаг прозрачности цвета A (1 бит), цвет B (14 бит) и флаг прозрачности цвета B (1 бит). Для PVRTC2 — данные модуляции (32 бит), флаг модуляции (1 бит), цвет B (15 бит), флаг hard transition (1 бит), цвет A (15 бит) и флаг прозрачности (1 бит). Битовая глубина значений A и B задается по-разному в зависимости от того, есть ли альфа-канал: либо RGB 5:5:4(5), либо RGBA 3:4:4:3(4), в скобках указан вариант для 15-битного цвета. Дополнительный бит прозрачности определяет наличие у цвета альфа-канала. В PVRTC его можно задавать независимо для A и B, когда как в PVRTC2 бит прозрачности только один, и оба цвета должны быть в одинаковом формате — либо RGB, либо RGBA. Цвет пикселя вычисляется билинейной интерполяцией цветов A и B. Каждому пикселю блока сопоставляется, в зависимости от разновидности формата, 2-битное или 1-битное значение модуляции, кодирующее вес интерполяции между A и B.

PVRTC2 расширяет алгоритм поддержкой четырех разных режимов блока, задаваемых флагами hard transition в сочетании с флагом модуляции: стандартная билинейная, punch-through alpha, резкий переход, локальная палитра.

Тригонометрия в бенчмарках

«Неважно, что ты любишь больше:
косинус ли, синус ли…»

Тригонометрия — основа многих приложений, от компьютерной графики до научных симуляций. Все мы привыкли вызывать sin и cos, не задумываясь, как они реализованы. А реализации могут быть разные! Работая над математической библиотекой для dlib2, я провел интересное исследование — какая тригонометрия лучше? Конечно, есть функции из std.math, и в большинстве случаев подойдут именно они. Но не все так просто — все зависит от того, что именно вы разрабатываете.

Если вы собираете обычное приложение, то кажется, что беспокоиться не о чем. Но если вам, по тем или иным причинам, нельзя обращаться к Phobos? Тогда есть два основных пути — sin и cos из стандартной библиотеки C, либо кастомная реализация, если код собирается под голое железо (например, при создании ядра ОС или программировании встраиваемой электроники). Но если вы используете LDC, то ничто не мешает использовать интринсики LLVM — они, оказывается, работают быстрее, чем std.math!

Я провел ряд тестов для всех вариантов тригонометрии:

  • Тест на точность — вычисление синуса и косинуса для 200 аргументов от -π до +π. Замерялась максимальная погрешность — расхождение результата с std.math.sin и std.math.cos;
  • Тест на производительность — время вычисления синуса и косинуса 1000000 раз.

Во всех кейсах я использовал LDC 1.39.0 под Windows 10. Получилось следующее:

  • std.math.sin, std.math.cos:
    • Время выполнения: 4 мс
  • LLVM интринсики llvm_sin, llvm_cos:
    • Время выполнения: 2 мс
    • Точность: абсолютная (макс. погрешность для sin: 0, для cos: 0)
  • Функции sin, cos из стандартной библиотеки C:
    • Время выполнения: 21 мс
    • Точность: абсолютная (макс. погрешность для sin: 0, для cos: 0)
  • Моя кастомная реализация на таблицах:
    • Время выполнения: 33 мс
    • Точность: порядка 10-7 (макс. погрешность для sin: 2.97038e-07, для cos: 1.78188e-07)

Также я пробовал версию с ассемблерными вставками, но она получилась почему-то медленнее кастомной — видимо, при использовании инлайнового ассемблера компилятор не задействует какие-то оптимизации (а еще есть мнение, что x87 fsin, fcos на современных процессорах медленные сами по себе). Смысла в таком варианте реализации особо нет, так что я его не стал рассматривать для включения в библиотеку.

В итоге в dlib2 войдут четыре реализации с таким приоритетом:

  • Если используется LDC, то синус и косинус — это интринсики (то есть, кодогенератор сам выбирает оптимальную реализацию под нужную архитектуру);
  • Если используются другие компиляторы (DMD, GDC):
    • Если код компилируется с поддержкой Phobos, то используются функции из std.math;
    • Если код собирается в режиме version(NoPhobos), но не version(FreeStanding) (то есть, под Windows или Unix-подобную ОС), то используются функции рантайма C;
    • Если же идет компиляция в bare metal, то используется кастомная реализация на таблицах.