Спайк A8: harfrust против системного шейпинга
Сессия A, 2026-10-04. Задача A8 брифа
docs/sessions/A-kernel.md(«harfrust против системного шейпинга»). Стенд —kernel/spikes/shaping-bench(в продукт не входит): Rust-драйвер на harfrust 0.13.3 / read-fonts 0.43.3 (те же версии, что уmeridian-layout), помощник на Swift для CoreText (coretext/ctshape.swift), эталон —hb-shapeиз HarfBuzz 14.3.1. Сырые данные —results-2026-10-04-m5max.{md,jsonl}, разбор расхождений —results-2026-10-04-m5max-diffs.txt. Решение оформлено как ADR в разделе 7.
1. Вопрос
Ядро шейпит текст документов через harfrust одинаково на всех ОС
(meridian-layout/src/shape.rs, детерминизм —
docs/algorithms/kernel-core.md §19). Нужно ли вместо него
или в дополнение звать системный шейпер (CoreText на macOS, DirectWrite
на Windows, HarfBuzz/Pango на Linux)? Конкретно:
- совпадают ли глифы и их положения у harfrust и CoreText на одних и тех же файлах шрифтов и текстах;
- насколько они различаются по скорости;
- где и почему расходятся, и есть ли случаи, когда без системного шейпинга не обойтись.
2. Методика
Три пути шейпинга на одних и тех же байтах шрифта и абзацах:
| Обозначение | Что это | Как вызывается |
|---|---|---|
| harfrust | Rust-порт HarfBuzz, шейпер ядра | ровно как shape_segment:
guess_segment_properties, явное направление и язык, в
режиме word выключены kern, liga,
clig (WORD_DEFAULT_OFF) |
| CoreText | системный шейпер macOS | ctshape.swift:
CTFontManagerCreateFontDescriptorsFromURL (лицо выбирается
по PostScript-имени из таблицы name),
CTFontCreateWithFontDescriptor на 12 pt, абзац — одна
CTLine без переноса, из каждого CTRun — глифы,
string indices, продвижения и положения |
| HarfBuzz C | эталон, от которого портирован harfrust; это же — системный шейпинг Linux (Pango) | hb-shape --shapers=ot внешней программой |
| HB→CT | обёртка HarfBuzz над CoreText | hb-shape --shapers=coretext — перекрёстная проверка
Swift-помощника |
Режимы свойств. word — так шейпит ядро
(Word по умолчанию не включает кернинг и стандартные/контекстные
лигатуры): у harfrust и HarfBuzz свойства kern,
liga, clig = 0; у CoreText — те же теги в
kCTFontFeatureSettingsAttribute плюс
kCTKernAttributeName = 0.0 («не кернить вовсе») и
kCTLigatureAttributeName = 0 («только обязательные
лигатуры»). default — настройки шейпера по умолчанию. Для
шрифтов AAT без GSUB проверен ещё режим word-aat (выключены
только kern и liga, см. раздел 5.2).
Метрики (все — в единицах шрифта; CoreText отдаёт
пункты, они переводятся × upem / 12):
- глифы = — доля абзацев, где последовательность ID глифов (в визуальном порядке) идентична;
- кластеры = — среди них доля абзацев с одинаковыми кластерами (индексы UTF-16; у harfrust байты UTF-8 переведены);
- сдвиги — число глифов, чьё положение
(x, y)от начала строки расходится больше чем на 0,5 единицы;Σ|Δx|— сумма расхождений положений по x, в единицах и в % ширины строк. Это и есть «суммарное расхождение продвижений»: положение глифа — накопленная сумма продвижений плюс смещение; - |ΔW| — расхождение ширины строки (у HarfBuzz — сумма
продвижений, у CoreText —
CTLineGetTypographicBounds), среднее по корпусу и максимум по абзацу, %; - Σ|Δadv| — сумма расхождений самих продвижений отдельных глифов (справочно, см. 5.1).
Корпус
(kernel/spikes/shaping-bench/corpus/, по абзацу на строку,
тексты написаны для спайка): ru — 14 абзацев (ё, ъ,
кавычки-ёлочки, тире, цифры), en — 13, ligkern
— 10 строк на лигатуры и кернинг (fi, ffl, AV, To, Ty, Wa, «Г»-образный,
Уфа…), he — 12 (из них 3 с огласовками), ar —
12 (2 с харакатами, лам-алиф), hi — 11 (конъюнкты क्ष, ज्ञ,
श्र, द्व, त्र्य…), zh — 6. Всего 236 абзацев × режим, ≈ 20,6
тыс. глифов. Все символы корпуса есть в cmap каждого шрифта
(стенд проверяет покрытие; подстановок шрифта в CoreText нет).
Шрифты: комплект ядра kernel/fonts/ —
Liberation Serif, Liberation Sans, Carlito (ru/en/ligkern, иврит у
Liberation); системные macOS — Arial
(Supplemental/Arial.ttf: латиница, кириллица, иврит,
арабский; OpenType), Geeza Pro (GeezaPro.ttc, лицо 0:
только AAT — morx, kern,
feat, без GSUB/GPOS), Kohinoor Devanagari
(Kohinoor.ttc, OpenType), Devanagari MT
(Supplemental/DevanagariMT.ttc, только AAT
— morx, feat), Hiragino Sans GB
(Hiragino Sans GB.ttc, OpenType). PingFang в этой версии
macOS нет в /System/Library/Fonts.
Скорость. Один проход = шейпинг всех абзацев набора.
Сначала отдельно меряется холодный проход, затем 3 прогрева и 50
замеренных проходов; в таблицах — медиана, p10–p90, мкс на абзац и млн
глифов в секунду. harfrust меряется в двух вариантах: «как в
ядре» — шейпер (ShaperData::shaper().build()),
набор свойств и план шейпинга строятся на каждый вызов, результат
собирается в новый Vec с переводом в EMU (так сейчас делает
shape_segment); «кэш плана» — шейпер один
на шрифт, ShapePlan один на (письменность, направление,
язык, свойства), буфер переиспользуется. CoreText
(swiftc -O): в каждом проходе —
CFAttributedString → CTLine → копирование
глифов, индексов, продвижений и положений всех прогонов в заранее
выделенные массивы (строки CFString построены заранее).
HarfBuzz C — через hb-shape --num-iterations:
(t(3001) − t(1)) / 3000, лучший из трёх запусков; это
оценка, в ней есть накладные расходы цикла hb-shape на
буфер.
3. Условия
| Машина | Apple M5 Max, 18 ядер, 128 ГБ |
| ОС | macOS 27.0 (26A428) |
| Rust | rustc 1.97.1 (8bab26f4f 2026-07-14), release:
opt-level=3, thin LTO, codegen-units=1 |
| Swift | Apple Swift 6.4 (swiftlang-6.4.0.34.1), swiftc -O,
CoreText системный |
| Шейперы | harfrust 0.13.3, read-fonts 0.43.3, HarfBuzz 14.3.1
(hb-shape из Homebrew, шейперы ot,
coretext) |
| Кегль | 12 pt; абзац — одна строка без переноса |
4. Замеры
4.1 Совпадение harfrust и CoreText
Сводно (236 абзацев на режим):
| Пара | Режим | Глифы = | Глифов | Сдвигов > 0,5 ед. | Σ|Δx| |
|---|---|---|---|---|---|
| harfrust ↔︎ CoreText | word (как ядро) | 225/236 (95,3 %); без Devanagari MT — 225/225 (100 %) | 20 695 | 0 | 0 |
| harfrust ↔︎ CoreText | default | 236/236 (100 %) | 20 540 | 1 | 670 ед. (0,004 % ширины корпуса) |
| harfrust ↔︎ CoreText | word-aat (только AAT-шрифты) | 23/23 (100 %) | 1 746 | 0 | 0 |
| harfrust ↔︎ HarfBuzz C | все режимы | 100 % | — | 0 | 0 |
| HB→CT ↔︎ CoreText (Swift) | все режимы | 100 % | — | 0 / 1 / 0 | тот же глиф, что выше |
По наборам (A = harfrust, B = CoreText; «HB C» — сравнение harfrust с
HarfBuzz C; «ΔW от kern/liga» — насколько режим default у
harfrust сужает строки относительно word, то есть что дают
кернинг и лигатуры):
| Корпус | Шрифт | Режим | Абз. | Глифов | Глифы = CT | Кластеры = CT | Сдвигов | Σ|Δx|, ед. | |ΔW| ср./макс., % | Σ|Δadv|, ед. | HB C: глифы = / сдвигов | ΔW от kern/liga, % |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ru | Liberation Serif | word / default | 14 | 1 434 | 100 % / 100 % | 14/14 | 0 / 0 | 0 | 0 / 0 | 0 | 14/14 / 0 | −0,44 |
| ru | Liberation Sans | word / default | 14 | 1 434 | 100 % / 100 % | 14/14 | 0 / 0 | 0 | 0 / 0 | 0 | 14/14 / 0 | −0,42 |
| ru | Carlito | word / default | 14 | 1 434 | 100 % / 100 % | 14/14 | 0 / 0 | 0 | 0 / 0 | 0 | 14/14 / 0 | −0,36 |
| ru | Arial | word / default | 14 | 1 434 | 100 % / 100 % | 14/14 | 0 / 0 | 0 | 0 / 0 | 0 | 14/14 / 0 | −0,42 |
| en | Liberation Serif | word / default | 13 | 1 418 | 100 % / 100 % | 13/13 | 0 / 0 | 0 | 0 / 0 | 0 | 13/13 / 0 | −0,09 |
| en | Liberation Sans | word / default | 13 | 1 418 | 100 % / 100 % | 13/13 | 0 / 0 | 0 | 0 / 0 | 0 | 13/13 / 0 | −0,09 |
| en | Carlito | word / default | 13 | 1 418 / 1 394 | 100 % / 100 % | 13/13 | 0 / 0 | 0 | 0 / 0 | 0 | 13/13 / 0 | −0,51 |
| en | Arial | word / default | 13 | 1 418 | 100 % / 100 % | 13/13 | 0 / 0 | 0 | 0 / 0 | 0 | 13/13 / 0 | −0,09 |
| ligkern | Liberation Serif | word / default | 10 | 756 | 100 % / 100 % | 10/10 | 0 / 0 | 0 | 0 / 0 | 0 | 10/10 / 0 | −2,83 |
| ligkern | Liberation Sans | word / default | 10 | 756 | 100 % / 100 % | 10/10 | 0 / 0 | 0 | 0 / 0 | 0 | 10/10 / 0 | −2,14 |
| ligkern | Carlito | word / default | 10 | 756 / 687 | 100 % / 100 % | 10/10 | 0 / 0 | 0 | 0 / 0 | 0 | 10/10 / 0 | −2,49 |
| ligkern | Arial | word / default | 10 | 756 | 100 % / 100 % | 10/10 | 0 / 0 | 0 | 0 / 0 | 0 | 10/10 / 0 | −2,14 |
| he | Liberation Serif | word / default | 12 | 829 | 100 % / 100 % | 9/12 | 0 / 0 | 0 | 0 / 0 | 40 040 | 12/12 / 0 | 0,00 |
| he | Liberation Sans | word / default | 12 | 829 | 100 % / 100 % | 9/12 | 0 / 0 | 0 | 0 / 0 | 45 040 / 50 683 | 12/12 / 0 | −0,43 |
| he | Arial | word / default | 12 | 830 | 100 % / 100 % | 9/12 | 0 / 0 | 0 | 0 / 0 | 19 106 | 12/12 / 0 | 0,00 |
| ar | Arial | word | 12 | 887 | 100 % | 9/12 | 0 | 0 | 0 / 0 | 9 786 | 12/12 / 0 | — |
| ar | Arial | default | 12 | 887 | 100 % | 9/12 | 1 | 670 (0,108 %) | 0 / 0 | 8 498 | 12/12 / 0 | 0,00 |
| ar | Geeza Pro (AAT) | word / default | 12 | 875 | 100 % / 100 % | 9/12 | 0 / 0 | 0 | 0 / 0 | 0 / 33 556 | 12/12 / 0 | −0,96 |
| ar | Geeza Pro (AAT) | word-aat | 12 | 875 | 100 % | 9/12 | 0 | 0 | 0 / 0 | 0 | 12/12 / 0 | 0,00 к word |
| hi | Kohinoor Devanagari | word / default | 11 | 895 | 100 % / 100 % | 0/11 | 0 / 0 | 0 | 0 / 0 | 8 144 | 11/11 / 0 | 0,00 |
| hi | Devanagari MT (AAT) | word | 11 | 933 (CT: 871) | 0/11 (0 %) | — | — | — | 0,198 / 1,613 | — | 11/11 / 0 | — |
| hi | Devanagari MT (AAT) | default | 11 | 871 | 100 % | 0/11 | 0 | 0 | 0 / 0 | 0 | 11/11 / 0 | +0,20 |
| hi | Devanagari MT (AAT) | word-aat | 11 | 871 | 100 % | 0/11 | 0 | 0 | 0 / 0 | 0 | 11/11 / 0 | +0,20 к word |
| zh | Hiragino Sans GB | word / default | 6 | 185 | 100 % / 100 % | 6/6 | 0 / 0 | 0 | 0 / 0 | 0 | 6/6 / 0 | 0,00 |
Дополнительная проверка вне таблицы: системный вариативный шрифт
.SF Hebrew (SFHebrew.ttf: оси
wght, opsz 17–28, таблица trak) —
ширина корпуса he у CoreText на 4,8 % больше, чем у
HarfBuzz/harfrust с экземпляром по умолчанию (797 760 против 761 425
ед.); --font-ptem=12 у hb-shape ширину не
меняет, opsz=17 даёт 818 805. То есть CoreText сам
подбирает для таких шрифтов оптический размер и/или трекинг по кеглю;
подробно не разбиралось (см. 5.4).
4.2 Скорость
Сводно по письменностям (сумма медиан по шрифтам набора / число
абзацев; прогон из results-2026-10-04-m5max.md):
| Корпус | Режим | harfrust, как в ядре | harfrust, кэш плана | CoreText | HarfBuzz C | HB→CT | CoreText / harfrust (ядро) | CoreText / harfrust (кэш) |
|---|---|---|---|---|---|---|---|---|
| ru | word | 2,32 | 1,13 | 15,64 | 1,37 | 18,94 | ×6,7 | ×13,8 |
| ru | default | 3,40 | 2,36 | 9,84 | 2,93 | 9,94 | ×2,9 | ×4,2 |
| en | word | 2,29 | 1,22 | 13,00 | 1,48 | 16,47 | ×5,7 | ×10,7 |
| en | default | 3,09 | 2,10 | 6,94 | 3,21 | 8,29 | ×2,2 | ×3,3 |
| ligkern | word | 1,94 | 0,86 | 12,66 | 1,07 | 15,53 | ×6,5 | ×14,8 |
| ligkern | default | 2,65 | 1,60 | 6,26 | 2,39 | 7,10 | ×2,4 | ×3,9 |
| he | word | 3,93 | 2,73 | 22,78 | 3,83 | 23,69 | ×5,8 | ×8,3 |
| he | default | 4,05 | 3,41 | 16,93 | 4,41 | 14,41 | ×4,2 | ×5,0 |
| ar | word | 6,56 | 5,33 | 33,18 | 5,30 | 28,49 | ×5,1 | ×6,2 |
| ar | default | 6,80 | 6,05 | 22,53 | 6,94 | 19,86 | ×3,3 | ×3,7 |
| hi | word | 12,24 | 10,47 | 69,22 | 14,21 | 50,03 | ×5,7 | ×6,6 |
| hi | default | 12,52 | 10,65 | 51,31 | 14,64 | 40,59 | ×4,1 | ×4,8 |
| zh | word | 1,31 | 0,44 | 7,87 | 0,50 | 13,05 | ×6,0 | ×17,7 |
| zh | default | 1,26 | 0,45 | 2,14 | 0,44 | 3,03 | ×1,7 | ×4,7 |
Значения — мкс на абзац (медиана). Весь корпус в режиме
word: harfrust «как в ядре» — 3,82 мкс/абзац (22,9 млн
глифов/с), с кэшем плана — 2,63 (33,3 млн/с), CoreText — 22,2 (3,9
млн/с), HarfBuzz C — 3,29 (26,6 млн/с).
Устойчивость. Ещё два полных прогона по 50 повторов
(без hb-shape) дали абсолютные времена на 20–35 % выше (на
машине параллельно работали другие процессы), но отношения стабильны:
CoreText / harfrust «как в ядре» в режиме word — ×6,4–7,2
(ru), ×5,7–5,9 (en), ×5,4–5,8 (he), ×4,3–5,1 (ar), ×4,2–5,7 (hi),
×6,0–6,2 (zh); в режиме default — ×1,7–4,2. Разброс p10–p90
внутри прогона у большинства наборов — единицы процентов у harfrust и до
10–30 % у CoreText; отдельные наборы под фоновой нагрузкой дают p90 до
×1,5–2 от медианы у обоих движков. Холодный (первый) проход: harfrust
4–31 мкс/абзац, CoreText 66–280 мкс/абзац (загрузка шрифта и кэшей
CoreText). Полная таблица по шрифтам —
results-2026-10-04-m5max.md.
5. Где расходятся и почему
5.1 Что выглядит как расхождение, но им не является
- Продвижения у знаков огласовки (иврит, арабский,
деванагари). HarfBuzz/harfrust дают знаку нулевое продвижение и
смещение
x_offset; CoreText переносит это смещение в продвижения — знак получает отрицательное продвижение, а основа — увеличенное (например,1301: +1067, 1262: 0против1301: +1867, 1262: −800). Положения всех глифов при этом совпадают до единицы шрифта (0 сдвигов во всех таких наборах), отсюда большие Σ|Δadv| при нулевых Σ|Δx|. Попутно: суммаCTRunGetAdvancesу строк с огласовками не равна ширине строки (на 8 % в абзаце с полной огласовкой), поэтому ширину CoreText надо брать изCTLineGetTypographicBounds, а не складывать продвижения. - Кернинг справа налево. В Liberation Sans (иврит,
default) кернинг пары harfrust прибавляет к продвижению одного глифа, CoreText — соседнего; положения совпадают. - Кластеры. harfrust (как и HarfBuzz, уровень кластеров 0) сливает знак с основой в один кластер и сохраняет кластер слога для переставленных матр деванагари; CoreText отдаёт каждому глифу индекс его собственного символа. Отсюда 9/12 (иврит, арабский — абзацы с огласовками) и 0/11 (деванагари). Это разные договорённости, а не разные глифы; ядру нужна именно семантика HarfBuzz — она монотонна и пригодна для курсора и выделения.
5.2
Настоящее расхождение 1: clig = 0 ломает AAT-шрифт
деванагари
Devanagari MT — шрифт только AAT (morx
без GSUB). В режиме ядра harfrust даёт 933 глифа вместо 871 у CoreText:
часть конъюнктов и лигатур с матрами не собирается (например,
य+े в «हाशिये» остаётся двумя глифами вместо
одного), ширина строк в среднем на 0,2 % больше, в худшем абзаце — на
1,6 %. HarfBuzz C ведёт себя так же (11/11 совпадений с harfrust).
Причина: harfrust переводит OpenType-свойство clig в
селектор AAT «контекстные лигатуры» (тип 1, селектор 18/19), а в этом
шрифте под этим селектором (флаг 0x40 цепочки
morx) лежат обязательные конъюнкты. CoreText выключение
clig (тегом OpenType) для AAT-шрифта не переводит, а
kCTLigatureAttributeName = 0 оставляет «обязательные»
лигатуры. Проверено hb-shape: -kern,-liga →
871 глиф, -kern,-liga,-clig → 933. Режим
word-aat (для шрифтов с morx и без GSUB
выключать только kern и liga) даёт 100 %
совпадение глифов и положений с CoreText и на Devanagari MT, и на Geeza
Pro. Для OpenType-шрифтов поправка ничего не меняет.
5.3
Настоящее расхождение 2: огласовка над лам-алифом (Arial, только
default)
Один глиф из 20 540: фатха над лигатурой лам-алиф в «ظَلَامٌ» — harfrust
ставит её в (1918, 294), CoreText (CTLine) — в (1248, 234), то есть
привязывает к другому компоненту лигатуры (GPOS mark-to-ligature).
Обёртка HarfBuzz над CoreText (--shapers=coretext) ставит
знак так же, как harfrust, поэтому это особенность типографа
CTLine, а не шрифта. В режиме ядра (word)
расхождения нет.
5.4 Чего в CoreText нет у ядра (и не должно быть)
- Оптический размер и трекинг по кеглю для системных шрифтов
Apple (
.SF Hebrew: +4,8 % ширины). CoreText применяет их неявно; ядро шейпит экземпляр по умолчанию и не передаёт кегль вpoint_size. Это скрытые шрифты интерфейса macOS, в документах Office их нет; если когда-нибудь понадобятся — осьopszиShapeOptions::point_sizeзадаются явно и детерминированно. - Bidi и подбор запасного шрифта — CoreText делает их
сам внутри
CTLine; в стенде текст однонаправленный и полностью покрыт шрифтом, у ядра это отдельные детерминированные шаги (unicode-bidi,meridian-fonts).
5.5 Что совпало без оговорок
Латиница и кириллица на всех четырёх шрифтах, оба режима: глифы,
кластеры и положения идентичны, в том числе лигатуры Carlito
(default: 1 394 вместо 1 418 глифов у обоих) и кернинг
(−2,1…−2,8 % ширины строк набора ligkern у обоих). Это же
подтверждает, что kCTKernAttributeName = 0 и
kCTLigatureAttributeName = 0 действительно выключают
кернинг и лигатуры CoreText. Иврит, арабский (включая AAT Geeza Pro с
таблицей kern), Kohinoor Devanagari и китайский — глифы и
положения идентичны.
6. Выводы
- На OpenType-шрифтах harfrust и CoreText дают одну и ту же раскладку. 225 из 225 абзацев в режиме ядра и 236 из 236 в режиме по умолчанию — одинаковые глифы; положения совпадают до 0,5 единицы шрифта у всех глифов, кроме одного (5.3). Системный шейпер не дал бы ядру ни одного «более правильного» глифа.
- harfrust полностью совпадает с HarfBuzz 14.3.1 (100 % глифов, 0 сдвигов во всех 49 сочетаниях «набор × режим»), то есть и с системным шейпингом Linux (Pango/HarfBuzz) при тех же шрифтах.
- AAT harfrust поддерживает сам (
morx,kerx,kern,trak,mort): Geeza Pro и Devanagari MT шейпятся так же, как CoreText, — при условии поправки 5.2. НынешнийWORD_DEFAULT_OFFвshape.rsдля AAT-шрифтов без GSUB неверен: выключениеcligломает конъюнкты. - CoreText в 4–7 раз медленнее harfrust «как в ядре» в режиме ядра и в 1,7–4 раза — в режиме по умолчанию; с кэшем плана разрыв в режиме ядра ×6–18. Выключение свойств через атрибуты делает CoreText ещё в 1,3–3,7 раза медленнее его собственного режима по умолчанию. Холодный старт CoreText — 66–280 мкс на абзац против 4–31 у harfrust.
- В ядре есть дешёвый выигрыш ×1,2–3:
shape_segmentстроит шейпер, набор свойств и план шейпинга на каждый отрезок; кэшShaperна гарнитуру иShapePlanна (письменность, направление, язык, свойства) снижает время с 3,82 до 2,63 мкс на абзац по всему корпусу (латиница — вдвое, китайский — втрое). harfrust с кэшем плана по корпусу быстрее циклаhb-shape(2,63 против 3,29 мкс; исключение — AAT Geeza Pro в режимеword, +17 %); без кэша — на 16 % медленнее HarfBuzz C по корпусу и до ×1,8 на латинице, где построение плана дороже самого шейпинга. Оценка «до 25 % медленнее HarfBuzz» изoss-text-render-lingo-math.mdверна только для нынешнего вызова без кэша. - Системный шейпинг нужен не для документов. Он
оправдан только там, где цель — пиксель в пиксель как у приложения ОС
(текст интерфейса оболочки — его и так шейпит Qt) или шрифты с неявной
системной обработкой (
opsz/trakскрытых шрифтов Apple). Для шрифтов Graphite у harfrust поддержки нет (HarfBuzz C — есть), но и CoreText Graphite не поддерживает; это отдельный риск (раздел 8), системный шейпер его не закрывает.
7. Решение (ADR-A8, 2026-10-04)
Контекст. Ядро обязано раскладывать документ
одинаково на любой машине (§19 kernel-core.md); библия и
архитектура уже говорят «шейпинг — всегда HarfBuzz», но называют
устаревший rustybuzz, а про AAT и системные шейперы ничего
не сказано.
Решение.
- harfrust остаётся единственным шейпером текста документов на
всех ОС. CoreText, DirectWrite и Pango в раскладку документа не
подключаем — ни основным путём, ни запасным. Платформенные API шрифтов —
только для перечисления и чтения файлов шрифтов (
fontique), шейпит всё равно harfrust. - Поправка
WORD_DEFAULT_OFFдля AAT (задача вmeridian-layout): если у шрифта естьmorxи нет GSUB, выключать толькоkernиliga;cligне трогать. Тест — Devanagari MT на macOS-CI или любой свободный AAT-шрифт деванагари/арабицы в корпусе. - Кэш шейпера и плана в
FontFace:Shaperстроится один раз,ShapePlan— по ключу (письменность, направление, язык, набор свойств); буферUnicodeBufferпереиспользуется черезGlyphBuffer::clear. - Сверка с эталоном в CI: этот стенд (или его часть «harfrust ↔︎ HarfBuzz C») — ночной job на macOS при обновлении harfrust; сверка с CoreText — справочная, расхождения разбираются вручную, как в разделе 5.
Предложение в библию
(docs/00-foundation.md, строка «Шейпинг текста»;
docs/03-architecture.md §12 — заменить «HarfBuzz
(rustybuzz)»):
Текст документов шейпит только harfrust закреплённой версии, одинаково на Windows, macOS и Linux; системные шейперы (CoreText, DirectWrite, Pango) в раскладку документа не допускаются — на OpenType-шрифтах они дают те же глифы и положения, но в 4–7 раз медленнее и зависят от версии ОС. Шрифты AAT (
morx/kerx/trak) harfrust обрабатывает сам; для них режим Word выключает толькоkernиliga.
Последствия. Детерминизм вёрстки сохраняется без оговорок по платформам; на macOS документы с AAT-шрифтами шейпятся так же, как в Pages, после поправки 2; выигрыш скорости от поправки 3 — ×1,2–3 на шейпинге.
8. Риски и ограничения спайка
- Только macOS/CoreText. DirectWrite не мерили (нужна машина с Windows); вывод о детерминизме от этого не зависит, а скорость DirectWrite для решения не нужна, раз системный шейпер не используется.
- Корпус небольшой (236 абзацев, ≈ 20 тыс. глифов на
режим) и однонаправленный: нет смешанного bidi, эмодзи, вариативных
шрифтов документов, тайского, кхмерского, бирманского, сингальского.
Перед релизом — прогнать тест-набор шейпинга HarfBuzz против harfrust
(уже записано в E0.2 отчёта
oss-text-render-lingo-math.md). - Абзац = одна строка без переноса; на переносе harfrust и CoreText не сравнивались (перенос у ядра свой).
- Скорость CoreText включает создание
CFAttributedStringиCTLineна каждый абзац — это его обычный путь; кэшей уровняCTTypesetterмежду абзацами не использовали. Замерhb-shape— оценка через внешний процесс. Абсолютные времена гуляют на 20–35 % между прогонами (на машине работали другие процессы), отношения — нет. - Graphite harfrust не поддерживает; шрифтов Graphite в корпусе Office не ожидается, при появлении — отображать без «умных» правил (как Word на Windows) и отметить в отчёте импорта.
- Расхождение 5.3 (огласовка над лам-алифом) не разобрано до конца: какой компонент правильный, покажет сравнение с Word на Windows в корпусе; в режиме ядра оно не проявляется.
- Вариативные шрифты с
opsz(5.4): если документы начнут ссылаться на такие шрифты, нужна явная политика выбора оптического размера по кеглю, иначе ширина будет отличаться от приложений Apple на несколько процентов.
9. Как повторить
kernel/spikes/shaping-bench/run.sh results 50 # swiftc -O, cargo build --release, полный прогон (≈ 15 мин из-за hb-shape)
cd kernel/spikes/shaping-bench
./target/release/shaping-bench --reps 50 --no-hb-speed # без замера hb-shape — несколько секунд
./target/release/shaping-bench --only hi --diffs diffs.txt # один корпус и подробный разбор расхожденийНужны macOS (CoreText, swiftc из Xcode Command Line
Tools) и hb-shape из HarfBuzz по пути
/opt/homebrew/bin/hb-shape. Результат —
results.md (таблицы), results.jsonl (сырые
строки agree/speed),
results-diffs.txt (первое расхождение каждого абзаца с
контекстом и кодами символов).