Каждый, кто хоть раз заглядывал на Hacker News или r/ItRunsDoom, знает традицию: DOOM должен работать на всём. PDF‑файлы, SQL‑запросы, кишечные бактерии, тест на беременность — список бесконечен, и каждый новый порт абсурднее и интереснее предыдущего
Но я задался вопросом: а можно ли запустить DOOM внутри шрифта?
На вопрос «зачем?» ответить я могу тоже самое, что и авторы остальных портов: «Руководствуясь академическим интересом», «Для исследования границ, казалось бы привычных систем», ну и «Просто по приколу». Суть была в том, чтобы запустить не «рядом» со шрифтом, не «в браузере, который рендерит шрифт». А именно внутри — чтобы сам шрифтовой движок вычислял 3D‑геометрию стен
Оказалось, что можно. И вот как:

Что такое TrueType hinting и почему он неожиданно мощный?
Когда Apple проектировала формат TrueType в конце 1980-х, перед инженерами стояла конкретная задача: шрифты должны выглядеть идеально на любом экране, при любом размере. Для этого в формат встроили «hinting» - набор инструкций, которые двигают контрольные точки глифов, подгоняя их под пиксельную сетку.
Набор инструкций оказался... щедрым. Вот что есть в TrueType hinting VM:
FDEF/ENDF — определение функций
CALL — вызов функций
RS/WS — чтение/запись в storage (26+ регистров)
IF/ELSE/EIF — условия
JMPR — безусловный переход (можно сделать while)
MUL/DIV/ADD/SUB — арифметика (F26Dot6 fixed‑point)
SCFS/GC — манипуляция координатами контрольных точек
Функции, циклы, условия, регистры, арифметика. Это стек‑машина, и она Тьюринг‑полна (с оговоркой на конечную память). Кто-то должен был это доказать чем-то поинтереснее счётчика :-)
Архитектура: шрифт как GPU
Идея простая, реализация - нет.
JavaScript играет роль CPU: обрабатывает ввод с клавиатуры, хранит состояние игры (позиция игрока, угол поворота), управляет тайминагми.
Шрифт играет роль GPU: получает позицию и угол через font-variation-settings оси, запускает рейкастинг внутри hinting VM, вычисляет высоту стен для каждого столбца экрана, и возвращает результат через координаты контрольных точек глифа, которые JS читает обратно.
Коммуникация:
JS -> Шрифт: записать позицию/угол в variation axes
Шрифт: запустить hinting-программу, вычислить 3D
Шрифт -> JS: прочитать координаты глифа (это вычисленная высота стен)
P.S.
Если нажать Tab в демке, откроется debug mode - там видно, как font-variation-settings оси меняются в реальном времени при движении игрока
Компилятор: потому что писать байткод руками - это боль
Первые эксперименты я делал вручную, записывая TT-инструкции по одной. К десятой функции стало ясно, что нужен компилятор.
Собственно, получился мини-компилятор из трёх частей:
Лексер - разбивает DSL-код на токены
Парсер - строит AST
Кодогенератор - транслирует AST в последовательность TT-инструкций
DSL выглядит примерно так: вы пишете функции с арифметикой и условиями, а компилятор превращает это в FDEF/ENDF блоки с правильной стековой адресацией. 451 тест помогает не сойти с ума при рефакторинге (однако их написание - нет).

Баги, которые заставили усомниться в реальности
За время разработки я нашёл несколько багов (или «особенностей»), которые стоят отдельного упоминания.
MUL truncation: 1 × 4 = 0. TrueType использует F26Dot6 fixed-point формат: 26 бит на целую часть, 6 бит на дробную. Когда вы умножаете два числа, каждое из которых меньше 1.0 в этом формате, результат округляется до нуля. Это не баг - это особенность. Но когда рейкастер вдруг перестаёт рисовать стены, а вы два дня ищете ошибку в логике... Короче, неприятно.
SVTCA[0] = Y, а не X. Инструкция SVTCA задаёт вектор проекции. Я предполагал, что [0] - это X-ось (как в любом здравом API). Нет. Спецификация индексирует с Y=0, X=1. Два часа жизни.
«return» не возвращает из while. В TT bytecode нет настоящего return. Точнее, есть, но он выходит из FDEF, а не из «цикла», который на самом деле реализован через JMPR. Пришлось изобретать флаговые переменные для имитации break/return.
Идейный вдохновитель
llama.ttf (июнь 2024) - LLM внутри шрифта через HarfBuzz WebAssembly shaper. Wasm - это по определению универсальный рантайм, так что «шрифт» тут скорее контейнер. Увидев этот проект, стало интересно разобраться в том, как вообще блин можно запустить модель в шрифте, но, как оказалось, всё куда интереснее, так что спасибо авторам за их проект, он действительно очень необычный и интересный.
Что это значит для безопасности?
Это не просто забавный факт. Каждый браузер, который рендерит TrueType-шрифты с включённым хинтингом, выполняет произвольный код внутри шрифтовой VM. В 2025 году были опубликованы исследования по side-channel атакам через hinting (микроархитектурные тайминги), но вычислительная мощность самой VM пока мало изучена.
Теоретически, вредоносный шрифт мог бы использовать hinting‑программу для чего угодно — от fingerprinting до утечки данных через тайминги рендеринга. TTF‑DOOM — это proof‑of‑concept того, насколько мощен этот «безобидный» механизм
Попробуйте сами
Демо (Chrome/Edge, нужна поддержка variable fonts)
Нажмите Tab для debug mode - увидите, как font-variation-settings оси обновляются в реальном времени
Если у вас есть идеи, что ещё можно запустить внутри TrueType hinting VM или хотите обсудить угрозы для безопасности - вперёд! Я весь внимание :-)
Комментарии (6)

MasterMentor
08.04.2026 14:26Хотелось бы статью про TrueType hinting VM.

Priiiiiinter Автор
08.04.2026 14:26спасибо за обратную связь, подумаю, может напишу что интересное :)

DandyDan
08.04.2026 14:26Для доказательства полноты по Тьюрингу обычно реализуют интерпретатор Brainfuck или какой-то другой системы, для которой доказана полнота.
maxcat
Очень круто!
А с ClearType такое прокатит? Или ClearType имеет фиксированный конвейер подстановки шрифта под экран (то есть там нельзя свод логику прикрутить)
Priiiiiinter Автор
Вообще да, ClearType, насколько мне известно, это технология субпиксельного рендеринга, которая работает после хинтинга, то есть сначала выполняется хинтинг, потом растеризатор строит контуры, и вот после уже ClearType применяет субпиксельное сглаживание к результату
Логика рейкастера отработает точно так же, просто финальная картинка будет чуть более сглаженной за счёт особенностей рендеринга
Единственный нюанс: ClearType может немного сдвигать точки по горизонтали на субпиксельном уровне, что может немного размазать стены, но на вычислительную логику это не влияет