Спайк A8: движок кистей — libmypaint через FFI или порт на Rust
Сессия A, 2026-10-04. Задача A8 брифа
docs/sessions/A-kernel.md(«libmypaint — FFI или порт»). Предложение решения — раздел 6. Стенд —kernel/spikes/mypaint-port(в продукт не входит): портrender_dab_maskиdraw_dab_pixels_BlendMode_Normal, побитная сверка с C, замеры; сырые данные —kernel/spikes/mypaint-port/results-2026-10-04-m5max.txt. Предыдущий разбор без замеров —docs/research/oss-graphics-pdf-charts.md§3.1.
1. Вопрос
Paint (и перо Notes) берёт движок кистей libmypaint и 196 кистей
mypaint-brushes (docs/00-foundation.md, строка Paint).
Нужно выбрать: FFI — немодифицированный C внутри
процесса, или порт — перенос на Rust в
meridian-raster (docs/03-architecture.md:
«растровые слои, кисти, тайлы 256²»). Критерии: лицензия, сборка на трёх
ОС, скорость, одинаковый результат на всех платформах (файлы Paint и
тесты корпуса сравниваются попиксельно), безопасность разбора чужих
данных, объём работы.
2. Что сделано
- Склонированы только зеркала
meridian-oss/libmypaint(коммитd5a88fbот 2026-04-14,v2.0.0-beta.1-195) иmeridian-oss/mypaint-brushes(08da4a4,v2.0.2-1). Из сети — только крейтcc. - Ядро libmypaint собрано системным
ccбез autotools: 11 файлов, нужных для мазка по тайлам, — без json-c и glib, с ручнымconfig.hв одну строку. Полная библиотека (16 файлов) — с json-c из системы и заголовками, сгенерированнымиgenerate.py(python3). - Порт на Rust
render_dab_mask(сcalculate_rr,calculate_rr_antialiased,calculate_opaи др.) иdraw_dab_pixels_BlendMode_Normal—src/dab_mask.rs,src/blend.rs; заголовки «Основано на libmypaint (ISC)»,NOTICEс текстом лицензии и списком перенесённого. ≈ 290 строк C → 400 строк Rust (с двуязычными комментариями, после rustfmt). - Сверка порта с C: эталон — немодифицированные
.cиз зеркала, собранныеbuild.rs(крейтcc,-O2) и вызываемые через FFI. Входы — детерминированный генератор (все параметры даба — целое / 2^k, без операций с плавающей точкой), повторён вc/bench.cдля чистого C. Тестtests/checksums.rsпроверяет контрольные суммы порта без C. - Замеры: микроядро (маска, маска + Normal) — Rust, C
через FFI, чистый C; полный путь FFI — все 196 кистей на записи
tests/events/painting30sec.dat(30 с рисования, 2 937 событий); кросс-проверка на x86_64 (Rosetta на Mac и Linux-сервер sagr с gcc/glibc); санитайзеры ASan/UBSan на широких и «диких» входах.
3. Разбор libmypaint и mypaint-brushes
3.1 Лицензии
| Что | Лицензия | Класс | Заметки |
|---|---|---|---|
libmypaint (COPYING) |
ISC («Copyright (C) 2008-2011 Martin Renold and contributors») | 1 | заголовки .c/.h ядра — тот же текст ISC; GPL/LGPL/MPL в
исходниках нет (поиск нашёл только m4-макросы autotools) |
fastapprox/*.h (быстрые pow/exp/log, Paul Mineiro,
2011) |
BSD-3-Clause | 1 | нужен только fastpow для спектрального режима |
rng-double.c (Кнут), fifo.c |
общественное достояние | 0 | |
m4macros/ax_check_*.m4 |
GPL с исключением Autoconf | — | только сборка autotools; не берём |
po/*.po |
«same license as the MyPaint package» (GPL-2+) | — | не берём, переводы свои |
| json-c 0.19 (зависимость) | MIT | 1 | только для разбора .myb |
mypaint-brushes brushes/** (196 .myb +
превью) |
CC0-1.0 (по Licenses.dep5, по
авторам) |
0 | Files: * (сборочные файлы, Licenses.md) —
GPL-2+, не берём |
Итог: и FFI, и порт допустимы политикой (классы 0–1). Для порта нужна
атрибуция ISC в NOTICE продукта и запрос B на запись в
third_party.toml (порт чужого кода — по правилу брифа).
3.2 Сборка и зависимости
| Обязательна? | Факт (замер на d5a88fb) |
|
|---|---|---|
| autotools (autoconf, automake, libtool, intltool) | нет | 11 файлов ядра и mypaint-brush.c собираются
cc -O2 -std=c99 с config.h из одной строки
#define MYPAINT_CONFIG_USE_GLIB 0 |
python3 (generate.py) |
да, при сборке | генерирует mypaint-brush-settings-gen.h (145 строк) и
brushsettings-gen.h (96) из
brushsettings.json; можно закоммитить готовые |
| json-c | да для mypaint-brush.c |
#include <json.h> безусловный (стр. 37), поле
brush_json в структуре кисти, разбор — стр. 1551–1690 (≈
140 строк). Без json-c:
fatal error: 'json.h' file not found. Остальные 15 файлов
json-c не используют |
| glib / gobject-introspection | нет | MYPAINT_CONFIG_USE_GLIB 0 (заглушки типов в
mypaint-glib-compat.h) |
| GEGL | нет | отдельный каталог gegl/ |
| gettext | нет | --disable-i18n; без HAVE_GETTEXT строки не
переводятся |
| OpenMP | нет | --enable-openmp — параллельно по тайлам в
end_atomic/get_color |
Полное ядро с json-c: 64 291 байт text+data (arm64,
-O2); 15 файлов без json-c (всё, кроме
mypaint-brush.c) — 0,76 с сборки последовательно.
Предупреждение clang в brushmodes.c:580:
(int)(random_sample_rate * RAND_MAX) —
RAND_MAX округляется во float до 2^31, при
rate = 1 получается переполнение int.
3.3 Объём кода
(wc -l, с комментариями)
| Файл | Строк | Что внутри | Для порта |
|---|---|---|---|
mypaint-brush.c |
1 698 | динамика кисти: 18 входов, 65 параметров, 44 состояния,
stroke_to, размазывание, разбор JSON |
да (JSON — на serde) |
mypaint-tiled-surface.c |
974 | маска даба (render_dab_mask и
помощники ≈ 270 строк — перенесено), draw_dab,
get_color, симметрия дабов |
маска — да; диспетчер — свой под тайлы 256² |
brushmodes.c |
626 | 8 функций наложения: Normal (перенесена), Normal_Paint
(спектральная), Normal_and_Eraser (+Paint), LockAlpha (+Paint), Color,
Posterize; get_color_pixels_accumulate |
да |
helpers.c |
608 | HSV/HSL/HCY, спектральное смешивание, rand_gauss |
да |
operationqueue.c + tilemap.c +
fifo.c + mypaint-fixed-tiled-surface.c |
255 + 89 + 114 + 152 | очередь дабов по тайлам 64×64 | нет, своя |
mypaint-mapping.c |
196 | кусочно-линейные кривые «вход → параметр» | да |
mypaint-symmetry.c + mypaint-matrix.c |
161 + 92 | 4 вида симметрии | да |
rng-double.c |
136 | генератор Кнута | да |
остальное (mypaint-surface.c,
-rectangle.c, -brush-settings.c,
mypaint.c) |
318 | интерфейсы, таблицы | частично |
итого .c / .h |
5 419 / 945 | 16 файлов .c, 19 .h |
|
fastapprox/*.h |
3 323 | нужен fastpow (≈ 30 строк) |
да |
brushsettings.json + generate.py |
826 + 349 | описание параметров | таблицы на этапе сборки |
3.4 Публичный API, потоки, детерминизм
- API (C, без glib): кисть — 25 функций
mypaint_brush_*(new,from_string,set_base_value,set_mapping_n/point,stroke_to,new_stroke,get/set_state, …); поверхность — интерфейсMyPaintSurfaceиз 6 указателей на функции (draw_dab,get_color,begin/end_atomic,destroy,save_png) + 9 функций тайловой поверхности (mypaint_tiled_surface_*,mypaint_tile_request_init; с запросами тайлов RGBA16) + 4 для симметрии. Пиксели —uint16RGBA, премультиплицированные, 0…2^15 («fix15»), тайлы 64×64. - Потоки: без OpenMP — однопоточно. С OpenMP —
#pragma omp parallel forпо тайлам вend_atomicиget_colorприthreadsafe_tile_requestsи > 3 тайлах;omp criticalпри накоплении цвета. Состояние кисти не потокобезопасно (одна кисть — один поток). - Детерминизм (по коду):
rand()из libc вget_color_pixels_accumulate(brushmodes.c:586) — случайная выборка пикселей при размазывании; последовательность иRAND_MAXразличаются в macOS, glibc и MSVC (32 767) → холст кистей с размазыванием зависит от ОС (не замерено, следует из кода). Генератор Кнута (rng-double) детерминирован.- libm: в динамике
expf×16,powf×8,sin/cos×7,hypotf×5,log×4,atan2f×3,fmodf×3; в маске —cos/sinот угла (раз на даб, clang сливает в__sincos_stret),floor,sqrtf. Точность libm разных ОС не гарантирована до последнего бита. - Слияние умножения и сложения в FMA компилятором C
(
-ffp-contract) — главный источник расхождений, замерен в 4.2. - Режим Normal целочисленный (uint32) — детерминирован везде.
4. Замеры
Условия. Apple M5 Max, 18 ядер, 128 ГБ, macOS
(Darwin 27); Apple clang 21.0.0 (clang-2100.3.34.2), rustc
1.97.1, release (opt-level=3, thin LTO), эталон C —
-O2 (как в autotools libmypaint). Машина была загружена
другими сессиями (load average 13–14 при финальном прогоне), поэтому
основная оценка — минимум из 15 повторов, в скобках —
медиана; реализации чередуются внутри каждого повтора. Сценарии: дабы со
случайными центром (с выходом за тайл на полрадиуса), жёсткостью 0,05–1,
мягкостью 0–0,8, вытянутостью 1–4, углом 0–360° на тайле 64×64 RGBA16 со
случайными премультиплицированными пикселями; aa — r 0,5–3
(ветка со сглаживанием r < 3), small — 3–8,
medium — 8–24, large — 24–64. Всё — одной
командой run.sh (раздел 8).
4.1 Побитная сверка порта с C (350 000 дабов)
| Сценарий | Дабов | C по умолчанию (clang, FMA): дабов с другой маской | макс. |Δ| маски (из 32 768) | Normal на общей маске: u16 ≠ | C с -ffp-contract=off: маски ≠ / тайл ≠ |
|---|---|---|---|---|---|
| aa_r0.5-3 | 200 000 | 2 352 (1,2 %) | 3 | 0 | 0 / 0 |
| small_r3-8 | 100 000 | 5 011 (5,0 %) | 2 | 0 | 0 / 0 |
| medium_r8-24 | 40 000 | 9 043 (22,6 %) | 3 | 0 | 0 / 0 |
| large_r24-64 | 10 000 | 5 037 (50,4 %) | 2 | 0 | 0 / 0 |
- С
-ffp-contract=offпорт совпадает с C побитно во всех 350 000 масках и итоговых тайлах (контрольные суммы FNV-1a равны:d4c89585…,946ea325…,d1843964…,cd861ab8…). - С настройкой clang по умолчанию на arm64
(
-ffp-contract=on, FMA) маски отличаются на 1–3 единицы fix15 в 1–50 % дабов; после всех дабов в тайле расходятся 1–10 компонент u16 на 1. - Режим Normal (целочисленный) совпадает всегда.
4.2
Один и тот же C на разных компиляторах и ISA (контрольные суммы чистого
C, c/bench.c)
| Сборка | aa | small | medium | large | Совпадает с портом |
|---|---|---|---|---|---|
Rust-порт (arm64, любой; тест tests/checksums.rs) |
d4c89585… |
946ea325… |
d1843964… |
cd861ab8… |
— |
clang arm64 -ffp-contract=off |
d4c89585… |
946ea325… |
d1843964… |
cd861ab8… |
да |
clang x86_64 (Rosetta), без -mfma |
d4c89585… |
946ea325… |
d1843964… |
cd861ab8… |
да |
gcc 13.3 x86_64 Linux/glibc, -std=c99 (±
-march=native, ± -ffp-contract=off) |
d4c89585… |
946ea325… |
d1843964… |
cd861ab8… |
да |
| clang arm64 по умолчанию | c4f8a61e… |
8fc3d6fd… |
633dd449… |
a59ae0f8… |
нет |
clang x86_64 -mfma -mavx2 |
c4f8a61e… |
8fc3d6fd… |
633dd449… |
a59ae0f8… |
нет |
gcc 13.3 x86_64
-std=gnu99 -march=native |
626f55fe… |
ced57ab4… |
8a29ddcc… |
a00647d4… |
нет |
Немодифицированный C даёт три разных результата в
зависимости от компилятора и флагов: gcc в режиме GNU C (умолчание
autotools libmypaint — -std не задан) использует
-ffp-contract=fast, clang — on, в режиме ISO C
у gcc — off. Без FMA результат одинаков на arm64 и x86_64,
на Apple libm и glibc (sin/cos от этих углов после
округления до float совпали). Цена фиксации
-ffp-contract=off для C — 1,5–3,3 % скорости маски в чистом
C (4–9 % в замере через FFI, другой прогон; таблица 4.3).
4.3 Скорость микроядра, нс на даб (минимум из 15; в скобках медиана)
C (FFI) — немодифицированный C, собранный cc с
-O2, вызов из Rust; чистый C — c/bench.c, без
Rust.
| Сценарий | Пикс./даб | Маска: Rust | Маска: C (FFI) | Маска: чистый C | Маска+Normal: Rust | Маска+Normal: C (FFI) | Normal, нс/пикс.: Rust / C |
|---|---|---|---|---|---|---|---|
| aa_r0.5-3 | 12,2 | 154 (156) | 158 (159) | 158 (165) | 173 (174) | 175 (176) | 1,55 / 1,40 |
| small_r3-8 | 65,2 | 232 (236) | 265 (268) | 271 (284) | 325 (331) | 340 (346) | 1,42 / 1,15 |
| medium_r8-24 | 396,2 | 909 (916) | 1 019 (1 030) | 1 051 (1 061) | 1 378 (1 399) | 1 435 (1 448) | 1,18 / 1,05 |
| large_r24-64 | 1 472,4 | 2 446 (2 487) | 2 606 (2 618) | 2 635 (2 802) | 4 088 (4 129) | 4 009 (4 038) | 1,12 / 0,95 |
То же при C с -ffp-contract=off (побитно равные
результаты): маска Rust 156 / 235 / 905 / 2 428 против C 164 / 281 / 1
109 / 2 733; маска+Normal 174 / 324 / 1 379 / 4 076 против 180 / 358 / 1
531 / 4 175. Чистый C без FMA: маска 162 / 275 / 1 072 / 2 723 (против
158 / 271 / 1 051 / 2 635 с FMA).
Первый, «дословный» порт (циклы
for xp in x0..=x1 с индексом) был медленнее C в 1,08–1,38
раза (маска 215 / 285 / 1 182 / 3 389 против 156 / 264 / 1 010 / 2 566):
clang векторизует цикл rr в C (93 векторные инструкции
.4s в render_dab_mask), а LLVM для Rust — нет
(0) из-за RangeInclusive и проверки границ. Переписывание
циклов через срезы строки (результат тот же побитно) дало векторизацию и
скорость из таблицы.
4.4
Полный путь FFI: libmypaint + json-c, 196 кистей ×
painting30sec.dat, холст 1024×1024
c/stroke.c: «динамика» — stroke_to без
get_color; get_color — выборка цвета для
размазывания (сама дорисовывает очередь дабов в затронутых тайлах и
строит маску); «растр» — end_atomic (маски и режимы по
тайлам).
| Кистей | Дабов | Время, мс | нс/даб | из них динамика / get_color / растр | |
|---|---|---|---|---|---|
| все | 196 (загрузились все) | 19 682 173 | 17 518 | 890 | 287 / 406 / 196 |
с размазыванием (get_color на каждый даб) |
95 | 11 639 989 | 13 303 | 1 143 (медиана по кистям 1 283) | get_color — 60 % времени |
| без размазывания | 101 | 8 042 184 | 4 215 | 524 (медиана 393) | 192 / 0 / 332 |
Примеры: classic/pencil — 89 083 даба, 25,6 мс, 287
нс/даб; classic/charcoal — 351;
deevad/watercolor_glazing — 2 217 (get_color 240 из 274
мс); deevad/airbrush — 1 547 дабов по 63 мкс (крупные
мягкие дабы, растр). Самая тяжёлая кисть набора
(Dieterle/Tail_Feathers2) — 714 мс на 30 с записи,
≈ 2,4 % одного ядра; медиана — 744 нс/даб. 12 кистей
Dieterle используют вход surfacemap_x, которого нет в
libmypaint d5a88fb (12 предупреждений «Unknown input_id»;
кисти грузятся, вход игнорируется).
4.5 Безопасность (санитайзеры, 300 000 вызовов маски + Normal)
- C,
clang -fsanitize=address,undefined,float-cast-overflow: выходов за границы памяти нет; 6 мест неопределённого поведения вmypaint-tiled-surface.cна недопустимых входах: float → int вне диапазона (стр. 419–422,floor(x ± r)при inf/1e30), float →uint16_tвне диапазона (стр. 477, отрицательная непрозрачность при мягкости > 1), переполнениеint(стр. 467, 469, 489). Вызывающий код ограничивает радиус и мягкость (CLAMPвmypaint-brush.c), но координаты не проверяет. UB проявляется по-разному на разных ISA (arm64 насыщает, x86 — усекает). - Порт на тех же входах: в release паники нет (маска пустая); в debug — паника «attempt to add with overflow» при x = 1e30; порчи памяти нет по построению.
5. Выводы
- Скорость не решает выбор. Порт микроядра быстрее C на маске на 3–12 % и равен ему на маске + Normal (от −4 % до +2 %); накладные FFI на даб не видны (C через FFI = чистый C в пределах шума). На полном пути самая тяжёлая из 196 кистей занимает ≈ 2,4 % ядра на 30 с рисования. Векторизация в Rust требует внимания (циклы по срезам), но даётся и не меняет результат.
- Детерминизм — главный аргумент за порт. Один и тот
же C даёт три разных холста в зависимости от компилятора (clang/gcc),
режима языка и наличия FMA. FFI потребует жёстко фиксировать
-ffp-contract=offна всех платформах (дёшево: единицы процентов скорости) — но остаютсяrand()libc в размазывании и libm в динамике, которые FFI не исправит без правки C. Rust не сливает операции в FMA никогда, порт побитно равен C без FMA и одинаков на arm64 и x86_64;rand()заменяется своим генератором, libm — чистым Rust (крейтlibm) там, где нужна одинаковость между ОС. - FFI без json-c невозможен без правки C:
mypaint-brush.cбезусловно включаетjson.hи хранит JSON кисти. Разбор.mybвсё равно лучше делать на Rust (serde) черезmypaint_brush_set_base_value/set_mapping_*, но json-c при FFI остаётся в сборке на трёх ОС (или нужен патч ≈ 140 строк). Сборка без autotools проста (cc+config.h+generate.py/готовые заголовки). - Безопасность: C-ядро не выходит за память на диких
входах, но содержит UB, а разбор
.mybидёт через json-c — при FFI нужен фаззинг C под санитайзерами (cargo-fuzz + cc) и проверка входов на стороне Rust. - Порт малого объёма и хорошо проверяем: перенос
маски и Normal (≈ 290 строк C) занял часы, а дифференциальная сверка с C
(немодифицированный C через
ccв тестах + контрольные суммы) ловит отличие в одну единицу fix15. Тот же приём масштабируется на весь движок: 196 кистей ×painting30sec.dat. - Нагрузка — в динамике и размазывании, не в растре.
78 % времени полного пути —
stroke_toиget_color; размазывание (95 из 196 кистей) в 2,2 раза дороже обычных кистей на даб. Значит, порт должен покрытьmypaint-brush.cцеликом, а оптимизировать — выборку цвета (get_color) и очередь дабов, а не только маску.
6. Решение (предложение ADR-A8, 2026-10-04)
Контекст. Paint и перо Notes используют кисти MyPaint (196 кистей CC0); холсты должны быть одинаковыми на Windows, macOS и Linux (тесты корпуса, обмен файлами); процесс редактора не должен содержать C без нужды.
Решение.
- Порт на Rust движка libmypaint в
meridian-raster(модульbrush): динамика (mypaint-brush.c), кривые, симметрия, маска даба и все 8 функций наложения,rng-double,fastpow. Очередь дабов и тайлы — свои (256², формат пикселейmeridian-raster); формулы маски и режимов — дословно, с заголовками «Основано на libmypaint (ISC)» и NOTICE. - FFI не используем в продукте. Немодифицированная
libmypaint из зеркала собирается крейтом
ccтолько в тестах как эталон (-ffp-contract=off): дифференциальные тесты «порт = C» побитно на всех 196 кистях ×painting30sec.dat(как в спайке). json-c в тестах — системный или заглушка. - Правила детерминизма для
meridian-raster: безmul_add/FMA-интринсиков и без «быстрой математики»; генератор случайности — свой с явным зерном вместоrand(); трансцендентные функции в путях, влияющих на пиксели, — через крейтlibm(чистый Rust); контрольные суммы холста — в тестах на трёх ОС. - Разбор
.myb—serde_json(json-c не нужен); входsurfacemap_xиз новых кистей — поддержать при порте.
Для библии (одна-две фразы): «Кисти Paint — порт
libmypaint (ISC) на Rust в meridian-raster с побитной
сверкой с C в тестах; C-код libmypaint в продукт не входит. Растр кистей
детерминирован между ОС: без FMA, без rand() libc,
математика — libm.»
7. Объём порта по модулям (оценка, чел.-дни)
| Модуль | Строк C | Оценка | Комментарий |
|---|---|---|---|
маска даба (render_dab_mask и помощники) |
≈ 270 | 0 (сделано) | перенос + сверка — в спайке |
режимы brushmodes.c (ещё 7 +
get_color_pixels_accumulate) |
600 | 2–3 | спектральные режимы с fastpow |
helpers.c (цветовые модели, спектр,
rand_gauss) |
608 | 2 | |
mypaint-brush.c (динамика, без JSON) |
≈ 1 560 | 6–8 | основной объём и основной риск расхождений (libm) |
mypaint-mapping.c |
196 | 0,5 | |
| симметрия + матрицы | 253 | 1 | |
rng-double.c + fastpow |
≈ 170 | 0,5 | |
таблицы параметров из brushsettings.json (вместо
generate.py) |
826 JSON | 1 | build.rs или константы |
загрузка .myb на serde + 196 кистей CC0 в ресурсах |
≈ 140 | 1 | |
своя очередь дабов и тайлы 256² вместо
operationqueue/tilemap/fifo |
≈ 610 | 2–3 | под формат meridian-raster |
| дифференциальные тесты 196 кистей против C, контрольные суммы на 3 ОС | — | 2–3 | стенд спайка — основа |
| итого | ≈ 5 000 | 18–23 чел.-дня (≈ 1 чел.-мес.) | без SIMD-оптимизации под 256² и без новых типов кистей |
Для сравнения, FFI (оценка): обёртка MyPaintSurface над
тайлами meridian-raster с конвертацией fix15 ↔︎ формат
слоёв, сборка cc на трёх ОС с json-c или патчем, фаззинг
под санитайзерами, фиксация флагов — 7–10 чел.-дней, но без решения
проблем rand()/libm и с C в процессе редактора.
8. Риски
- libm в динамике: переход на крейт
libmдаст одинаковость между ОС, но отличие от libmypaint (эталон в тестах использует системную libm) — сверку динамики придётся вести с допуском или собирать эталон с той же libm (оценка: подменитьexpf/powf/…в C через-D-макросы на вызовы Rustlibmпо FFI). - Размазывание в оригинале недетерминировано
(
rand()); эталон для 95 кистей — только при подменеrandв тестовой сборке C (макрос), иначе сравнение статистическое. - Тайлы 256² вместо 64² меняют порядок обработки
дабов в
get_color(дорисовка очереди) — возможны отличия от libmypaint на кистях с размазыванием; сверять на тайлах 64² и отдельно проверять независимость от размера тайла. - Векторизация в Rust не гарантирована (пример 4.3):
нужны бенчмарки в CI с порогом, при необходимости — явный SIMD
(
std::simd/wide) с теми же поэлементными операциями (без FMA). - Набор кистей и версия движка: 12 кистей используют
вход
surfacemap_x, неизвестный ревизииd5a88fb. - Отладочные паники порта на диких входах
(переполнение
skip) — в продуктовом коде ранний выход для дабов вне тайла и проверка координат на входе.
9. Как повторить
# всё сразу: клоны зеркал в $WORK, сборка C без autotools, сверка, замеры, 196 кистей; ≈ 1–2 мин
WORK=$TMPDIR/mypaint-spike REPS=15 kernel/spikes/mypaint-port/run.sh results.txt
# по отдельности (MYPAINT_SRC — клон meridian-oss/libmypaint на d5a88fb):
cd kernel/spikes/mypaint-port
MYPAINT_SRC=$WORK/libmypaint MYPAINT_FP_CONTRACT=off cargo run --release -- verify # побитно = C
MYPAINT_SRC=$WORK/libmypaint cargo run --release -- verify # C с FMA: расхождения
MYPAINT_SRC=$WORK/libmypaint cargo run --release -- bench 15 # скорость Rust и C (FFI)
cargo test --release # контрольные суммы порта без CКросс-проверка на Linux (раздел 5 файла результатов) выполнена
вручную на sagr: те же 11 файлов ядра и c/bench.c,
gcc -O2 -std=c99|gnu99 [-march=native] [-ffp-contract=off];
временный каталог удалён.