Chroix — optimization research (raw agent output, 2026-10-03)
Источник:
dev/fera/community/CHROIX-OPTIM-RESEARCH.md(read-only; сгенерировано scripts/sync-docs.mjs)
Chroix — optimization research (raw agent output, 2026-10-03)
Section titled “Chroix — optimization research (raw agent output, 2026-10-03)”Five parallel research agents (Opus). Raw structured output, to be distilled into V2 proposals.
Compile-time codegen
Section titled “Compile-time codegen”Compiled templates (static/dynamic split)
Section titled “Compiled templates (static/dynamic split)”- idea: proc-macro view! парсит дерево виджетов и генерит статический дескриптор:
static TEMPLATE: Template { roots: &'static [TemplateNode], dyn_paths: &'static [&'static [u8]] }. Статические поддеревья — это данные в rodata, динамика — только «дыры» (индексы сигналов/выражений). Диффинг статики = сравнение указателей, рантайм ходит только по дырам. - runtimeGain: Нулевые аллокации VDOM на статических поддеревьях (в чате — 90%+ дерева: аватарки, бабблы, разметка); diff O(число динамических узлов) вместо O(дерево); дешёвый re-render при смене сигнала — пересоздаётся только дыра.
- compileTimeCost: Средняя: полный syn-парс rsx (включает syn/full) — +2–5 с на proc-macro крейт один раз, дальше линейно по объёму макросов. Митигируется venial/unsynn вместо syn.
- tradeoffs: Нужен свой парсер DSL внутри proc-macro (поддержка if/for в разметке усложняет схему путей); hot-reload разметки требует повторного парсинга исходника (как dioxus-hot-reload); дебаг-вывод macro expansion нужен сразу (cargo-expand).
- existingUsage: Dioxus (rsx! → Template, с 0.4; dioxus-hot-reload парсит rsx текстуально), Leptos (view! → typed builders; SSR static segments), Slint (компилирует весь .slint). Prior art вне Rust: Vue 3 static hoisting + block tree, Solid — compiled statements без VDOM.
- rustFit: Идеально: Template — это ’static данные, свободно пересекают границу fload-DLL как данные (код меняется при свопе, данные-описания нет). Хорошо ложится на решение «нет runtime tracking»: список дыр известен при компиляции.
Dirty bitmask codegen (Svelte/Compose-style)
Section titled “Dirty bitmask codegen (Svelte/Compose-style)”- idea: Макрос при парсинге тела view/update собирает, какие сигналы читаются (через syn visitor по
.read()/.get()), присваивает каждому сигналу бит в u64/u128 и генерит:if dirty & MASK_X != 0 { ... }вокруг блоков обновления. Никакого runtime-графа подписок — только маски. - runtimeGain: Инвалидация за 1-2 инструкции на проверку; нет HashMap<SignalId, Vec
> и его аллокаций/кэш-миссов; отлично дружит с frame budget — проверка 1000 масок дешевле одного кэш-промаха на графе. - compileTimeCost: Низкая: один syn::visit::Visit по телу функции, генерация масок — тривиальный emit. Самая дешёвая техника из списка на единицу выигрыша.
- tradeoffs: Лимит 64/128 сигналов на скоуп (далее — битсет из нескольких слов, усложнение codegen); динамические зависимости (чтение сигнала по вычисленному ключу) выпадают из схемы — для них нужен рантайм-фолбэк; переименование API чтения ломает visitor.
- existingUsage: Svelte ($$.dirty bitmask — канонический пример), Kotlin Compose compiler (генерит параметр $changed: Int + skippability по stability типов), Relm4 (view! макрос генерит вотчеры только на реально используемые поля модели). В Rust-фреймворках полноценно не сделано — свободная ниша.
- rustFit: Отлично: маски — Plain Old Data, пролетают через DLL-границу без ABI-проблем; прямо реализует уже принятое решение «compiled signal graph, нет runtime tracking».
Flat dependency tables из AST
Section titled “Flat dependency tables из AST”- idea: По результатам анализа чтений/записей (см. bitmask + update codegen) макрос на корне приложения (app!{}) генерит
static EDGES: &[(u32,u32)], топологический порядок обхода и таблицу «сигнал → список грязных масок». Планировщик кадра — тупой цикл по таблице, граф не строится и не хранится в куче. - runtimeGain: Планирование кадра без аллокаций и без обхода графа указателей — плоский массив, prefetch-friendly; детерминированный порядок обновления; удаление целых классов багов «гонка подписок».
- compileTimeCost: Низкая-средняя по времени компиляции, высокая по сложности авторства макроса: анализ в пределах одного макро-вызова прост, межфункциональный — требует собирать весь граф в корневом app!{}.
- tradeoffs: Циклические зависимости обнаруживаются только если весь граф виден одному макросу — динамические плагины через fload не попадут в таблицу (для них нужен runtime-сегмент графа или запрет реактивности через границу); большие таблицы раздувают rodata (незначительно).
- existingUsage: Svelte (топологическая сортировка $: блоков при компиляции), Slint (property binding graph резолвится компилятором, включая two-way bindings), Vue 3 compiler. Контраст: bevy schedule и leptos/dioxus-signals строят граф в рантайме.
- rustFit: Очень хорошо: таблицы — данные, шарятся между DLL; но межвиджетные зависимости требуют единой точки сбора (корневой макрос), что согласуется с Elm-outside: единый update loop = единая таблица.
Layout specialization — код вместо интерпретатора
Section titled “Layout specialization — код вместо интерпретатора”- idea: Для статически известного layout (фиксированные ряды/колонки, константные отступы, известные пропорции) макрос генерит прямой код вычисления Rect:
r.y = header_h + i * row_hвместо обхода дерева интерпретатором (taffy-стиль). Интерпретатор остаётся только для действительно динамических поддеревьев (список сообщений переменной высоты). - runtimeGain: Убирает обход дерева, кэш constraint’ов и виртуальные вызовы measure на статическом большинстве UI; вычисления скалярные и SIMD-уязвимые; layout статического экрана — десятки инструкций вместо тысяч.
- compileTimeCost: Средняя: объём сгенерированного кода растёт (мономорфизация layout-функций), при codegen-units=1 ощутимо на больших приложениях. Делить: специализация только для помеченных layout!{}-узлов.
- tradeoffs: Макросу нужна собственная модель layout-системы (риск рассинхрона с рантайм-интерпретатором — нужен тест «spec == interp» на одних входах); анимированные/резиновые layout всё равно требуют интерпретатора; рост бинаря.
- existingUsage: Slint (генерит код layout из .slint, включая вложенные формулы), const-generic compile-time layout редко, но taffy/кайя (interpreter) — то, что заменяем. Prior art: compile-time layout в Dear ImGui-стиле статических панелей, частично Clay (но Clay интерпретатор).
- rustFit: Хорошо: сгенерированные fn мономорфизируются и инлайнятся при codegen-units=1; на границе DLL layout-функция вызывается по указателю из дескриптора — ABI-простая (constraints in, rect out).
Const-eval анимаций и easing LUT
Section titled “Const-eval анимаций и easing LUT”- idea: easing-кривые, решение cubic-bezier (итерации Ньютона), коэффициенты пружин, цветовые рампы (oklch-интерполяция темы) считаются в const fn и материализуются как
static EASE_OUT_CUBIC: [f32; 257]. Кадр анимации = lookup + lerp. - runtimeGain: Ноль float-математики на кадр на кривую; детерминизм между платформами (нет libm-дрейфа — важно для snapshot-тестов headless-рендера); таблицы лежат в rodata, общие для всех анимаций.
- compileTimeCost: Ничтожная: const fn с float-циклами — миллисекунды на таблицу. Ограничение — const eval budget для огромных таблиц (лимитируется, поднимается фичей/переносом в build.rs).
- tradeoffs: Параметризованные кривые (пружина с произвольным stiffness/damping) не сводятся к одной таблице — либо сетка LUT по дискретным параметрам, либо рантайм-расчёт для нестандартных; точность таблицы vs плавность — подбирать шаг.
- existingUsage: Классика геймдева (LUT для easing/springs), const-fnv1a-hash (const-хэши в match), palette (частично const fn), glam (const fn векторная математика). Compose/SwiftUI считают easing в рантайме — тут Rust может быть строго лучше.
- rustFit: Идеально: f32-арифметика в const fn стабильна с Rust 1.82; LUT — данные, тривиально живут за DLL-границей; headless-рендер и Vello-рендер видят одни и те же значения (важно для пары Vello+headless).
build.rs кодогенерация схемы display list
Section titled “build.rs кодогенерация схемы display list”- idea: Операции display list (rects, glyphs runs из Parley, clips, transforms, layer push/pop) описываются в схеме (flatbuffers .fbs или rkyv-derive структуры); build.rs генерит encode/decode, общий для UI-крейта и рендер-крейта. Граница fload передаёт буфер, а не repr(C) структуры.
- runtimeGain: Zero-copy: рендерер читает команды прямо из буфера UI-потока (rkyv/flatbuffers), нет сериализации/десериализации на кадр; схема версионируется — старый рендерер может отвергнуть новый список явно.
- compileTimeCost: Разовая: prost-build/flatc/rkyv-derive отрабатывают в build.rs и кэшируются; инкрементально перегенерация только при смене схемы. Дисциплина: схема — отдельный крейт, чтобы обе стороны DLL собирались из одного источника.
- tradeoffs: Flatbuffers требует builder-стиля записи (неудобнее struct literals); rkyv привязывает к версии rustc на обеих сторонах (для hot-swap dylib это и так требование); схема требует дисциплины эволюции (удалять поля нельзя).
- existingUsage: prost-build/tonic (protobuf codegen в build.rs), flatbuffers (zero-copy, версионированная схема), rkyv (архивные типы с определённым layout), zerocopy (Fuchsia), capnp. Для DLL-плагинов: stabby, abi_stable — та же задача «без repr(C)».
- rustFit: Сильно: прямо реализует «fload без repr(C)» — rkyv archived types имеют гарантированный layout при одинаковом rustc; flatbuffers выдерживает даже разные версии компилятора. Headless-бэкенд потребляет тот же буфер — один контракт на обе реализации.
Макросы derived state / selectors
Section titled “Макросы derived state / selectors”- idea: Макрос
select!(state => .chats[id].unread_count)генерит линзу (путь в state) + memo-узел, чьи зависимости на этапе компиляции попадают в dep-таблицу и dirty-маску. Пересчёт derived-значения запускается только когда поднят соответствующий бит. - runtimeGain: Мемоизация без сравнения замыканий и без runtime-графа; триггер пересчёта O(1) через маску; selector equality (skip если результат не изменился) генерится с PartialEq-шорткатом.
- compileTimeCost: Низкая: парсинг path-выражения — маленький syn-visitor; эмит линз механический. Дороже становится при вложенных вычислимых ключах (индексы по сигналу — запретить или рантайм-фолбэк).
- tradeoffs: Вычислимые ключи (id из сигнала) ломают статический анализ — нужен escape hatch
select_dyn!с рантайм-подпиской; линзы через Option/Vec индексации требуют сгенерированного кода с проверками, ошибки должны быть читаемыми (care про diagnostics). - existingUsage: Zustand/Recoil selectors (prior art, JS), leptos create_memo/create_selector (рантайм — контраст), dioxus-signals memo (рантайм), lens-rs (Rust-макрос линз). Svelte 5 $derived — компилируемый derived, ближайший аналог.
- rustFit: Хорошо: селекторы — чистые fn над &State, естественны для Elm-outside (один Model, линзы в него); compose с bitmask-техникой даёт полный fine-grained слой без runtime tracking.
Type-level view trees / мономорфизированный dispatch
Section titled “Type-level view trees / мономорфизированный dispatch”- idea: Композиция виджетов как вложенные дженерики (
Column<Label, List<Msg, Bubble>>) с tuple-имплементациями трейта View до N (как xilem/Bevy). Никаких trait objects на горячем пути — весь dispatch статический, дерево типов = дерево виджетов. - runtimeGain: Полная девиртуализация и инлайнинг measure/layout/draw на статических ветках; PGO видит конкретный call graph; нулевая стоимость композиции обёрток (Padding<Align
> исчезает). - compileTimeCost: ВЫСОКАЯ, максимальная в списке: взрыв мономорфизации × codegen-units=1 × LTO — именно это делает bevy/xilem-разработку мучительной. Митигировать: двухпрофильная сборка (dev: dynamic linking / bevy_dylib-стиль, release: CU=1+LTO+PGO+BOLT), #[inline] только на доказанно горячем, type-erased Element на границах редко меняющихся поддеревьев.
- tradeoffs: Compile time и размер бинаря; сообщения об ошибках на глубоких типах чудовищны (нужны static_assertions::assert_impl_all, derive-where, ручные type alias’ы); recursion_limit; рефакторинг типа дерева = рефакторинг всех сигнатур.
- existingUsage: Xilem (тип дерева = его форма, tuple-имплементации View до N, Memoize), Bevy (tuple SystemParam/WorldQuery до 15, общеизвестно тяжёлые сборки), sycamore (generic views). Контраст: iced стирает типы в Element (dyn) — платит рантаймом.
- rustFit: По runtime — отлично, но есть конфликт с hot-swap: мономорфизированный код живёт в той dylib, где инстанциирован; виджеты, собранные в app-DLL, нельзя свопнуть по одному. Решение: мономорфизация внутри DLL, через границу — dyn за fload-vtable (nylon-ring).
Enum dispatch для встроенных виджетов
Section titled “Enum dispatch для встроенных виджетов”- idea: Замкнутое множество встроенных виджетов приложения собирается макросом в один enum Widget { Label(..), Button(..), … } с сгенерированными match-имплементациями трейтов (enum_dispatch или свой макрос). dyn остаётся только на границе плагинов.
- runtimeGain: Dispatch через match — предсказуемый переход + возможность инлайна мелких методов, против непредсказуемого indirect call через vtable; меньше указателей на vtable в памяти виджета.
- compileTimeCost: Низкая: enum_dispatch — лёгкий derive без полного syn-парсинга чужих типов; центральный enum пересобирает только свой крейт при добавлении виджета (а это и есть свопаемый app-DLL — даже плюс).
- tradeoffs: Добавление виджета = правка центрального enum (для closed world — норм); размер enum = размер максимального варианта (большие виджеты — Box внутри варианта); не подходит для открытого плагинного набора.
- existingUsage: enum_dispatch (crate, бенчи ~10x против dyn на мелких методах), derive_more, подход «closed set enum» внутри tokio/smol и рендереров. Slint генерит enum item’ов из .slint.
- rustFit: Сильно: внутренние виджеты живут в одном DLL — enum там же; плагины через fload — trait object на границе, внутри границы enum. Не конфликтует с type-level деревьями: enum — для листьев/примитивов.
Typestate/compile-time FSM для виджетов
Section titled “Typestate/compile-time FSM для виджетов”- idea: Жизненные циклы виджетов (загрузка истории чата: Idle→Loading→Loaded→Failed) и инварианты формы графа (арность связей, «эффект не читает то, что пишет») проверяются типами/макросом при компиляции; переходы генерятся исчерпывающим match.
- runtimeGain: Исчезают рантайм-проверки состояния и unreachable!-ветки; переходы — branchless по таблице; компилятор выкидывает мёртвые состояния.
- compileTimeCost: Средняя: DSL-парсинг маленький, но typestate раздувает trait resolution и ухудшает ошибки; rust-fsm-подход (макрос генерит обычный enum + match) дешевле для компилятора, чем чистые typestate-типы.
- tradeoffs: Typestate раздувает сигнатуры и ошибки компилятора; не каждый инвариант выражаем; риск переинжинирить — брать только для 2-3 реально болезненных автоматов (загрузка, reconnect, auth).
- existingUsage: rust-fsm (DSL → compile-time проверенный FSM), statig, smlang, typestate-паттерн в hyper/embedded-hal. Bevy States — рантайм (контраст). Для графов сигналов аналогов нет — ниша.
- rustFit: Хорошо для per-widget FSM; честно: полную ацикличность сигнального графа типами НЕ проверить без чудовищных typenum-конструкций — ограничить типами локальные инварианты, граф валидировать в корневом макросе (техника 3) с человекочитаемой ошибкой.
PHF/const-hash интернинг идентификаторов
Section titled “PHF/const-hash интернинг идентификаторов”- idea: Имена классов стилей, id виджетов, строковые ключи сообщений —
static MAP: phf::Map<&'static str, u16>или match поconst fn fnv1a(s). Никаких HashMap/SipHash на горячем пути lookup’ов. - runtimeGain: O(1) lookup с идеальным хэшем без рантайм-таблиц и их инициализации; сравнение по u16-id вместо строк везде ниже по стеку (стили, события, dispatch).
- compileTimeCost: Ничтожная: phf_macros и const fn хэши — копеечны. Осторожно только с объёмом: тысячи строк в одной phf-таблице ощутимы при парсинге макроса.
- tradeoffs: Ключи должны быть известны при компиляции — пользовательские/плагинные строки требуют рантайм-интернера рядом (двухуровневая схема: phf для ядра, interner для плагинов); коллизии const-fnv в match надо контролировать (assert на этапе макроса).
- existingUsage: phf/phf_macros, Servo string_cache (interning атомов в движке), const-fnv1a-hash (const-хэш в match-константах), string-interner (рантайм — контраст).
- rustFit: Идеально: статики, безопасны через DLL-границу; id вместо строк — ещё и ABI-упрощение для fload-сообщений.
Asset baking в build.rs
Section titled “Asset baking в build.rs”- idea: Иконочные атласы, MSDF/SDF-глyфы, предпарсенные темы (палитры уже в линейном RGB), локали — запекаются build.rs в байты и подключаются через include_bytes!/include_dir. Рантайм не содержит парсеров SVG/ttf/локалей.
- runtimeGain: Старт без парсинга и растеризации; меньше рантайм-зависимостей (и их кода в бинаре); предсказуемый frame 1 — атлас уже в нужном формате для Vello.
- compileTimeCost: Разовая в build.rs (MSDF-генерация может быть секунды на шрифт — кэшировать в target/ и пропускать по хэшу входов); рантайм-экономия на каждом старте.
- tradeoffs: Рост артефактов сборки; инкрементальность build.rs требует дисциплины (rerun-if-changed); правка ассета = пересборка куска пайплайна.
- existingUsage: rust-embed, include_dir, include_bytes!, типичные game pipeline’ы (baked atlases), vergen-стиль кодогенерации метаданных. msdfgen/atlasgen тулчейны — из геймдева.
- rustFit: Хорошо, с оговоркой: запечённые ассеты внутри swappable dylib раздувают payload хот-свапа. Решение: ассеты — отдельный data-файл или отдельный НЕсвопаемый крейт/DLL, свопаемый UI шлёпает только пути/handle’ы.
Codegen сообщений и write-masks (Elm-outside)
Section titled “Codegen сообщений и write-masks (Elm-outside)”- idea: Derive/макрос над
enum Msg: генерит диспатч-таблицу хендлеров, а из анализа тел update-хендлеров (какие сигналы пишутся) — write-mask на вариант сообщения. Планировщик: обработал Msg::X → поднял биты WRITE_MASK[X] → dirty-проверки по маскам (техника 2). - runtimeGain: Инвалидация после update — без diff’а состояния и без «погаси всё»: точные биты записанных сигналов; dispatch сообщений — таблица fn-указателей/прямой match, исчерпанность проверена компилятором.
- compileTimeCost: Низкая-средняя: derive по enum Msg — стандартный syn-derive; таблица записей требует видеть тела хендлеров — либо они в том же макро (update!{}), либо аннотации #[writes(..)] с ручной дисциплиной.
- tradeoffs: Write-анализ хрупок к косвенным записям (через &mut переданный дальше) — нужны либо аннотации, либо консервативный fallback (маска «всё» для неанализируемых хендлеров); ложные биты = лишняя перерисовка, не баг — деградация мягкая.
- existingUsage: Elm compiler (эффекты/сообщения — канон), Relm4 (макрос связывает поля модели и виджеты), iced (рантайм-сообщения — контраст), enum-map/enum_dispatch для таблиц. Svelte-компилятор — аналог для write-анализа.
- rustFit: Очень сильно: это прямое усиление уже выбранного Elm-outside; Msg enum и есть естественная граница сообщений для fload (enum + schema-кодогенерация = сериализуемые события через DLL).
Лёгкая макро-инфраструктура (анти-техника compile time)
Section titled “Лёгкая макро-инфраструктура (анти-техника compile time)”- idea: Инфраструктурные решения, чтобы макро-тяжёлая стратегия не убила итерацию: venial/unsynn вместо полного syn там, где грамматика простая; отдельный proc-macro крейт, собираемый один раз, а свопаемые DLL зависят только от его выхода через тонкий runtime-крейт; workspace-сплит; sccache + rust-lld; cargo-expand/insta/trybuild как обязательный тулчейн макросов.
- runtimeGain: Косвенный: позволяет держать в релизе codegen-units=1+LTO и при этом итерировать; dev-профиль с dynamic linking даёт секундный цикл правки UI-DLL.
- compileTimeCost: Отрицательная (это техника СНИЖЕНИЯ compile time): venial/unsynn режут стоимость proc-macro в разы против syn/full; sccache+rust-lld — константные секунды на линковке свопаемых DLL.
- tradeoffs: venial/unsynn покрывают не весь синтаксис (сложные generics придётся докручивать); поддержка своего тулчейна макросов — постоянная статья работ; двухпрофильная сборка требует дисциплины CI (проверять оба профиля).
- existingUsage: venial (godot-rust/gdext — крупный proc-macro проект на нём), unsynn, paste, seq-macro, derive-where, static_assertions, trybuild, insta, cargo-expand, cargo-hakari (feature unification), sccache. Bevy — dynamic linking feature (bevy_dylib) как эталон митигации для dev-профиля.
- rustFit: Обязательна для Chroix: hot-swap сам по себе — инструмент быстрой итерации, и он должен быть зарезервирован под runtime-перезагрузку кода, а не съедаться монструозными пересборками макросов.
Compile-time feature/ABI gates для hot-swap
Section titled “Compile-time feature/ABI gates для hot-swap”- idea: Компилятором сгенерированные assert’ы: const-eval проверки размеров/смещений ключевых структур display list, версии схемы и feature-флаги вшиваются в обе стороны; при загрузке DLL — handshake по сгенерированной таблице версий. Любой рассинхрон — отказ загрузки, а не UB.
- runtimeGain: Нуль на горячем пути; экономия — отсутствие охоты за UB при несовпадении версий DLL (самый дорогой класс багов hot-swap).
- compileTimeCost: Средняя: build.rs скачивает/проверяет версии, тесты медленнее. Снимает целый класс «тихих» багов ABI при частых свопах.
- tradeoffs: Нужно версионирование всего, что пересекает границу; ложные отказы при неосторожном бампе версий раздражают.
- existingUsage: guppy/hakari (determinism фичей), vergen (версии в бинарь), cargo-deny, flatbuffers reflection (схема в рантайме), stabby (проверки layout при загрузке).
- rustFit: Прямо обслуживает fload/nylon-ring модель: без repr(C) рассинхрон layout — главный риск, и его надо гасить сгенерированными проверками, а не дисциплиной.
Notes: Ключевые ссылки и крейты: dioxus-core (Template statics), slint-build, leptos_hot_reload, relm4-macros (view! watchers), enum_dispatch, stabby/abi_stable (контраст для fload/nylon-ring), rkyv/flatbuffers/prost-build/zerocopy, phf/phf_macros, const-fnv1a-hash, venial (godot-rust gdext), unsynn, paste, seq-macro, derive-where, static_assertions, trybuild, cargo-expand, insta, rust-fsm/statig/smlang, lens-rs, rust-embed/include_dir, linkme/inventory. Cross-language prior art: Svelte ($$.dirty bitmask + topo-sorted реактивность), Vue 3 (patch flags + block tree + static hoisting), Solid (compiled statements вместо VDOM), Compose compiler plugin ($changed:Int bitmask, stability inference, skippability) — Compose это самый близкий аналог «flat dependency tables на уровне компилятора». Приоритет для Chroix: техники 1+2+3+11 — это ОДНА когерентная макро-система (view! парсит тело, извлекает чтения сигналов, генерит Template + dirty-mask + dep table + write-mask хендлеров); делать вместе, не по отдельности. 6 (схема display list) — обязательна из-за fload, не опция. 7 — дешёвая победа, делать сразу. 5 — высший runtime-выигрыш но худший compile time и конфликт гранулярности с hot-swap: мономорфизированный код живёт внутри dylib, где определён; применять внутри одного DLL-модуля, не через границу. Предостережения: linkme/inventory НЕ работают через границу dylib (у каждой DLL своя копия distributed slice) — для плагинов нужна явная регистрация при загрузке; asset baking внутрь swappable dylib раздувает payload хот-свапа — ассеты в отдельный файл/несвопаемый крейт; с codegen-units=1 + LTO тяжёлая мономорфизация убьёт итерацию — митигировать двухпрофильной сборкой (dev: dynamic linking как bevy_dylib, release: CU=1+LTO+PGO+BOLT). Смежное (не compile-time, но из той же оперы): warm-start через rkyv-снапшот инициализированного состояния + mmap — ускоряет старт, отдельная техника.
Zero-copy ABI / DLL boundary
Section titled “Zero-copy ABI / DLL boundary”Static pointer table через exported symbol
Section titled “Static pointer table через exported symbol”- idea: Одиночный
#[no_mangle] pub static PLUGIN_API: PluginApiв DLL, где PluginApi — flat struct из extern fn pointers. Host один раз резолвит через GetProcAddress и дальше вызывает напрямую. Никакого vtable, никакого dyn — просто адрес функции. - runtimeGain: Zero-cost dispatch: одна индирекция через статический адрес, компилятор может инлайнить через PGO/BOLT. Полностью соответствует уже решённому ‘link-time dispatch без vtables’.
- compileTimeCost: Низкая — обычные extern fn и static
- tradeoffs: Все поля обязаны быть заполнены; добавление нового поля — append-only в конец struct, иначе layout ломается.
- existingUsage: Hotline (live coding Rust engine), many game engines, pluggy-подобные проекты, classic Win32 plugins
- rustFit: Идеально. Уже по сути является nylon-ring-моделью. На Windows требует
#[no_mangle] pub static+dllimportкорректно, с libloading работает из коробки.
Versioned struct envelope (sType/pNext pattern)
Section titled “Versioned struct envelope (sType/pNext pattern)”- idea: Каждая repr(C) структура начинается с
sType: u32(enum-магик) иpNext: *const c_void. Расширения — это цепочка структур через pNext. Host обходит цепочку, резолвит только те sType, которые знает. - runtimeGain: Позволяет эволюционировать ABI без ломки: старый DLL видит знакомые sType, новые игнорирует. Проверка версии O(1) на загрузке.
- compileTimeCost: Средняя — макросы для chain-резолва
- tradeoffs: Traversal цепочки на каждой инициализации; typed-обёртка требует unsafe для cast’ов.
- existingUsage: Vulkan (VkInstanceCreateInfo, VkDeviceCreateInfo, все extension-структуры), OpenXR, DirectX 12 (через D3D12_FEATURE_DATA), WebGPU native
- rustFit: Хорошо. Через
#[repr(C)]+*const c_voidи макрос для typed traversal. Немного boilerplate, но никаких vtables.
Layout fingerprint через build.rs
Section titled “Layout fingerprint через build.rs”- idea: build.rs проходит по всем repr(C) типам, вычисляет хеш от (имя, offsets, sizes, alignments) и встраивает в DLL как
pub static ABI_FINGERPRINT: u64. Host после LoadLibrary читает этот символ и сверяет со своим — при расхождении отказывается грузить или откатывается. - runtimeGain: Убирает целый класс UB при hot-swap: layout-несовместимость обнаруживается на загрузке, а не на разыменовании.
- compileTimeCost: Средняя — build.rs + proc-macro для сбора layout
- tradeoffs: Ложные срабатывания при refactoring (переименование приватного поля меняет хеш); нужна allowlist-политика.
- existingUsage: Fuchsia FIDL (ordinal hashing), Cap’n Proto schema evolution, некоторые embedded RTOS (закрытые), cbindgen + diff в CI у Parity/Wasmtime
- rustFit: Очень хорошо. Можно начать с
cbindgen+ текстового diff в CI, потом перейти на полноценный fingerprint черезsyn/proc-macro.
Append-only structs + size prefix
Section titled “Append-only structs + size prefix”- idea: Любая repr(C) структура имеет первое поле
struct_size: u32. Host при чтении смотрит struct_size и знает, какая версия layout пришла. Дальше — match по версии. Append-only правило: новые поля только в конец. - runtimeGain: Версионирование почти бесплатно; позволяет держать несколько версий DLL одновременно.
- compileTimeCost: Средняя — генерация кода
- tradeoffs: Не ловит изменение типа существующего поля; требует дисциплины ‘только append’.
- existingUsage: Windows Runtime (WinRT), некоторые plugin-системы; существует неофициальный паттерн в社区
- rustFit: Хорошо. Просто дисциплина + один
#[repr(C)]header.
Double-buffer DLL + frame barrier
Section titled “Double-buffer DLL + frame barrier”- idea: Host держит две загруженные версии DLL: current и next. На границе кадра, когда весь UI-thread code гарантированно не исполняет DLL-код (quiescent point), host переключает atomic pointer на next, а старую выгружает. Никакого EBR/HP — естественная quiescence от frame boundary.
- runtimeGain: Hot-swap за O(1) на границе кадра; никаких locks, никаких hazard pointers. Под frame budget 16ms помещается тривиально.
- compileTimeCost: Низкая — одна atomic-операция на фрейм
- tradeoffs: Требует, чтобы DLL не держала long-lived pointers в host state через кадровую границу; state serialization или внешнее хранение state (в host) обязательны.
- existingUsage: Hotline, Unreal live coding (через serialization), некоторые audio plugin hosts (VST3 reload), Emacs dynamic modules (через unload-safety protocol)
- rustFit: Отлично. Rust + libloading позволяет Library::drop, но нужно гарантировать, что нет outstanding references — frame barrier это гарантирует.
EBR / hazard pointers для unload-safety
Section titled “EBR / hazard pointers для unload-safety”- idea: Классический EBR (crossbeam-epoch) или hazard pointers: читатели регистрируют активные указатели, освобождение откладывается до тех пор, пока все читатели не выйдут из критической секции.
- runtimeGain: Позволяет hot-swap без явной границы кадра — unload когда безопасно.
- compileTimeCost: Высокая — unsafe обёртки, Send/Sync маркеры
- tradeoffs: Сложная memory model; crossbeam-epoch имеет известные subtleties; hazard pointers требуют bounded количество читателей.
- existingUsage: Folly hazard pointers (Meta), crossbeam-epoch, hyaline, seizе, — все для Rust; C++: folly, absl, libcds
- rustFit: Можно, но для Chroix — overkill. UI-фрейм имеет естественную точку quiescence каждые 16ms, и двойной буфер + frame barrier даёт те же гарантии за 1% сложности. Использовать EBR только если появятся фоновые треды, долгоживущие в DLL-коде.
Shared memory ring buffer через memmap
Section titled “Shared memory ring buffer через memmap”- idea: Создать file-mapping (CreateFileMapping/MapViewOfFile на Windows) между host и DLL как один большой arena. Host пишет display list / event queue туда, DLL читает без copy. Ring buffer поверх с head/tail в разных cache lines.
- runtimeGain: Zero-copy между host и DLL; на Windows это один syscall на setup, дальше — обычные load/store. Latency порядка наносекунд.
- compileTimeCost: Низкая — bipbuffer/shmem-ipc готовые
- tradeoffs: DLL и host делят одно адресное пространство фактически, но mapping может иметь разные базовые адреса — нужны относительные указатели (offset-based, не raw pointers). Это требует дисциплины.
- existingUsage: shared_memory crate, shmem-ipc, raw_sync, iceoryx (C++ but with Rust bindings), Chromium Mojo shared buffers
- rustFit: Хорошо. memmap2 + raw_sync (для shared mutexes) + bipbuffer (для ring). Всё есть на crates.io.
SPSC ring buffer (bipbuffer / bbqueue)
Section titled “SPSC ring buffer (bipbuffer / bbqueue)”- idea: Однопродюсер-одноконсьюмер lock-free ring buffer с bipartite layout (два contiguous сегмента для wrap-around). Producer пишет display list chunks, consumer читает.
- runtimeGain: Lock-free, wait-free для producer; один atomic load/store на операцию; ~5-10ns per op.
- compileTimeCost: Низкая — готовые крейты
- tradeoffs: Bounded size — нужно выбрать capacity; переполнение = drop frame или блокировка.
- existingUsage: bipbuffer crate, bbqueue (для embedded, но подходит), crossbeam-queue::ArrayQueue (bounded), rigtorp’s SPSCQueue (C++)
- rustFit: Идеально для UI pipeline: UI thread производит команды, render thread потребляет. bipbuffer покрывает 95% случаев.
Cache-line padding для hot atomics
Section titled “Cache-line padding для hot atomics”- idea: Все hot поля (head/tail ring buffer, signal dirty flags, atomic counters) оборачиваются в
CachePadded<T>(или ручной#[repr(align(64))]), чтобы не делить cache line с соседними полями. - runtimeGain: Убирает false sharing — может давать 2-10x на contended atomics. На 60fps петле это прямые микросекунды.
- compileTimeCost: Низкая
- tradeoffs: Трата 64-128 байт на поле; нужно применять осознанно, а не везде.
- existingUsage: crossbeam-utils::CachePadded — используется везде в crossbeam, tokio, rayon
- rustFit: Тривиально. crossbeam-utils::CachePadded уже стабилен.
Offset-based pointers в shared memory
Section titled “Offset-based pointers в shared memory”- idea: Все указатели в shared region хранить как u32/u64 offset от базы mapping. При доступе:
base + offset. Это делает layout position-independent и работает корректно, даже если host и DLL имеют mapping по разным адресам. - runtimeGain: Позволяет share memory между процессами/DLL с разными base addresses; упрощает serialization.
- compileTimeCost: Низкая
- tradeoffs: Каждый dereference — одна дополнительная аддитивная операция (компилятор обычно её сливает); типобезопасность страдает без обёрток.
- existingUsage: serde-arena, memmap2-backed stores в tantivy, rkyv (zero-copy deserialization)
- rustFit: Хорошо, но требует дисциплины: никаких raw pointers в repr(C) структурах, которые идут в shared memory. rkyv этот паттерн формализует.
Bump arena per frame
Section titled “Bump arena per frame”- idea: Выделять всю frame-specific data (display list, layout results, текст-шейпы) в bump arena. На конец кадра — одним memset или просто reset pointer. Никаких individual drop.
- runtimeGain: O(1) allocation (pointer bump), O(1) deallocation (reset). Убирает allocator pressure на hot path.
- compileTimeCost: Низкая — bumpalo готовый
- tradeoffs: Требует, чтобы DLL не хранила references в arena через кадровую границу; нужен явный контракт ‘frame-scoped’.
- existingUsage: bumpalo crate, typed-arena, rustc itself использует arena в queries, Bevy uses bump allocator для frame data
- rustFit: Идеально. bumpalo::Bump известен и проверен. Можно прокидывать через DLL boundary как raw pointer + capacity, потому что arena живёт в host.
libloading с pre-resolved symbols
Section titled “libloading с pre-resolved symbols”- idea: Стандартный libloading::Library для загрузки DLL, но все symbol resolutions кешируются на старте в struct of fn pointers. Никаких повторных GetProcAddress на hot path.
- runtimeGain: GetProcAddress — syscall-ish, дорого. Pre-resolve превращает это в один setup cost.
- compileTimeCost: Низкая
- tradeoffs: Никаких — это просто best practice.
- existingUsage: libloading::Library + Symbol, почти все Rust plugin-системы: bevy_dynamic_plugin, vizia, cargo-expand, mlua для Lua plugins
- rustFit: Идеально, уже baseline для nylon-ring.
abi_stable / stabby crates
Section titled “abi_stable / stabby crates”- idea: Крейты, которые дают stable ABI для Rust-типов (Vec, String, Option, Box) через FFI, версионируют vtables автоматически.
- runtimeGain: Позволяет шарить Rust-типы через DLL boundary без ручной repr(C) дисциплины.
- compileTimeCost: Высокая (тяжёлые макросы, свой runtime)
- tradeoffs: Vtables (конфликт с уже принятым решением), тяжёлые, compile-time дорого, своя экосистема.
- existingUsage: abi_stable использовался в sauron (web framework, abandoned), некоторых старых plugin-системах; stabby активно развивается (ZettaScale), но в production широкого применения не видно
- rustFit: Плохо для Chroix. Уже решено ‘link-time dispatch без vtables’ — abi_stable/stabby построены на vtables внутри. Плюс тянут runtime, заметный compile time, и не дают ничего, что ручная repr(C) дисциплина не даёт дешевле. НЕ брать.
NUMA-aware allocation (VirtualAlloc2 + preferred node)
Section titled “NUMA-aware allocation (VirtualAlloc2 + preferred node)”- idea: Привязка memory allocation к NUMA node через специальные API.
- runtimeGain: На multi-socket серверах — до 2x. На desktop (один socket) — ноль.
- compileTimeCost: Низкая
- tradeoffs: Усложняет allocator, на desktop бесполезен.
- existingUsage: Windows VirtualAlloc2/MapViewOfFileNuma2, но в desktop UI это почти никто не делает
- rustFit: Не нужен. Fera — desktop Telegram client, один socket гарантирован. Скип.
Thread affinity для producer/consumer
Section titled “Thread affinity для producer/consumer”- idea: Производящий тред (UI) и потребляющий (render) жёстко пинятся на разные физические cores через SetThreadAffinityMask. Гарантирует, что их cache lines не мигрируют.
- runtimeGain: На Windows даёт предсказуемость: меньше context switches, лучше locality. Измеримо на 144+ Hz дисплеях.
- compileTimeCost: Низкая
- tradeoffs: На гетерогенных (P/E cores Intel 12th+) требует знания топологии; на маленьких системах может навредить.
- existingUsage: rayon, tokio (work-stealing, не strict pinning), rigtorp libraries (lock-free, cache-pinned)
- rustFit: Просто. WinAPI через windows-rs, на старте приложения.
State serialization при hot-swap
Section titled “State serialization при hot-swap”- idea: Все долгоживущие state-объекты (signal graph, widget state) живут в host. DLL экспортирует чистые функции вида
fn render(state: &State, frame: &mut Frame). При hot-swap state не трогается, только code меняется. - runtimeGain: Hot-swap становится тривиальным: выгрузил code, загрузил code, state на месте.
- compileTimeCost: Высокая (сериализация state)
- tradeoffs: Требует дисциплины: никакого state в static переменных DLL (это ломает hot-swap); вся долгоживущая память — host-owned.
- existingUsage: Hotline использует именно это; Unreal live coding аналогично
- rustFit: Идеально с compiled signal graph: граф — это данные, код над ним — swappable. Согласуется с Elm-outside архитектурой.
Feature detection вместо version match
Section titled “Feature detection вместо version match”- idea: Host при старте вызывает
u32 get_abi_version()из DLL. Если major версия отличается — отказ. Если minor — работаем, но host знает, что часть features недоступна. Семантическое версионирование на уровне ABI. - runtimeGain: Позволяет постепенный rollout: новый host со старой DLL работает в degraded режиме.
- compileTimeCost: Низкая
- tradeoffs: Требует дисциплины в bump версий; не ловит layout-несовместимость (для этого — fingerprint).
- existingUsage: Obsidian, Unity native plugins, FlatBuffers versioning, Protobuf field numbers
- rustFit: Тривиально. Один extern fn + константы.
Notes: Браузер вёл себя нестабильно (редиректы на другие страницы, нестандартная среда), поэтому опирался на training data по crates.io/GitHub, который по этой теме (stable ABI, hot-swap, lock-free ring buffers, EBR/hazard pointers) достаточно полон и не требует свежих новостей.
Ключевые выводы для Chroix:
-
nylon-ring как fork baseline — ОК. Он уже flat repr(C), без vtables, host-сайд заполняет структуру вызовов. Его слабые места: (а) версионируется руками, (б) нет чёткой дисциплины layout при hot-swap, (в) нет shared-memory режима. Всё это дорабатывается локально — менять baseline не нужно.
-
Для hot-swap Chroix нужны три вещи, которых нет из коробки:
- Versioned struct envelope (
{ magic: u64, size: u32, abi_ver: u16, … }как в VulkansType/pNext) — все top-level структуры начинаются с этого, дальше append-only поля. Это единственный работающий паттерн, который используют Vulkan, Win32, COM, Obsidian, Unreal — все. - Double-buffer + quiesce barrier вместо полноценного EBR/HP. UI-фрейм имеет естественную точку quiescence — конец кадра. Хост ждёт конца текущего кадра, выгружает старую DLL. Это проще и предсказуемее любого lock-free реклеймера.
- Layout fingerprint через
build.rs, который хеширует layout всех repr(C) структур (имена, offsets, sizes) и встраивает в DLL как exported symbol. Host при LoadLibrary сверяет fingerprint — при расхождении грузит старую или откатывается. Убирает целый класс UB.
- Versioned struct envelope (
-
Что НЕ брать:
abi_stable— тяжёлый, тащит свою runtime, vtables внутри, конфликтует с уже решённым “link-time dispatch без vtables”. Использовался вsauron(но тот на него забил) и ряде старых plugin-систем. Для Chroix overkill.stabby— активно развивается (nightly-only repr), но подразумевает stable layout для Vec/String/Option через FFI. Хорош, если DLL-и компилируются разными версиями rustc. В Chroix обе стороны собираются одним toolchain — не нужен, и его generic-ABI это огромная compile-time цена.- Полноценный hazard pointers (как в folly) — для UI hot-swap overhead не оправдан, кадровый barrier дешевле.
-
Что брать сверху:
bipbufferилиshmem-ipcring buffers если host/DLL работают на разных тредах.cache-padded/crossbeam-utils::CachePadded— обязательно для всех hot counters/head/tail.cbindgenдля генерации C-header’ов из repr(C) типов — чтобы можно было вручную сверять layout между версиями в CI.- Паттерн “factory через extern fn, возвращающую fat struct of fn pointers” — как в
dll-syringe/pluginсистемах, но без vtable: обычный struct { draw: fn(…), layout: fn(…) } — это и есть ваш link-time dispatch.
-
NUMA — на Windows desktop UI можно смело игнорировать. Один сокет, мало ядер, latency разницы нет. Не усложняйте.
-
Cache-line awareness — да, критично: producer/consumer heads ring buffer на разных cache lines, arena region headers padded, hot signal state не шарит line со статикой. Это даёт измеримый выигрыш на 60fps петле.
-
Static pointers через
#[no_mangle] pub static— работает, но Windows DLL имеет свои .data/.bss секции и они не shared между host/DLL — то есть “static pointer в host” и “тот же static в DLL” это разные адреса. Использовать только как one-way: host экспортирует pointer, DLL его читает через GetProcAddress. Обратное — нет.
Смотри также: libloading (база для LoadLibrary), shared_memory crate (windows/file_mapping backend), memmap2 если пойдёте в mmap-backed ring.
GPU pipeline & frame scheduling
Section titled “GPU pipeline & frame scheduling”Multi-rate render lanes с per-lane deadline
Section titled “Multi-rate render lanes с per-lane deadline”- idea: Разделить контент на частотные полосы: stickers/video = 60-75fps, chat list = 45fps при активном скролле и 24fps в покое, chrome/typing indicator = 24fps. Каждый lane имеет свой deadline, свой dirty bit, и свой render pass. Frame scheduler комбинирует lanes в один present, пропуская lanes, чей dirty не взведён или чей интервал ещё не наступил. При пропуске — берём последний кэшированный layer этой полосы.
- runtimeGain: На 24fps-контенте (чтение чата) CPU/GPU load падает в 2.5-3× против 60fps everywhere. На комбинированной сцене (стикеры + текст) стикеры идут на 75fps, текст на 45 — экономия 40% кадров текста. Ключевое: при отсутствии изменений вообще — 0 кадров (idle = 0 GPU)
- compileTimeCost: Низкий: это runtime scheduler + приоритеты; единственный compile-time кусок — генерация lane-assignment из signal graph (макросы уже есть для графа)
- tradeoffs: Сложность композитора: нужно уметь composite N слоёв разной свежести в один кадр (Vello layers или wgpu texture blits). Риск jitter при пересечении lane boundaries (стикер 75fps поверх текста 24fps может визуально «плыть» при синхронной анимации). Требует disciplined invalidation — пропущенный dirty = застрявший пиксель.
- existingUsage: Chromium Compositor Scheduler (разные задачи на BeginImplFrame vs BeginMainFrame), Unity/Unreal render thread lanes, Bevy run criteria / FixedUpdate, Discord/Slack (Electron) — throttling фоновых каналов
- rustFit: Отлично: lane = enum + RingBuf последних layers; scheduler — маленький actor на host-стороне (за пределами fload DLL boundary, что важно: timing не пересекает dylib); dirty flags от compiled signal graph приходят как compile-time известные маски. Не требует repr(C).
Retained display list + damage tracking на уровне узлов
Section titled “Retained display list + damage tracking на уровне узлов”- idea: Display list снабжается stable IDs (из compiled signal graph — каждый узел знает свой stable key). На каждом кадре строим новый DL, сравниваем с предыдущим по IDs, получаем список изменённых AABB. Рендерим только damaged regions в offscreen, потом blit damaged rects в swapchain через dirty-rect present (DXGI_PRESENT_PARAMETERS на Windows, VK_EXT_swapchain_maintenance1, или Metal partial present).
- runtimeGain: Типовой чат-скролл трогает 10-30% экрана → 3-5× меньше fragment work. Idle cursor blink = один 20×20px rect → почти нулевой кадр. Особенно мощно на ноутбучных iGPU где fragment bandwidth — узкое место.
- compileTimeCost: Низкий: damage tracking это логика на display-list diffing; можно генерировать статически известные «damage classes» из сигнал-графа
- tradeoffs: Нужен persistent swapchain/backing store (flip model). На Linux Wayland damage — нативен, на Windows требует DXGI 1.4+, на macOS — IOSurface partial update. Vello как immediate-mode renderer не дружит с partial redraw из коробки: нужно либо перерендерить damaged region в отдельный texture и composite, либо форкать/оборачивать Vello scene builder. Сложность clipping: damage rect пересекающий transformed/clipped node требует conservative bbox.
- existingUsage: WebRender (Picture Cache + tile invalidation), Chromium CC (DamageTracker), Flutter Impeller (dirty region management), macOS Core Animation (setNeedsDisplayInRect), Windows DWM (dirty rects в flip model)
- rustFit: Идеально для Chroix: display list уже contract (plain data), diffing — чистая Rust-функция, testable headless. Stable IDs приходят из compiled signal graph — бесплатно. Dirty-rect present на wgpu требует либо raw-window-handle + platform code (Windows DXGI), либо wgpu-hal extension — но host-side, не в DLL.
Текстурный атлас для UI-растров (стикеры, эмодзи, аватары)
Section titled “Текстурный атлас для UI-растров (стикеры, эмодзи, аватары)”- idea: Все мелкие raster (иконки, эмодзи, аватары 64×64, стикерные кадры после CPU-raster) упаковываются в 2-4 больших атласа 2048². Кадр стикера после tlottie/velato CPU-raster пишется прямо в атлас-rect через texture write_region. Vello сэмплирует из атласа по UV. Атлас persistent между кадрами — перезаписываются только слоты изменившихся стикеров.
- runtimeGain: Один texture bind на весь UI вместо десятков → draw call collapse. Запись 512² кадра в атлас = 1 MiB write vs 1 MiB upload новой текстуры, но без аллокации. Стикерный loop-resident атлас убирает per-frame upload вообще.
- compileTimeCost: Средний: атлас-пакер на этапе build-time для статических ассетов (gen atlas в build.rs), runtime packer для динамики
- tradeoffs: Bleeding между соседними rect при linear filtering — нужен 1-2px padding. Fragmentation при динамических размерах. Атлас overflow → нужна политика eviction (LRU по стикерам). Vello 0.10 работает с images не идеально — может потребоваться кастомный image resolver.
- existingUsage: Google Skia (GrAtlasManager), Flutter engine, Chrome UI (градиенты/иконки в атласах), игровые UI (Dear ImGui single-font-atlas), Telegram Desktop (стикерные атласы), tlottie→bitmap→atlas паттерн в TDLib
- rustFit: Идеально: etagere / guillotiere crates для rectangle packing, wgpu queue.write_texture для update, Vello принимает images с UV. Может жить за fload boundary как часть renderer dylib (attlas management — внутреннее дело engine).
Vello Scene batching и reusable scene
Section titled “Vello Scene batching и reusable scene”- idea: Vello Scene собирается за один проход: все push_layer с одинаковым blend/clip идут подряд, glyphs батчатся в один glyph run, images ссылаются на атлас (см. атлас технику) — тогда один сэмплер. Критично: сортировать display list перед lowering в Scene так, чтобы state changes (shader/texture/blend) были монотонны. Reuse Scene через reset() и builder pattern (уже в fera-ui-engine-vello).
- runtimeGain: Vello уже эффективен, но state churn убивает: один кадр с 50 отдельными image textures может дать 50 texture binds vs 1 при атласе. Reusable Scene убирает аллокации на кадр (~50-100µs экономии на большом DL).
- compileTimeCost: Низкий-средний: паттерны батчинга в коде, не в типах
- tradeoffs: Сортировка DL перед rendering нарушает painter’s order если делать наивно — нужно stable sort по (z, blend, texture) и сохранять back-to-front. Vello internals уже батчат path encoding, выигрыш в основном на image/text state changes. Over-engineering риск: профилировать до оптимизации.
- existingUsage: Vello (coarse path rasterizer сам батчит по encoding), WebRender (batching по shader/texture в same render pass), Flutter Impeller (entity pass batching), SDF text (один draw на весь текст), Bevy (render phase batching)
- rustFit: Отлично: vello::Scene имеет reset(), kurbo paths дёшево клонируются через Arc. Lowering проходит один раз за кадр — совместимо с 24/45/60-75 lanes.
Async text shaping pre-pass на worker pool
Section titled “Async text shaping pre-pass на worker pool”- idea: Parley shaping для видимых+adjacent сообщений запускается в rayon pool параллельно с main thread Vello Scene build предыдущего кадра. Результат — готовый parley::Layout, кэшированный по (text hash, width bucket, font version). При следующем frame layout уже готов. Для message list: shape-ahead на 1 экран вверх/вниз от viewport.
- runtimeGain: Text shaping — 20-40% кадрового времени в текстовом UI. Вынос в async даёт почти zero-cost текст на main thread (только glyph upload). Для длинного чата: shaping 50 сообщений параллельно на 4 cores vs последовательно = 3-4× ускорение scroll-into-view.
- compileTimeCost: Низкий: это scheduling
- tradeoffs: Speculative shaping тратит CPU на контент, который может не показаться (overscroll). Font fallback resolution усложняет кэш-key. Нужна отмена in-flight shaping при быстром скролле (epoch/generation counters).
- existingUsage: Chromium (font shaping в worker threads), Servo (parallel layout), Flutter (text layout на UI thread но async raster), Cosmic-text (shaping cache), Parley (LayoutContext reuse)
- rustFit: Отлично: Parley Layout Send (после завершения shaping), кэш — обычный RwLock
. Совместимо с Elm-outside: shaping это не view-функция, а подготовка ресурса. fload boundary не мешает — shaping может жить в отдельном модуле.
Frame caching + re-present без re-render (layer cache)
Section titled “Frame caching + re-present без re-render (layer cache)”- idea: Статичные полосы (chrome, sidebar, список чатов в покое) рендерятся в собственные texture layers один раз, далее каждый кадр — только composite этих textures + re-render только активных lanes (стикеры). Composite — дешёвый fullscreen textured quad. При dirty в статичной полосе — re-render только её texture, не всего кадра.
- runtimeGain: Idle чат с мигающим курсором: 1% GPU против full re-render. Scroll на 45fps: message list layer кэширован, рендерится только viewport delta. Battery: GPU sleep между композитами.
- compileTimeCost: Низкий
- tradeoffs: VRAM pressure: 4K fullscreen RGBA layer = 33 MiB; 3-4 layers = 100+ MiB. Resize окна инвалидирует все layers. Transform/animation на layer требует re-render или GPU-side transform (усложняет композитор).
- existingUsage: WebRender (tile cache: статичные страницы = re-composite cached tiles), Flutter RasterCache, macOS Core Animation (layer backing stores), game UI (menu screens), Android SurfaceFlinger (переиспользование буферов)
- rustFit: Отлично: wgpu Texture для каждого layer, composite pass — минимальный render pass. Vello рендерит в texture через render_to_texture. Layer tree совместим с display list contract.
Power management: background throttling + lane pause
Section titled “Power management: background throttling + lane pause”- idea: Visibility-driven lane gating: unfocused window → все lanes кроме critical (badge, звук) на 0 fps. Minimized → render thread parked. Per-signal energy class: compiled signal graph помечает каждый сигнал как Critical/Normal/Cosmetic; в power-save Cosmetic lanes принудительно на 12fps или off. Стикерные циклы останавливаются если sticker не в viewport (IntersectionObserver-style).
- runtimeGain: Background window: 0% GPU vs passive 3-8%. Laptop battery: 30-60 мин экранного времени. Cosmetic-off mode: -20-30% GPU на слабом железе.
- compileTimeCost: Низкий
- tradeoffs: Resume latency: возврат из deep idle должен быть <100ms чтобы не чувствовался. Неправильная классификация сигнала (Cosmetic на самом деле важен) = UI bugs. Per-platform различия в suspend semantics.
- existingUsage: Chromium (background tab throttling до 1% CPU), Android (Choreographer frame callback gating), iOS (CADisplayLink pause в background), Discord (background suppression), Telegram Desktop (останавливает sticker loops при unfocus)
- rustFit: Идеально: winit события Focused/Suspended → host scheduler → lane enable mask. Cosmetic классы — compile-time из signal graph (макросы могут генерировать энергетический класс из типа сигнала).
Present mode policy: Mailbox vs FIFO + adaptive switching
Section titled “Present mode policy: Mailbox vs FIFO + adaptive switching”- idea: Mailbox (aka triple buffering) как дефолт для активного скролла/анимаций: swapchain держит 3 буфера, present никогда не блокирует, старый кадр молча дропается. Fifo (vsync-strict) для статичного контента — меньше power. Adaptive: при frame time <80% budget → Mailbox, иначе → Fifo для плавности. FifoRelaxed на Windows для tearing-tolerant сцен.
- runtimeGain: Mailbox при скролле: input-to-photon latency падает на 8-16ms (1 кадр) против Fifo. Fifo при idle: GPU downclock раньше → power. Правильный режим убирает очередь кадров (queue buildup = lag).
- compileTimeCost: Низкий
- tradeoffs: Mailbox тратит GPU на кадры, которые не покажутся (drpoed). На некоторых Android драйверах Mailbox энергозатратнее. Adaptive logic добавляет hysteresis complexity. FifoRelaxed не везде поддерживается.
- existingUsage: wgpu::PresentMode::{Fifo, Mailbox, Immediate, FifoRelaxed}, DXGI flip model, Android SurfaceFlinger, X11 Present extension, все game engines
- rustFit: Идеально: wgpu PresentMode — один enum, host-side выбор. Динамическое переключение — reconfigure surface. Не пересекает fload boundary.
Triple buffering и временной pipeline
Section titled “Triple buffering и временной pipeline”- idea: 3 swapchain images при 60-75fps, 2 при 24/45 (экономия VRAM). Кадр N рендерится в то время, как N-1 present, N-2 в очереди. С careful synchronization: acquire next image → если не готова за 2ms — render в кэшированный frame и skip (не блокируем main thread).
- runtimeGain: CPU не ждёт GPU: submit сразу после render, не ждём present. При frame time spikes один плохой кадр не каскадится. 24fps lane с 2 буферами: -33 MiB VRAM.
- compileTimeCost: Низкий
- tradeoffs: Третий буфер = +33 MiB на 4K. Acquire timeout fallback требует повторного использования предыдущего кадра (нужен retained framebuffer). На Windows DXGI flip model уже почти это делает под капотом.
- existingUsage: wgpu Surface configuration, DXGI_SWAP_EFFECT_FLIP_DISCARD, Vulkan mailbox present mode, Metal drawable pool
- rustFit: Отлично: wgpu управляет image count через desired_maximum_frame_latency. acquire_timeout паттерн на host side.
Async compute / offload non-graphics work
Section titled “Async compute / offload non-graphics work”- idea: Vello уже использует compute для path flattening — расширить: glyph atlas upload, image decode (JPEG→texture через compute), damage-region clear, atlas compaction — всё на compute queue параллельно graphics queue. wgpu позволяет submit command buffers на разных queues (если device exposes).
- runtimeGain: Image decode и upload не блокируют render pass. На iGPU с shared queues — мал; на дискретных с отдельными compute queues — 10-20% frame time. Главное: убирает long render pass stalls.
- compileTimeCost: Низкий
- tradeoffs: wgpu multi-queue support ограничен — на практике всё равно одна queue семья; выигрыш скорее от better packing command buffers. Сложность синхронизации (barriers между compute и graphics). На слабом железе overhead может превысить gain.
- existingUsage: wgpu compute pipelines, Vello (path coarse raster уже compute), Unreal Nanite, WebGPU async compute proposals
- rustFit: Хорошо: wgpu ComputePass API прямой, Vello already compute-heavy. Но multi-queue в wgpu пока ограничен (одна queue семья на device обычно).
Headless golden-frame backend параллельно Vello
Section titled “Headless golden-frame backend параллельно Vello”- idea: chroix-headless (уже запланирован) используется не только для тестов, но как production fallback: при слабом GPU/remote desktop/battery saver — рендер в CPU buffer через vello_cpu, композит через sw blit. Тот же display list, тот же damage tracking, другой backend.
- runtimeGain: Не runtime gain, а testability: каждая оптимизация (damage, lanes, caching) проверяется golden frames без GPU флакиности. CI ловит регрессии damage tracking пиксель-в-пиксель. Также coverage для vello_cpu fallback path.
- compileTimeCost: Низкий
- tradeoffs: Дублирование тестовых сценариев если API расходится. vello_cpu performance не отражает GPU reality — нужны оба бенчмарка.
- existingUsage: Vello (render_to_image API), Flutter golden tests, WebRender wrench reftests, Bevy headless rendering
- rustFit: Идеально: vello_cpu 0.2 уже собран и работает (по fera-ui-stack). Тот же Engine trait из ui-contract.
Tile-based rendering для message list (WebRender-style)
Section titled “Tile-based rendering для message list (WebRender-style)”- idea: Message list — это вертикальный strip из tiles 256×256. Viewport + 1 tile margin рендерятся в кэш; скролл = сдвиг UV + re-render только вновь вошедших tiles. Каждый tile имеет dirty flag от display list diff. Рендер tile в offscreen atlas-like texture.
- runtimeGain: Scroll: 90% экрана — cached tiles, re-render 1-2 новых. Memory: 4K list = не 33 MiB fullscreen, а 8-10 tiles = 5-8 MiB. Damage локализован: изменение одного сообщения = 1 tile, не full list.
- compileTimeCost: Низкий
- tradeoffs: Tile boundary artifacts при transforms/clip. Overscroll/rapidl scroll требует быстрого re-render — нужен приоритет видимых tiles. Sticky headers/avatars усложняют tile mapping.
- existingUsage: Chromium GPU raster + tiles, WebRender PictureCacheTile, Flutter Impeller, Картографические движки (map tiles)
- rustFit: Хорошо: tile indexing — plain math, Vello рендерит в texture, LRU eviction — стандартный. Совместимо с display list (tile = subtree).
VRAM-resident loop frames для стикеров
Section titled “VRAM-resident loop frames для стикеров”- idea: Стикерные 90 кадров в VRAM как atlas-slot на весь цикл (см. атлас технику): tlottie/velato raster каждого кадра однажды → все 90 frames в атласе → стикер играет бесконечно без CPU. Для 50 стикеров = 50×22.5MiB@256² = 1.1 GiB VRAM — квотируется (топ-N видимых).
- runtimeGain: Steady-state stickers: 0% CPU после загрузки. Compare с tlottie live decode: 50 стикеров × 30fps × 1 MiB decode = 1.5 GB/s CPU → 0.
- compileTimeCost: Средний: retention policy — дополнительная логика
- tradeoffs: 1.1 GiB VRAM на стресс-сцене — не влезает в слабые iGPU (512 MiB). 256² экономит 4× но качество страдает на Retina. Eviction + re-raster при cache miss = jitter. Нужна политика: loop-resident только для on-screen + recently-seen.
- existingUsage: Game engines (sticker/animation loops), Telegram Desktop (sticker loop), Discord (emoji loops)
- rustFit: Хорошо: интегрируется с атласом. Velato (Vello+Lottie) может рендерить напрямую в GPU texture. Квота eviction — host-side policy.
Display list как glyph-run + image-ref format
Section titled “Display list как glyph-run + image-ref format”- idea: Parley даёт shaped glyphs → они хранятся как (glyph_id, x, y) runs, а не pixels. Glyph atlas (single texture) обновляется инкрементально: новый glyph = rasterize один раз, добавить в atlas. Кадр рендерит text как instanced quads из atlas. Damage на тексте = перегенерация runs, но glyphs уже в atlas.
- runtimeGain: Text rendering без shaping в steady state: один glyph уже в atlas = free. Scroll текста = re-use glyph runs, только transform. Типичный чат: 95% glyphs уже в atlas после первого экрана.
- compileTimeCost: Низкий: формат данных, не код
- tradeoffs: Glyph atlas overflow на CJK (нужны большие atlas или eviction). Sub-pixel positioning усложняет cache (разные subpixel offsets = разные glyphs). Font fallback → cache invalidation.
- existingUsage: GPUI (text runs как positioned glyph IDs), WebRender (glyph cache), Flutter, Photoshop
- rustFit: Идеально: Parley даёт glyph positions, swash/ cosmic-text rasterizes glyphs в atlas. Vello имеет glyph support. Совместимо с display list contract.
Notes: Привязка к уже решённому в Chroix/Fera: (1) compiled signal graph + Elm-outside означает, что damage information приходит из графа бесплатно — не нужен runtime read-tracking ради dirty-rect, сигналы знают что поменялось; используйте это как вход для damage tracking, не повторяйте Salsa. (2) Display list как backend contract = идеальное место для retained scene: display list уже сериализуемая структура, её же можно диффить. (3) Vello+headless пара: damage/dirty-rect логика тестируется на headless без GPU. (4) fload DLL boundary: renderer в DLL, поэтому frame pacing/scheduling — на host-стороне (winit event loop), а не внутри dylib; dylib отдаёт готовый Scene/текстуры, host решает когда present. (5) Frame budgets 24/45/60-75 из Fera QA напрямую мапятся на multi-rate lanes. (6) Уже существующий код: fera-ui-engine-vello делает reusable scene reset per frame — это первая ступень к retained scene, но сейчас full-rebuild. Ключевые проекты для кражи идей: WebRender (tile cache, picture cache), Chromium CC (damage, scheduler), Flutter (raster cache, partial repaint в Impeller), Cosmic-text/Parley для shaping cache, Bevy (multi-rate), wgpu::PresentMode, DXGI_SWAP_EFFECT_FLIP_DISCARD + dirty rects, Velato для Lottie→Vello, tlottie как CPU-raster источник битмапов в атлас.
Memory layout & allocation
Section titled “Memory layout & allocation”Per-frame bump arena (bumpalo, phases reset)
Section titled “Per-frame bump arena (bumpalo, phases reset)”- idea: Одна arena на кадр (или на фазу: build / layout / paint), в неё скидываются все временные ноды, display-list-команды, промежуточные строки; в конце кадра один
reset()вместо N free. В Chroix — три арены под три бюджета (24/45/60–75 fps), живут на стороне host за fload-границей. - runtimeGain: Убирает ~весь alloc/free трафик hot path; идеальная локальность (все данные кадра — один contiguous chunk, prefetch-friendly); O(1) деаллокация всего кадра.
- compileTimeCost: Низкая — bumpalo маленький, без макросов-монстров.
- tradeoffs: Нет per-object free, нет Drop для содержимого (только Copy/plain data или ручной boxed::Box); при переполнении chunk растёт — пик памяти = худший кадр; нужна дисциплина «не тащить &’_ за пределы кадра».
- existingUsage: bumpalo; bumpalo-herd для per-thread пулов; Vello (vello::Scene + Encoding reset per frame); Dioxus templates.
- rustFit: Отлично.
Bump!Sync, поэтому по одной на поток или bumpalo-herd; лайфтаймы естественно выражают «не переживает кадр». Важно: арена живёт в host, плагину отдаётся&mut [u8]/SceneBuilder — host-leased buffers, как уже принято в nylon-ring/fload.
Flat node pool с generational indices (slotmap)
Section titled “Flat node pool с generational indices (slotmap)”- idea: UI-дерево =
Vec<Node>+ u32 индексы (parent/first_child/next_sibling) вместо Box/Rc. Для списков с виртуализацией (чат Telegram) — generational indices (slotmap) чтобы безопасно переиспользовать слоты без ABA. - runtimeGain: Нет pointer chasing и allocator pressure; обход дерева — последовательный проход по памяти; стабильные NodeId для display-list contract и для headless-тестов.
- compileTimeCost: Низкая; slotmap — маленький крейт.
- tradeoffs: Две косвенности на lookup (index → slot); удаление не мгновенно освобождает память (freelist); generational check — лишняя ветка на каждый доступ.
- existingUsage: slotmap; slab; Taffy (TaffyTree поверх slotmap-like storage); AccessKit TreeUpdate; egui Memory.
- rustFit: Отлично; NodeId = Copy-тип, безопасно передаётся через fload как plain data (не repr(C) структура, а u64 key).
AoS для дерева, SoA для горячих проходов (hybrid)
Section titled “AoS для дерева, SoA для горячих проходов (hybrid)”- idea: Само дерево — AoS (Node ~64–128 B: style-id, layout-cache, flags, child range). Для горячих SIMD-проходов (measure, cull, paint-order) на кадр строится SoA-scratch (отдельные колонки x/y/w/h/flags) в той же bump-арене. Не хранить SoA постоянно — генерить per frame.
- runtimeGain: AoS даёт нормальный random access при редактировании; SoA даёт векторизацию и плотный streaming на layout/paint, где обходятся тысячи нод.
- compileTimeCost: Средняя — гибрид требует двух представлений + код перегона; const-generics helpers могут раздуть мономорфизацию.
- tradeoffs: Двойная запись при изменении; код сложнее; на маленьких UI (<1k нод) SoA не окупается — порог включения по размеру дерева.
- existingUsage: Bevy ECS (SoA archetypes + sparse sets); Servo/Stylo; morphorm.
- rustFit: Хорошо; SoA-scratch в bump-арене — идиоматично; SoA-структуры держать внутри engine, не выставлять наружу.
Interning строк и стилей (Symbol = u32)
Section titled “Interning строк и стилей (Symbol = u32)”- idea: Все строки (class names, style keys, accessibility labels, text runs) и style-блоки интернируются один раз при загрузке темы/компиляции view, дальше по коду ходит u32 Symbol. Сравнение стилей = сравнение u32; hash map lookups исчезают из per-frame.
- runtimeGain: O(1) сравнение/хеширование идентификаторов; дедупликация — одинаковые стили хранятся один раз; computed-style cache становится
Vec<ComputedStyle>индексируемый Symbol’ом. - compileTimeCost: Низкая; lasso/string-interner маленькие, тяжёлый макро-код не нужен.
- tradeoffs: Интернер растёт монотонно (или нужен GC по epoch); дополнительный lookup на горячем пути если текст приходит извне каждый кадр — тогда интернировать только ключи стилей, а не контент.
- existingUsage: lasso; string-interner; hashbrown raw-entry; Salsa interner pattern; egui Id; GPUI ElementId; Parley (font cache).
- rustFit: Отлично; Symbol — Copy + Send + Sync, свободно пересекает fload-границу. Не дублировать то, что уже интернит Parley/Skia (glyph runs, fonts).
Zero-allocation hot path (smallvec/arrayvec + reuse scratch)
Section titled “Zero-allocation hot path (smallvec/arrayvec + reuse scratch)”- idea: Все временные Vec/HashMap/String в build/layout/paint — либо bumpalo::collections, либо SmallVec
на стеке (children до N, style overrides до M), либо переиспользуемые scratch-буферы в Engine (double-buffered). Цель: 0 аллокаций на steady-state кадр. - runtimeGain: Убирает jitter от аллокатора — критично для 24 fps бюджета; предсказуемый worst-case frame time.
- compileTimeCost: Низкая; smallvec/arrayvec/tinystr — tiny crates.
- tradeoffs: Требует дисциплины и аудита (любой
.to_string()/format!в hot path — регрессия); smallvec при переполнении молча уходит в heap — нужны debug-asserts на capacity. - existingUsage: smallvec; arrayvec; tinystr; heapless; Vello (smallvec для path segments); Parley.
- rustFit: Отлично; естественно сочетается с compiled signal graph — раз граф статический, размеры scratch можно оценить на этапе компиляции view.
Copy-on-write / слайс-строки через индексы (Rope-lite)
Section titled “Copy-on-write / слайс-строки через индексы (Rope-lite)”- idea: Строки не String, а
StrSpan { buf: ArenaPtr/StaticPtr, start: u32, len: u32 }— substring, concat, trim — только манипуляции индексами без копирования байт. Для статического текста (лейблы из шаблонов) —&'static strнапрямую; для динамического — один большой text-blob на кадр в арене. - runtimeGain: Сборка текста кадра без memcpy; trim/slice — O(1); меньше churn в allocator.
- compileTimeCost: Низкая; copypasta/extent — обычные структуры.
- tradeoffs: Указатель на арену живёт не дольше кадра — нельзя сохранить StrSpan между кадрами; сложнее отлаживать; concat через индексы требует отдельного build pass.
- rustFit: Хорошо, но осторожно: полноценный COW-subtree для UI — ловушка (atomic Rc traffic + промахи кэша). Применять только к строкам и к доказуемо-иммутабельным static template fragments, которые compiled graph может выделить как const.
Memory-mapped theme/style blob (zero-deserialize)
Section titled “Memory-mapped theme/style blob (zero-deserialize)”- idea: Тема/стили компилируются в flat binary blob (rkyv или capnp-like), mmap’ится read-only; чтение стиля = dereference указателя в mmap, без парсинга и аллокаций на старте. Холодные страницы подгружаются ОС по требованию.
- runtimeGain: Мгновенный запуск UI (нет парсинга темы); нулевой RSS на неиспользуемые темы; share между процессами/окнами.
- compileTimeCost: Средняя — rkyv/capnp имеют свои proc-macro/схемы; но это один раз для формата темы.
- tradeoffs: Нужен offline-компилятор темы; версионирование формата; unsafe при чтении (rkyv archive access); правка темы = пересборка blob, не hot-edit без доп. слоя.
- existingUsage: rkyv (zero-copy deserialize); capnp; memmap2; паттерн из game assets.
- rustFit: Умеренно. Конфликт с принципом fload «no repr(C)» решается тем, что blob — opaque bytes на границе, а типизированный доступ остаётся внутри engine. Требует отдельного compile-step для темы.
Reuse pools + double-buffered layout (prev/cur)
Section titled “Reuse pools + double-buffered layout (prev/cur)”- idea: Две bump-арены: в кадр N пишем в A, читаем из B (предыдущий layout для dirty-check и hit-test); в конце кадра swap. Удалённая нода не free’ится — её слот уходит во freelist и переиспользуется на следующем кадре.
- runtimeGain: Dirty-checking против предыдущего кадра — сравнение двух плоских Vec, а не обход с refcount; никаких free-list-локов; стабильные адреса внутри кадра.
- compileTimeCost: Низкая.
- tradeoffs: Пик памяти = 2× дерева; dirty-check всё ещё O(tree) (хоть и дешёвый); нужен явный протокол «кто владеет cur/prev».
- existingUsage: Dioxus (double-buffered templates); Vello encoding reuse; Parley Layout cache.
- rustFit: Отлично для compiled signal graph: раз граф статический, двойной буфер выделяется один раз под известный max-размер. Ложится на 24/45/60–75 fps бюджеты — разные пулы на разные частоты.
Custom global allocator + segregated heaps
Section titled “Custom global allocator + segregated heaps”- idea: Поверх bump-арен — кастомный global allocator (mimalloc с настройкой arena per thread) или отдельный slab для «долгоживущих» объектов (шрифты, glyph cache, decoded images), чтобы они не фрагментировали общий heap и не смешивались с per-frame churn.
- runtimeGain: Меньше contention и фрагментации; предсказуемый p99 frame time; long-lived данные не мешают hot path.
- compileTimeCost: Низкая.
- tradeoffs: Один allocator на процесс — конфликтует с hot-swappable модулями, если они тоже хотят свой; нужно дисциплинировать плагины не тащить свой allocator через DLL-границу.
- existingUsage: mimalloc; jemalloc; паттерн из OPTIM-IDEAS-3-research (plugins slab); Blender/Chrome partition alloc.
- rustFit: Хорошо;
#[global_allocator]— стабильный механизм; но только один на процесс — если fload-плагины тоже линкуют свой, конфликт. Держать allocator в host, плагины аллоцируют через host-API.
Structural sharing для иммутабельных фрагментов (COW done right)
Section titled “Structural sharing для иммутабельных фрагментов (COW done right)”- idea: Для больших read-mostly поддеревьев (список чатов, статический sidebar) — persistent структура (im-rc::Vector / rpds), где «изменение» = новый root с structural sharing вместо копирования. Использовать только там, где subtree реально разделяется между кадрами и редко мутирует.
- runtimeGain: Update большого списка без копирования; cheap snapshot для undo/diff.
- compileTimeCost: Низкая.
- tradeoffs: Rc/Arc трафик и cache-miss на pointer-chase — ровно то, от чего уходят flat Vec. Не применять в hot path paint/layout; применять только на границе данных (chat list model), не на границе виджетов.
- existingUsage: im-rc; rpds; Jetpack Compose (persistent snapshots); Elm.
- rustFit: Средне. В Chroix с compiled signal graph большая часть дерева статична — persistent слой нужен только на границе «реактивный список». Сочетать с interning — ключи фрагментов как Symbol.
Notes: Territory: memory layout & arena allocation for Chroix. Verified crates via crates.io (Playwright, 2026-10-03): bumpalo v3.20.3 current, features collections/boxed/allocator-api2 (stable shim over nightly Allocator), bumpalo-herd for multi-thread pools. Real-engine precedents cross-checked from first-hand knowledge + prior verified Fera-UI-stack notes (memory file fera-ui-stack.md): Vello (wgpu display list + per-frame vello::Scene bump), Parley (Layout reuse + font/skia-text interning), Taffy (taffy::tree::TaffyTree over slotmap), morphorm (flat ECS-ish storage), egui (Id interner + Memory retained pools), GPUI (App arena + ElementId), AccessKit (flat TreeUpdate nodes vec + NodeId), Bevy ECS (SoA archetypes + sparse sets), Clay (C, pure arena + integer ids), Yoga (leaf-node pooling), Stylo/Blink (style interning + computed-style cache), Dioxus (bumpalo per-template), Salsa/interner pattern, string-interner/lasso/hashbrown/smallvec/arrayvec/tinystr as concretes. Cross-cutting notes: (1) bump arena per FRAME (build/layout/paint) is the canonical UI pattern — allocate the whole frame, reset once, zero per-node free; Chroix’s compiled signal graph makes this easier because there is no runtime subscription bookkeeping that would need long-lived heap state. (2) Flat Vecbumpalo::Bump and node pools in the host, hand plugins &mut [u8]/SceneBuilder (host-leased buffers, matching nylon-ring pattern already noted in conversation.md:963). Compile-time cost across the board is low-to-moderate (arena/pool generics are cheap; heavy const-generics in SoA helpers are the main offender); none of this conflicts with LTO/PGO/BOLT/codegen-units=1.
Diffing, caching & invalidation
Section titled “Diffing, caching & invalidation”Salsa-style incremental computation
Section titled “Salsa-style incremental computation”- idea: Каждый узел в compiled signal graph — это memoized query с явными входами. Изменился вход → инвалидируются только зависимые запросы. Red-цикл: Q(x) → Q(y) → Q(z) — при set(x) пересчитывается только цепочка. Salsa 2022+ даёт durability и interning, что критично для identity (один и тот же String ‘hello’ = один interned id, дифф сводится к сравнению u32).
- runtimeGain: Стабильный 60fps при типичных правках: пересчёт O(изменённые узлы), а не O(весь UI). rust-analyzer держит ~5-15ms на re-analysis файла в 10k строк — это порядок, который нужен Chroix для hot path перерисовки. Также бесплатный snapshotting (Salsa::snapshot) для time-travel и отладки.
- compileTimeCost: Низкий. Salsa — стабильный crate, публичный API макросами. Мономорфизация растёт линейно от числа запросов; одна signal-нода = одна query. Заметных скачков compile time нет.
- tradeoffs: Salsa не магия — нужно правильно грануляризовать запросы (слишком крупные → сплошная инвалидация, слишком мелкие → overhead на tracking). Требует дисциплины ‘чистых’ функций — сайд-эффекты в query ломают memo. Громоздкий код при глубоких иерархиях.
- existingUsage: salsa-rs/salsa (rust-analyzer — флагманский пользователь, обрабатывает терабайты логических изменений при живом редактировании), vercel/turbo (Turborepo вдохновлён), magmide, facet. Для UI: cranky (https://crankycoder.com), эскизы в Bevy community crates.
- rustFit: Отлично. С Salsa граф зависимостей — compile-time типизирован, никакого runtime tracking. Естественно ложится на Elm-outside (state → query → view). Для fload boundary: interned ids передаются как u32, никаких Rust-типов через DLL. Salsa живёт на host-side, в DLL уходит уже готовый display list.
Structural sharing через im-rc / rpds
Section titled “Structural sharing через im-rc / rpds”- idea: Persistent B-tree vector (bitmapped vector trie, как в Clojure) для message list и любых больших списков. Ссылка на хвост O(1), вставка/удаление O(log32 n) — фактически константа. При изменении одного элемента shared-структура: unmodifiable prefix/suffix переиспользуется без копирования. На фронте diffing сводится к pointer-compare поддеревьев.
- runtimeGain: Вставка 1 сообщения в 100k-список — O(log32 100k) ≈ 3-4 копии узла, а не realloc всего Vec. Diffing сообщения на экране: pointer compare поддеревьев — miss = только изменённая ветка. Кэш локальности хуже чем Vec, но полностью компенсируется отсутствием полного rebuild.
- compileTimeCost: Низкий. im-rc — чистая структурная библиотека, типизированная мономорфизация на типах элементов. Добавляет ~10-20s к чистой сборке проекта.
- tradeoffs: Хуже cache locality, чем Vec. Iteration медленнее ~1.5-2x. Rc/Arc overhead на каждый узел. Не подходит для горячих малых коллекций (<1000 элементов) — Vec + SlotMap выигрывает.
- existingUsage: im-rc, rpds. В UI: Druid использовал im::Vector для больших списков до переписывания на Xilem; Yew имеет persistent list helper. Вне Rust: PersistentVector в Clojure, Immutable.js, React + Immer.
- rustFit: Хорошо для message list, плохо как универсальный контейнер. im-rc даёт Rc-подсчёт ссылок — это single-thread, что идеально для UI-потока. Для fload: передаём иммутабельный snapshot целиком, либо Arcim::Vector для zero-copy. Не смешивать с Salsa-интернированием на одном и том же уровне — одна система identity.
SlotMap + generational IDs для UI-узлов
Section titled “SlotMap + generational IDs для UI-узлов”- idea: Все UI-узлы — это generational index (u32 id + u32 generation), хранящиеся в SlotMap. Удалил узел → generation инкрементируется, старые id становятся stale и безопасно отвергаются. Диффинг двух деревьев: hash по id — разный id = разный узел, одинаковый id + одинаковая generation = один и тот же узел, рекурсия по потомкам не нужна для удалённых веток.
- runtimeGain: O(1) вставка/удаление, отсутствие дырок в памяти (dense storage), отличная cache locality при итерации. Диффинг поддерева = сравнение одного u64 (id+generation) — если совпал, рекурсия вниз не нужна. Это даёт быстрый bail-out на неизменённых subtree, что критично для больших списков.
- compileTimeCost: Низкий. slotmap — маленькая библиотека (~2k LOC). Макрос new_key_type! генерирует типы; compile time почти не растёт.
- tradeoffs: Generation — это не значение, которое можно сравнивать между независимыми снапшотами (разные снапшоты = разные SlotMap). Нужно явно версионировать snapshot’ы. Не решает проблему reordering внутри списка — для этого нужен отдельный механизм keyed diff.
- existingUsage: slotmap crate. Bevy использует для Entity. Roblox Lua-адаптация. Servo для DOM nodes (аналогичная идея через arena).
- rustFit: Отлично. SlotMap — чистый safe Rust без unsafe. Id передаётся через fload boundary как u64 — никаких указателей. Salsa interning и SlotMap ID можно слить в одну систему identity: query key = NodeId.
Keyed diffing для message list (Solid/Vue3-style)
Section titled “Keyed diffing для message list (Solid/Vue3-style)”- idea: Для списка сообщений keyed diff через ‘two-ended scan’ + LIS (Longest Increasing Subsequence) на хвосте. Алгоритм: скан с обоих концов пока совпадает, потом map key→index, визиты по map, для оставшихся — LIS даёт минимальное число moves. Это то, что использует Solid/Vue3.
- runtimeGain: Перестановка 1000 элементов — O(n) в типичном случае (добавление в конец/начало) и O(n log n) в худшем. Практически: 100k-список с 1 вставкой — ~200ns на diff. Для message list в Telegram-клиенте: scrollback редко reorder’ится, типичный случай — prepend/append, что обрабатывается за O(k) где k = число изменённых.
- compileTimeCost: Низкий-средний. Алгоритмическая сложность в коде, но без тяжёлых макросов. Своя реализация на ~200 строк — быстрее компилируется, чем similar.
- tradeoffs: LIS — дополнительная сложность. При большом числе reordering патология может выродиться в O(n²). Нужен fallback: если reordering >30%, просто rebuild. Для очень коротких списков (<32) простой O(n²) brute force быстрее из-за константы.
- existingUsage: Peterson-Miller-Myers в React (keyed reconciliation), Solid.js (mapArray), Lit-html repeat. В Rust: similar (diff для строк), imara-diff, keyed-list в Dioxus.
- rustFit: Отлично. Алгоритм компактный, полностью safe, работает на &[K] с K: Hash+Eq+Copy. Совместим с persistent vector: diff работает поверх im::Vector через итератор. Для fload: отдаём host-стороне готовый список patch-операций (InsertAt/RemoveAt/MoveTo), а не сами ключи.
Dirty region tracking через bitset + damage rects
Section titled “Dirty region tracking через bitset + damage rects”- idea: На каждый layout/display-pass заводим bitset размером в число узлов. Layout-фаза помечает dirty бит на узле и всех предках. Display-фаза делает AND с битовой маской visible-region — пересчёт только dirty ∩ visible. Bitset компактный (8К узлов = 1KB) и сканируется SIMD (u64::trailing_zeros в цикле).
- runtimeGain: Типичное обновление (новое сообщение в чате): 5-10 узлов dirty из 10000. Display list rebuild = O(dirty), а не O(total). Итоговое repaint = только damaged rects через Vello partial redraw (если wgpu поддерживает) или хотя бы scissor-rect. Это центральная оптимизация для 60fps на слабом железе.
- compileTimeCost: Низкий. Чистая арифметика над &[u64] / bitvec. bitvec — средних размеров crate, даёт ~5-10s к сборке. Можно обойтись fixedbitset или ручным u64-сканированием.
- tradeoffs: Требует явной дисциплины mark_dirty — забытая пометка = stale рендер. Альтернатива — версии/тики как в Bevy, но они дороже на read. Damage rect tracking может дробиться: 1000 мелких rect’ов хуже, чем один большой, нужен merge-порог.
- existingUsage: Flutter RenderObject.markNeedsLayout + dirty list, Servo’s restyle damage, Bevy change detection (Ticks + bitmask per archetype), Emacs redisplay dirty flags.
- rustFit: Отлично. Никакой динамики: bitset живёт в одном месте, инвалидирование через явный
mark_dirty(id). Совместимо с compiled signal graph: выход каждой сигнальной ноды — dirty bits. Через fload передаётся готовый &[u64] или damage rect list.
Display list diffing на уровне Vello Scene
Section titled “Display list diffing на уровне Vello Scene”- idea: Display list — это flat Vec
. Дифф двух списков — это sequence diff (как строки). Добавляем stable id к каждой команде (hash от NodeId + command slot), затем Myers O(ND) diff. Результат: reuse-префикс, insert/delete/move в середине, reuse-суффикс. Vello Scene строится инкрементально из стабильных сегментов. - runtimeGain: Скролл на 1px неизменного содержимого: 95% команд переиспользуются (только transform на корне). Это экономит rebuild scene graph в Vello — что на больших сценах 5-15ms на кадр. Поддержка Vello partial present ещё сильнее уменьшает GPU-время.
- compileTimeCost: Средний. Диффинг display list — кодогенерация по командам (macro-generated match arms). Каждая новая команда добавляет дифф-логику; compile time растёт линейно.
- tradeoffs: Требует детерминированной выдачи команд: один и тот же UI → один и тот же порядок DrawCmd. Анимации (transform, opacity) ломают reuse префикса — нужно явно выносить их в отдельный ‘ephemeral’ слой. Diff сам по себе O(ND) может доминировать при больших перестройках, тогда fallback — full rebuild.
- existingUsage: Chrome’s cc::DisplayItemList + paint diff, Servo’s WebRender display list caching (SceneBuilder), Flutter’s Layer tree diffing, Makepad’s draw list reuse. Vello пока этого не имеет из коробки, но Scene API предполагает append/extend — естественное место для инкрементальной подачи.
- rustFit: Хорошо. Display list уже flat — идеально для diff. Стабильный id команды = NodeId + slot index — простая арифметика, никаких хэшей строк. Через fload отдаём host-стороне список patches, host применяет к своему retained Scene.
Layout caching с per-node computed size (taffy-style)
Section titled “Layout caching с per-node computed size (taffy-style)”- idea: Вместо пересчёта layout с нуля при любом изменении — хранить layout tree в arena (Vec
), каждая нода имеет cache entry {size, position, content_hash}. При invalidation: помечаем путь от изменённой ноды к корню + всех потомков. Layout-фаза: обход дерева, ноды с valid cache — O(1) skip, ноды dirty — пересчёт. - runtimeGain: Типичный UI: layout — 30-50% frame time. Caching убирает ~90% пересчётов: изменение текста в одном TextField = пересчёт 5-10 нод вместо всего дерева. Bevy + taffy показывает 60fps на сложном UI за счёт этого.
- compileTimeCost: Низкий-средний. Требует проектирования вокруг arena и явного layout tree API. Сам taffy — зрелая библиотека, compile time посредственный.
- tradeoffs: Arena фрагментируется при массовом create/destroy — нужен compaction или free list. Кэш размеров чувствителен к ‘flex’ изменениям (изменение размера родителя инвалидирует всё поддерево). Нужно явное разделение ‘intrinsic size’ и ‘final size’ — иначе каскадная инвалидация.
- existingUsage: taffy (Dioxus, Bevy UI) — Block/Flexbox/Grid layout с явным dirty tracking на стороне клиента; Clay (C-библиотека, immediate-mode layout с arena); Yoga (Facebook) — классический retained layout с диффом.
- rustFit: Отлично. Arena — Vec
, никаких Box/RefCell. NodeId — generational index (совпадает с SlotMap техникой выше). Для fload: layout results — это массив (id, x, y, w, h), trivially serializable, никакого repr(C) не нужно.
Golden frame testing с perceptual hash (dHash/pHash/SSIM)
Section titled “Golden frame testing с perceptual hash (dHash/pHash/SSIM)”- idea: Для каждого golden-теста: рендерим кадр headless (Vello → CPU rasterization или wgpu offscreen), считаем perceptual hash (pHash или dHash) и сравниваем с эталоном. Threshold: distance < N — pass. Это ловит регрессии в рендере без fragile pixel-exact сравнения.
- runtimeGain: В проде — ноль (это тестирование). В разработке — позволяет агрессивно менять pipeline (оптимизации layout, display list diff) без ручного визуального review. Можно ввести ‘perf budget’ assertions: время рендера кадра < X — иначе fail.
- compileTimeCost: Низкий. Библиотеки маленькие. Само тестирование — это внешний harness, не часть hot path.
- tradeoffs: pHash ловит структурные, но не цветовые регрессии (гамма, альфа-канал). Нужно комбинировать с pixel-exact diff для критичных областей (текст). Эталоны надо хранить — LFS или отдельный репо. Шум от GPU-драйверов: тот же код на Intel/AMD/NV может давать разные снимки на субпиксельном уровне — threshold подбирается экспериментально.
- existingUsage: insta (snapshot testing в Rust), skia-safe + golden images в Servo, headless-gl в браузерном мире, dssim-core (Netflix SSIM), image-compare crate. Визуальная регрессия в egui — через скриншоты.
- rustFit: Хорошо. Vello + wgpu offscreen — стандартный паттерн (используется в bevy для скриншотов). dHash тривиален (~30 строк). Через fload: тест-харнесс на host-side, плагин отдаёт кадр — host сравнивает.
Compile-time hashing для signal graph identity
Section titled “Compile-time hashing для signal graph identity”- idea: При компиляции UI-плагина вычисляется Merkle hash от его AST signal graph + типов событий. На hot-swap: если новый DLL имеет тот же root hash — state миграция не нужна, patch применяется in-place. Если hash изменился — используем под-деревья с совпадающими hash для частичной миграции состояния.
- runtimeGain: Hot-swap плагина без state loss: типичное обновление (изменился один виджет) = Merkle path даёт нам точный список сохраняемых состояний. Время hot-swap: O(изменённые узлы), а не O(все виджеты). Это делает hot-reload пригодным для ежедневной разработки.
- compileTimeCost: Средний. Макросы на вычисление хэша от типа/AST добавляют compile-time cost. Inventory даёт небольшой link-time overhead. Не доминирует.
- tradeoffs: Хэш чувствителен к переименованиям и reordering полей — refactoring ломает state. Нужна ‘stable naming’ дисциплина в коде UI. Merkle-tree сам по себе — дополнительный кодоген, ~5-10% к размеру DLL.
- existingUsage: Bevy Assets (AssetPath + hash → AssetId), rustc queries (DepNode = hash of query key), Buck2/Bazel для build cache. В UI: Makepad’s LiveId — hash от строки на этапе компиляции.
- rustFit: Хорошо. Хэш вычисляется proc-macro на стороне plugin crate. Через fload передаётся u128 — тривиально. Соответствует ‘compiled signal graph’ ADR: идентичность известна на этапе компиляции, не нужен runtime reflection.
Pipeline stage caching — multi-level cache между layout → display list → GPU
Section titled “Pipeline stage caching — multi-level cache между layout → display list → GPU”- idea: Каждый этап pipeline (layout → display list → Vello scene → GPU commands) имеет свой кэш с независимой инвалидацией. Изменение цвета инвалидирует display list, но не layout. Изменение layout инвалидирует scene, но не source tree. Каждый уровень проверяет своё входное значение на equality — если совпало с кэшированным, downstream не дёргается.
- runtimeGain: Ховер на кнопке: layout не меняется, только цвет. Должно занять 0.1ms (перерисовка одного draw cmd), а не 5ms (перестройка всего). Это центральная оптимизация для отзывчивости — perceived latency падает в 10-50x на типичных взаимодействиях.
- compileTimeCost: Средний. Каждый cache level — отдельный тип. Композиция generic types увеличивает мономорфизацию, но линейно.
- tradeoffs: Каждый уровень хранит своё состояние — растёт RSS (примерно 2-3x от минимального). Требует PartialEq на промежуточных представлениях, что может быть дорого для сложных типов (хэш вместо полного сравнения). При ошибке в equality — застревание stale данных, нужны golden-frame тесты для ловли.
- existingUsage: rust-analyzer (SourceText → Parse → AstId → Hir), Servo (DOM → Style → Layout → DisplayList → WebRender scene), Chrome (Blink → cc layers → GpuCmds). Vello CPU-fallback имеет похожую layered архитектуру.
- rustFit: Отлично. Каждый уровень — чистая функция с PartialEq на выходе. Совместимо с Salsa: каждый уровень = query. Через fload отдаётся display list — уже проверенный на equality с кэшем.
Topological ordering с priority inversion detection (petgraph::algo::toposort)
Section titled “Topological ordering с priority inversion detection (petgraph::algo::toposort)”- idea: Compiled signal graph — это DAG. Перед исполнением: toposort для определения порядка вычислений. Tarjan SCC для обнаружения циклов (signal A зависит от B, B от A — должно быть compile error). Каждому узлу присваивается topological level — изменения распространяются ‘волной’ снизу вверх, без лишних пересчётов.
- runtimeGain: Избегаем повторного вычисления узла, если несколько его зависимостей изменились — порядок гарантирует один проход. Детекция циклов на этапе загрузки DLL, а не в рантайме — это критично для hot-swap безопасности.
- compileTimeCost: Низкий. petgraph — зрелая библиотека, но тяжёлая (~30s к сборке). Можно заменить на ручной tarjan на ~100 строк.
- tradeoffs: Циклы — это ошибка компиляции плагина, но при hot-swap нужно явно проверять (старый плагин + новый плагин могут в сумме дать цикл). Dynamic edge addition (редко, но бывает для conditional rendering) требует пересчёта toposort — можно инкрементально, но сложно.
- existingUsage: petgraph::algo::toposort, petgraph::algo::tarjan_scc. Bevy render graph использует toposort для планирования passes. Buck2/Bazel — topological build execution.
- rustFit: Отлично. Граф известен на этапе компиляции (compiled signal graph) — petgraph строится один раз при загрузке плагина, результат кэшируется. Через fload передаётся готовый Vec
— порядок исполнения.
Vello retained scene + partial present (wgpu surface damage)
Section titled “Vello retained scene + partial present (wgpu surface damage)”- idea: Display list diff даёт нам damage rect. Передаём его в Vello через Scene::append только изменённых подсцен + на wgpu уровне используем partial present (Metal — presentsWithTransaction + dirty rects; Vulkan — VK_EXT_swapchain_maintenance1 с dirty regions; DX12 — DXGI_PRESENT_PARAMETERS). На неподдерживаемых платформах — scissor test на этапе rasterization.
- runtimeGain: Скролл на 1px: GPU перерисовывает только полосу в 1px + newly-visible region. Экономия fill-rate в 10-100x на типичных UI операциях. На слабом GPU (Intel iGPU) это разница между 60fps и 15fps.
- compileTimeCost: Низкий. Это конфигурация Vello и наша собственная подача данных. Никакой макрос-магии.
- tradeoffs: Platform-specific код для partial present — на каждой ОС свой путь. Сложно отлаживать: damage rect bug = артефакты на экране. Полная перерисовка иногда быстрее, чем 100 маленьких partial updates — нужен merge порог. Vello active development — API меняется.
- existingUsage: wgpu TextureView::begin_render_pass с viewport, Vello Scene::append с transform, Chrome’s partial swap (DAMAGE_EXT / VK_KHR_swapchain_mutable_format). DirectComposition на Windows, Metal partial redraw на macOS/iOS.
- rustFit: Хорошо, но требует расширения Vello: Vello не имеет retained mode из коробки, мы строим его поверх (Scene::reset + append только dirty подсцен). wgpu partial present пока нестабилен — нужен fork или feature flag. Для fload: host-side Vello instance, plugin отдаёт draw commands с damage rect.
Compiled signal graph diffing — патч на уровне AST
Section titled “Compiled signal graph diffing — патч на уровне AST”- idea: Proc-macro #[derive(Signal)] анализирует AST: для каждого поля генерирует отдельный signal node + function ‘apply_patch(old, new) -> Option
’. При hot-swap: старое состояние передаётся в новый плагин через fload как сериализованный блоб, новый код пытается применить его через совпадающие поля (по имени + типу). Несовпадающие — default. - runtimeGain: Hot-reload плагина за 10-50ms: сериализация состояния + десериализация + применение только изменённых полей. Без этой техники hot-swap требует либо полного рестарта UI (плохой UX), либо сложного ручного миграционного кода.
- compileTimeCost: Низкий. Макрос на анализ AST небольшой. Основной cost — на стороне кодгена.
- tradeoffs: Сериализация состояния — это contract между версиями плагина. Переименование поля = потеря состояния. Нужна явная политика ‘stable field names’ + linter. Bincode не даёт schema evolution, rkyv — даёт, но сложнее. Для сложных типов (Vec
) нужен отдельный diff, иначе полный сериализованный список = тяжёлый hot-swap. - existingUsage: Leptos server functions, Dioxus hot reload (через subsecond + dioxus-devtools), Makepad Live DSL, SwiftUI @State, Compose Stable annotations.
- rustFit: Отлично. Соответствует ADR ‘compiled signal graph’. Patch format — bincode/rkyv через fload, никакого repr(C). Derive-макрос генерирует всё на этапе компиляции — zero runtime reflection.
Priority-based frame scheduling (input > anim > layout > render)
Section titled “Priority-based frame scheduling (input > anim > layout > render)”- idea: На каждый кадр назначаем priority: user-input > animation > layout > render. Salsa queries помечаются durability: user-input = lowest durability (инвалидируется всегда), layout = medium, scene build = high. Scheduler выбирает: в бюджете 8ms — исполняем только high-priority + user-input. Low-priority (prefetch, thumbnail decode) откладывается.
- runtimeGain: В бюджете 24fps (41ms) всё успевает. В бюджете 60fps (16ms) — нет. Priority scheduling позволяет держать 60fps на hot path, откладывая второстепенное. Perceived latency падает: input → screen за 16ms, даже если под капотом layout ещё не доделан (показываем прошлый кадр с transform).
- compileTimeCost: Низкий. Макросный codegen на уровне типов.
- tradeoffs: Требует явной классификации всех задач по приоритету — ошибка классификации = jank. Откладывание low-priority может привести к starvation (на слабом железе prefetch никогда не выполняется) — нужен aging механизм. Сложно отлаживать: race между priority levels = визуальные артефакты.
- existingUsage: Bevy change detection (Ticks
), Salsa durability (серый = freeze на N revisions), React Fiber lanes, SwiftUI AttributeGraph (priority propagation). - rustFit: Хорошо. Приоритеты — это дополнительное поле в NodeId/Query key. Tokio::task::Priority (если используется) или ручной очередью. Совместимо с current_thread runtime: один поток, явная очередь по приоритетам.
Notes: Привязка к уже решённым ADR-пунктам: compiled signal graph, Elm-outside, display-list как backend contract, Vello+Parley, fload как DLL boundary, frame budget 24/45/60-75 fps, LTO/PGO/codegen-units=1. Эти техники diffing/caching/invalidation сидят поверх этих решений. Главные связки: (1) Salsa + interning — на входе pipeline, до layout; (2) SlotMap + generational IDs — кости деревьев; (3) bitset dirty + coarse damage tracking — между layout и display list; (4) display list diff + retained Vello scene — на выходе; (5) structural sharing — только для больших списков (message list), не для общего случая. Не делать: fine-grained signal tracking в рантайме (уже отвергнуто), Virtual DOM diffing как React (подходит хуже, чем slot map + bitset dirty для retained UI), межфреймовое удержание Arc