From 1d7f9191ea02a200fd4713c1dfe1c899c0934622 Mon Sep 17 00:00:00 2001 From: NotBigGhost Date: Sun, 30 Aug 2026 20:58:33 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A1=D1=80=D0=B0=D0=B2=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20Rust-=20=D0=B8=20C++-=D0=B2=D0=B5=D1=80=D1=81?= =?UTF-8?q?=D0=B8=D0=B9:=20=D0=B7=D0=B0=D0=BC=D0=B5=D1=80=D1=8B=20=D0=B8?= =?UTF-8?q?=20=D1=80=D0=B0=D0=B7=D0=B1=D0=BE=D1=80=20=D1=80=D0=B0=D1=81?= =?UTF-8?q?=D1=85=D0=BE=D0=B6=D0=B4=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Условия, цифры и разбор по коду. Замеры на одной машине подряд: сборка (полная, тёплая, инкрементальная — в release и debug), размеры бинарей и каталогов, состав и объём зависимостей, старт до готовности рендерера, процессорное время при 60 Гц, строки по модулям одинаковыми правилами для обеих версий. Главные наблюдения: * инкрементальная release-сборка Rust 52 с против 4.4 с — это цена thin LTO, а не языка: со сравнимым уровнем оптимизации 5.5 с против 4.4 с; * FetchContent клонирует 775 МБ истории на каждый каталог сборки и не имеет общего кэша; 76 крейтов Rust занимают 80 МБ в общем реестре; * загрузчики .obj сходятся на cow.obj точно и расходятся на plane.obj на 0.13% треугольников — из-за разной триангуляции многоугольников с n >= 5. Плюс четыре дефекта C++-версии, найденных при переносе, в том числе строка «Mesh ready», которую троттлинг категории MeshIO отбрасывает всегда: счётчики после сварки не печатались никогда. Co-Authored-By: Claude Opus 5 --- docs/rust_vs_cpp.md | 395 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 395 insertions(+) create mode 100644 docs/rust_vs_cpp.md diff --git a/docs/rust_vs_cpp.md b/docs/rust_vs_cpp.md new file mode 100644 index 0000000..65387e2 --- /dev/null +++ b/docs/rust_vs_cpp.md @@ -0,0 +1,395 @@ +# Rust против C++: две версии редактора SimVulcan + +Ветка `rust` содержит порт редактора с C++20 на Rust. Обе версии лежат в дереве рядом, +собираются независимо и делают одно и то же. Этот документ — не выбор победителя, а +разложенные факты: что изменилось, чего это стоило и что померилось. + +## Что именно сравнивается + +Программа одна и та же: окно 1280×720, орбитальная камера вокруг начала координат, три +опорные плоскости сетки и цветные оси XYZ, загрузка `.obj` в отдельном потоке, сварка +вершин с допуском `1e-4`, три режима отображения модели, единый проход динамического +рендеринга с глубинным вложением, submit через `synchronization2`, два кадра в полёте. + +**Общее:** каталог `shaders/` — обе версии компилируют одни и те же `.vert`/`.frag` тем +же `glslangValidator -V --target-env vulkan1.3`; каталог `assets/` — одни и те же модели. + +**Разное по договорённости:** интерфейс. В C++ это Dear ImGui, в Rust — **egui**. Крейт +`imgui` существует, но это биндинги: сборка всё равно тянула бы ~40 тыс. строк C++ через +`cc`, и «Rust-версия», компилирующая C++, обесценила бы сравнение экосистем. Панели +из-за этого выглядят иначе; набор элементов управления тот же. + +## Условия замеров + +| | | +|---|---| +| машина | Intel Core i5-1135G7, 4 ядра / 8 потоков, 16 ГБ, SSD | +| видео | Intel Iris Xe, драйвер 31.0.101.5186, экран 60 Гц | +| ОС | Windows 11 Home 10.0.26200 | +| C++ | MSVC 14.50.35717 (VS 18 BuildTools), CMake 4.3.2, Ninja 1.13.2 | +| Rust | rustc / cargo 1.97.1, MSVC-ABI | +| общее | Vulkan SDK 1.4.341.1 | + +Обе сборки — релизные (`CMAKE_BUILD_TYPE=Release`; `cargo --release` с `lto = "thin"`, +`codegen-units = 1`). Обе однопоточно ограничены одним и тем же железом; замеры сделаны +подряд, без других нагрузок. + +## Цифры + +### Сборка + +Все числа C++ уменьшены на 3.0 с — накладные расходы `vcvars64.bat`, который приходится +вызывать в каждом сеансе (замерены отдельно, среднее из трёх). У `cargo` такого нет. + +| | C++ | Rust | +|---|---|---| +| конфигурация (исходники зависимостей на месте) | 9.7 с | входит в сборку | +| **release с нуля** | **114 с** | **194 с** (thin LTO) · 182 с (без LTO) | +| release, ничего не менялось | 1.1 с | 1.0 с | +| **release после правки одного файла** | **4.4 с** | **52.3 с** (thin LTO) · 5.5 с (без LTO) | +| debug с нуля | 112 с | 83 с | +| debug после правки одного файла | 8.0 с | 5.2 с | + +Правился в обоих случаях самый крупный файл Vulkan-слоя: `src/vk/Renderer.cpp` и +`crates/simv-vk/src/renderer.rs`. + +Две оговорки, без которых таблица врёт. + +**Уровни оптимизации разные.** `CMAKE_BUILD_TYPE=Release` у MSVC — это `/O2` без +оптимизации всей программы (CMake не включает `INTERPROCEDURAL_OPTIMIZATION` сам). +Профиль порта просит `lto = "thin"` и `codegen-units = 1`, то есть межкрейтовую +оптимизацию. Отсюда и 52 секунды на инкрементальную пересборку: правка одной строки +заставляет заново оптимизировать и слинковать весь бинарь. Колонка «без LTO» — +`CARGO_PROFILE_RELEASE_LTO=false`, `CODEGEN_UNITS=16`, то есть примерный аналог `/O2` +без LTCG: **5.5 с против 4.4 с**, и разницы уже нет. Инкрементальная сборка в Rust +медленная не сама по себе, а ровно настолько, насколько её просят оптимизировать. + +**«С нуля» здесь не значит «с чистой машины».** Обе цифры сняты при уже скачанных +зависимостях. Честная холодная сборка C++ до конца не дошла: `FetchContent` клонирует +девять репозиториев с полной историей, и на этом соединении она за пятнадцать минут +успела скачать два из них (glfw 24 МБ, glm 100 МБ) и была прервана. Готовое дерево +зависимостей от отладочной сборки занимает **775 МБ**, и общего кэша у FetchContent нет: +каждый новый каталог сборки скачивает всё заново. Реестр cargo, наоборот, один на +пользователя. + +### Артефакты и зависимости + +| | C++ | Rust | +|---|---|---| +| `SimVulcan.exe`, release | **1.18 МБ** | **5.80 МБ** | +| `SimVulcan.exe`, debug | 5.68 МБ | 28.97 МБ | +| каталог сборки | 78 МБ | 469 МБ | +| объявлено зависимостей | 9 | 15 | +| всего пакетов в графе | 10 | **76** | +| исходники зависимостей на диске | **775 МБ** на каждый каталог сборки | **80 МБ** в общем реестре | + +Пятикратная разница в размере бинаря — это в основном статически влинкованная стандартная +библиотека Rust и форматирование `core::fmt`; C++-версия тянет CRT из системы. Разница в +числе пакетов (76 против 10) впечатляет ровно до того момента, как посмотреть на объём: +76 крейтов Rust занимают в восемь раз меньше места, чем девять библиотек C++, потому что +`.crate` — это архив выпуска, а `FetchContent` — клон с историей. + +### Выполнение + +Обе версии показывают в режиме FIFO на экране 60 Гц, поэтому частота кадров у них +одинаковая по построению и ничего не измеряет. Сравнимы старт и процессорная стоимость. + +| | C++ | Rust | +|---|---|---| +| старт до готовности рендерера (лучший из трёх) | 1068 мс | **754 мс** | +| то же, средний | 1080 мс | 872 мс* | +| процессорное время при 60 Гц | **11.2%** реального | 12.6% реального | + +\* без первого запуска сразу после сборки: он занял 3.4 с, пока система подтягивала +свежий 5.8-мегабайтный образ с диска. У C++-версии разброс между запусками — 21 мс. + +Полтора процента разницы по процессору — это egui против Dear ImGui, а не Rust против +C++: egui пересобирает раскладку каждый кадр, ImGui хранит её между кадрами. Сцена +(сетка + модель) в обеих версиях записывается одинаковыми вызовами. + +### Совпадают ли результаты + +Проверка, что порт считает то же самое: один и тот же файл загружается обеими версиями, +сравниваются счётчики. + +| файл | C++ (tinyobjloader) | Rust (tobj) | +|---|---|---| +| `cow.obj` | 2451 вершины, 4898 треугольников | **2451, 4898** | +| `plane.obj` | 10673 вершины, 20035 треугольников | 10638, **20061** | + +`cow.obj` сходится точно. `plane.obj` расходится, и оба расхождения объяснимы: + +* **35 вершин.** tinyobjloader отдаёт глобальный массив `attrib.vertices` целиком, включая + вершины, на которые не ссылается ни одна грань; tobj такие отбрасывает. Сварка потом + всё равно оставила бы их висеть в списке — на картинку они не влияют. +* **26 треугольников.** В `plane.obj` 121 грань с пятью и более вершинами. tobj режет + n-угольник веером, что даёт ровно `n − 2` треугольника — суммарно 20061, и это + совпадает с прямым подсчётом по файлу. tinyobjloader для `n ≥ 5` применяет + earcut-подобную триангуляцию и выбрасывает выродившиеся треугольники, отсюда 20035. + Разница 0.13% и целиком лежит в библиотеке чтения, а не в переносе. + +Сварка вершин (`weld_vertices`) перенесена построчно, включая бакетирование округлением +и FNV-хеш; три её теста из Catch2 перенесены дословно и проходят. + +### Побочная находка: строка, которой не бывает + +При сверке счётчиков выяснилось, что C++-версия **никогда не печатает** результат сварки. +`ObjLoader::LoadObj` пишет `Loaded OBJ '…': N verts, M tris` в категорию `MeshIO`, а +обработчик в `main.cpp` сразу за ним пишет туда же `Mesh ready: …`. У категории стоит +троттлинг 0.5 с, оба сообщения уровня `Info` — второе гарантированно отбрасывается. +В журналах обеих загрузок (`cow.obj` и `plane.obj`) строки `Mesh ready` нет. + +Это и стало причиной единственного расхождения в переносе журнала: в порте троттлинг по +умолчанию выключен, а `set_throttle` включается там, где сообщения идут покадрово. +Иначе стартовый вывод терял бы имя видеокарты — в C++ он уцелел лишь потому, что +низкоуровневый код Vulkan пишет напрямую через `spdlog`, минуя категории. + +## Строки кода + +Считано одинаковыми правилами для обеих версий: строка комментарийная, если после +обрезки пробелов начинается с `//` (для C++ ещё блочные `/* … */`); хвостовые +комментарии за кодом не вычитаются. + +| модуль | C++ | | | | Rust | | | | +|---|---|---|---|---|---|---|---|---| +| | всего | пусто | комм. | **кода** | всего | пусто | комм. | **кода** | +| журнал, окно (`core`) | 370 | 83 | 11 | **276** | 310 | 36 | 62 | **212** | +| Vulkan (`vk`) | 2120 | 380 | 88 | **1652** | 2491 | 238 | 255 | **1998** | +| меш (`mesh`) | 236 | 50 | 14 | **172** | 395 | 50 | 70 | **275** | +| редактор (`editor`) | 305 | 68 | 20 | **217** | 431 | 48 | 62 | **321** | +| приложение (`app`) | 85 | 14 | 5 | **66** | 226 | 27 | 43 | **156** | +| тесты | 65 | 9 | 5 | **51** | *внутри модулей* | | | *≈150* | +| **итого** | **3181** | 604 | 143 | **2434** | **3853** | 399 | 492 | **2962** | +| система сборки | 431 | | | **399** | 232 | | | **197** | + +Порт длиннее примерно на пятую часть, и перевес почти весь в Vulkan-слое: +346 строк кода +в `simv-vk` — это написанная руками замена vk-bootstrap. Приложение выросло с 66 до 156 +строк, потому что в него переехал `App` из `simv_core` (см. расхождение 1) и поиск +каталога моделей стал честным обходом предков вместо пяти захардкоженных `../`. + +Комментариев в порте втрое больше (492 против 143), и это перекос замера, а не свойство +языка: значительная их часть — пометки «здесь расхождение с C++-версией и вот почему», +написанные ради этого документа. По строкам собственно кода разрыв — 2962 против 2434, +и он меньше, если вычесть тесты: они в Rust живут внутри модулей (≈150 строк), в C++ — +отдельной целью (51 строка). + +## Структурные расхождения + +Каждое — с причиной. Ни одно не сделано «чтобы было красивее». + +### 1. `App` переехал в исполняемый крейт + +В C++ граф целей цикличен: `core::App` владеет `vk::Renderer`, а `vk::Renderer` +принимает `core::Window&`. Работает это только потому, что `simv_vk` **не линкует** +`simv_core`, а видит его заголовки через `target_include_directories(simv_vk PUBLIC ..)`, +и всё сходится на компоновке исполняемого файла. Cargo цикл между крейтами отвергает +сразу, поэтому `App` (25 строк) поднят на уровень выше обоих — в `simv-app`. + +Это единственный пункт, где Rust потребовал перекладывать код, и заодно единственный, +где он указал на настоящую проблему в исходной раскладке: срез слоёв, который CMake +пропустил молча. + +### 2. Границу модулей проверяет компилятор + +Главный инвариант проекта — «весь Vulkan живёт в `src/vk/`» — в C++ держится на +дисциплине и записи в `CLAUDE.md`: ничто не мешает написать `#include ` в +`mesh/ObjLoader.cpp`, все заголовки видны. В порте `simv-mesh` и `simv-editor` просто не +имеют `ash` в зависимостях: нарушение не соберётся. + +Побочный эффект: `EditorUI` не может назвать тип `vk::RenderMode`. C++ обходит ту же +границу, возвращая из панели голый `int`, который `main.cpp` приводит +`static_cast` — связь двух перечислений там держится на честном слове. +В порте у панели свой `DisplayMode`, а разбор делает `match` в `simv-app`: добавится +вариант — компилятор укажет место. + +### 3. pImpl не понадобился + +`vk::Renderer` в C++ прячет всё за `struct Renderer::Impl` и `unique_ptr` ради одного: +чтобы заголовок не тащил volk в трансляционные единицы `App` и `main`. В Rust заголовков +нет, и приватные поля модуля дают ту же изоляцию бесплатно — минус один уровень +косвенности и минус ручное объявление/определение `Impl` на 45 строк. + +### 4. Замыкание интерфейса стало параметром, а состояние — возвратом + +В C++ `Renderer` хранит `std::function`, а замыкание из `main.cpp` захватывает по +ссылке камеру, панели и **сам рендерер**, чтобы вызвать `SetViewProj`, `SetRenderMode`, +`SetGridVisible`. В Rust так нельзя: замыкание вызывается из метода рендерера, то есть +рендерер оказался бы одолжен дважды. + +Решение: замыкание передаётся параметром в `draw_frame` и **возвращает** `FrameState` +с матрицей вида-проекции, режимом и видимостью сетки. Состояние применяется к тому же +кадру, отставания нет. Сеттеры исчезли за ненадобностью. + +```cpp +// C++: состояние ставится изнутри колбэка +app.SetUiCallback([&]() { + editorUI.Draw(camera); + renderer.SetViewProj(camera.ViewProj(renderer.AspectRatio())); +}); +``` + +```rust +// Rust: состояние возвращается из замыкания +renderer.draw_frame(window, |ctx| { + editor_ui.draw(ctx, camera); + FrameState { view_proj: camera.view_proj(aspect), .. } +})?; +``` + +### 5. Выгрузка меша уехала из середины кадра + +В C++ панель загрузки зовёт `OnLoaded` прямо из `Draw`, то есть из середины +`Renderer::DrawFrame`, **уже после** `vkAcquireNextImageKHR`. Обработчик оттуда дёргает +`SetMeshCpu`, а тот — `vkDeviceWaitIdle` и выгрузку на видеокарту. Законно, но ждать +простоя устройства посреди записи кадра — сомнительно. + +В Rust это невозможно по той же причине, что и пункт 4. Панель складывает готовый меш в +поле, приложение забирает его после кадра. Ограничение языка здесь вынесло тяжёлую +операцию из горячего пути — не потому, что кто-то это заметил. + +### 6. `Buffer` и `Image` перестали быть мёртвым грузом + +В C++ обе обёртки собираются в цель `simv_vk`, но **ими не пользуется никто**: и +`GridRenderer`, и `GpuMesh`, и глубинное вложение `Renderer` создают ресурсы прямыми +вызовами VMA. Это 200 строк, которые компилируются каждую сборку и никогда не работают. +В порте на них построены все три потребителя — не из-за языка, а потому что порт писался, +когда потребители уже известны. С gpu-allocator обёртка ещё и обязательна по существу: +она держит `Allocation`, который иначе пришлось бы освобождать руками в каждом `Drop`. + +### 7. Асинхронная загрузка: `mpsc` вместо `std::future` + +`LoadObjAsync` в C++ возвращает `std::future`, и исключение из рабочего потока +всплывает при `get()`. В порте поток шлёт `Result` по каналу, панель +опрашивает `try_recv()`. Разница не в количестве кода (его столько же), а в том, что +ошибка стала значением: пропустить её нельзя, а тип в сигнатуре перечисляет, что вообще +может пойти не так. + +### 8. SPIR-V вшит в исполняемый файл + +`build.rs` компилирует шейдеры в `OUT_DIR`, откуда они попадают в бинарь через +`include_bytes!`. Вместе с этим исчезли: функция `FindSpvPath` с перебором путей, два +POST_BUILD-шага копирования каталогов в `CMakeLists.txt`, требование запускать редактор +из его собственного каталога и целый класс ошибок «шейдер не найден» во время выполнения +— отсутствующий шейдер теперь ломает сборку. Из путевых зависимостей остался только +`assets/meshes`, и он ищется от бинаря, а не от текущего каталога. + +### 9. Цикл событий принадлежит библиотеке + +GLFW отдаёт цикл приложению (`while (!ShouldClose()) { PollEvents(); DrawFrame(); }`), +winit забирает его себе и зовёт приложение через `ApplicationHandler`. `Window::ShouldClose` +и `Window::PollEvents` исчезли, их место заняли `window_event` и `about_to_wait`. Это +свойство winit, а не Rust: крейт `glfw` существует и сохранил бы прежнюю форму, но он +такие же биндинги к C, как `imgui`. + +## Чего в экосистеме Rust не нашлось + +**Замены vk-bootstrap нет.** В C++ выбор физического устройства по требуемым +возможностям, поиск семейств очередей, проверка наличия слоя валидации и сборка цепочки +показа — это `vkb::InstanceBuilder`, `vkb::PhysicalDeviceSelector`, `vkb::SwapchainBuilder`, +суммарно около сорока строк вызовов. В порте это `context.rs` и `swapchain.rs`, написанные +руками: перебор устройств, чтение `PhysicalDeviceFeatures2` с цепочкой `Vulkan12/13Features`, +разбор флагов семейств очередей, проверка поддержки показа на поверхность, выбор формата +с откатом, зажим размера и числа образов по возможностям поверхности. Это **самая +большая статья расходов порта** и главная причина, по которой `simv-vk` вышел длиннее +своего прообраза. + +**Ритм выпусков разный.** `ash` 0.38 вышел в апреле 2024 и с тех пор не обновлялся — +это не заброшенность (Vulkan 1.3 он покрывает полностью, а генерируемый API стабилен), +но зафиксировать стоит. `egui` 0.36 и `egui-ash-renderer` 0.13 — август 2026, живее +некуда, причём вторая обновилась вслед за первой за две недели. Обратная сторона живости +— поломки: `egui` 0.36 переименовал `Context::run` в `run_ui`, сменил форму +`TexturesDelta` (карта правок вместо одной на текстуру) и объявил `set_textures` +устаревшим. Порт писался под 0.36 и учитывает всё это; годичной давности пример из сети +не собрался бы. + +**Мелочи, которые в C++ приходят с библиотекой.** spdlog печатает локальное время из +коробки; в Rust за форматированной меткой пришлось взять `chrono`. GLM определяется +одним заголовком; glam — крейт, зато соглашение о пространстве отсечения у него вынесено +в путь функции (`camera::rh::proj::vulkan::perspective` вместо +`GLM_FORCE_DEPTH_ZERO_TO_ONE` плюс ручной `p[1][1] *= -1`). + +## Где какой язык помог, а где помешал + +**Rust помешал — заимствования на границе рендерера.** Пункты 4 и 5 выше: конструкция +«объект хранит замыкание, которое трогает этот же объект» в C++ пишется не думая, а в +Rust не компилируется вовсе. Обход занял полчаса и, если честно, дал код лучше исходного +— но полчаса были потрачены на спор с компилятором, а не на задачу. + +**Rust помешал — цепочки pNext.** `push_next` держит `&mut` на вложенную структуру, пока +жива голова цепочки, поэтому прочитать `f13.synchronization2` при живом +`PhysicalDeviceFeatures2` компилятор не даёт. В C++ pNext — сырой указатель, и вопроса +нет. Лечится блоком, который гасит заимствование: + +```rust +let (f13, f12, f10) = { + let mut f13 = vk::PhysicalDeviceVulkan13Features::default(); + let mut f2 = vk::PhysicalDeviceFeatures2::default().push_next(&mut f13) /* … */; + unsafe { instance.get_physical_device_features2(handle, &mut f2) }; + (f13, /* … */ f2.features) +}; +``` + +**Rust помог — порядок уничтожения.** В C++ `Renderer::Impl::~Impl` — это тридцать строк +ручного разрушения в правильном порядке, и любая перестановка полей в объявлении структуры +её не сломает, но и не поможет: связи нет. В Rust порядок полей **и есть** порядок +уничтожения, а `Drop` у каждой обёртки снимает вопрос «а это уже освободили?». Ловушка +осталась одна и она подписана в коде: `ash::Device` клонируется как таблица функций, а не +как владеющая ссылка, поэтому контекст обязан объявляться последним. + +**Rust помог — ошибки как значения.** `CheckVk(r, where)` в `Renderer.cpp` — это ручная +проверка каждого вызова с бросанием `runtime_error`. В порте её место занял `?` и +трейт-расширение на четыре строки, навешивающее имя вызова. Пропустить проверку нельзя: +`#[must_use]` на `VkResult` не даст. + +**Rust помог — незагруженный меш.** В C++ это `GpuMesh` с состоянием «пустой», метод +`IsValid()` и отдельный флаг `hasMesh` в `Renderer`, которые обязаны быть согласованы. +В порте — `Option`, и рассогласовать нечего. + +**Ничья — сам Vulkan.** Записи `vkCmdBeginRendering`, барьеры sync2, сборка конвейеров +выглядят в обеих версиях почти одинаково, строка в строку. `ash` не пытается быть +безопасным поверх Vulkan, и это правильно: `unsafe`-блоки локализуют ровно те места, где +C++ тоже опасен, не притворяясь, что их нет. + +## Итог + +Ни один из замеров не даёт разницы в порядок величины, так что решение к таблице не +сводится. Что можно утверждать по итогам порта: + +**Порт занял примерно на пятую часть больше строк**, и почти весь перевес пришёлся на +`simv-vk`. Причина одна и она названа выше: замены vk-bootstrap нет, и сорок строк +вызовов превратились в двести пятьдесят строк выбора устройства и сборки цепочки показа. +Вне Vulkan-слоя объёмы сопоставимы. Если проект растёт дальше именно в Vulkan-часть, +перевес разовый: инициализация пишется один раз. + +**Сборка: паритет везде, кроме одной клетки.** Полная сборка release у C++ вдвое быстрее +(114 с против 194 с), полная debug — наоборот, медленнее (112 с против 83 с), +инкрементальная в debug у Rust быстрее (5.2 с против 8.0 с). Единственная резкая +клетка — инкрементальная release: 52 с против 4.4 с. Это цена thin LTO, а не языка: со +сравнимым уровнем оптимизации (LTO выключен, как у MSVC `/O2` без LTCG) те же 5.5 с +против 4.4 с. Практический вывод простой — держать в профиле разработки LTO выключенным, +включая его только для выпуска. + +**Четыре дефекта в C++-версии нашлись при переносе, и ни один не искали.** Цикл в графе +целей CMake, который проходит только из-за незалинкованной цели. Выгрузка меша с +`vkDeviceWaitIdle` посреди записи кадра. Рассогласование `GpuMesh::IsValid()` и флага +`hasMesh` — два источника правды об одном. Строка `Mesh ready`, которую троттлинг +категории `MeshIO` отбрасывает всегда, так что счётчики после сварки не печатались +никогда. Первое стало ошибкой сборки, второе — невозможным, третье — невыразимым, +четвёртое всплыло при сверке чисел. + +**И двести строк мёртвого кода.** `Buffer` и `Image` собираются каждую сборку и никем не +используются. Это не про язык, а про то, что переписывание само по себе работает как +аудит. + +**Цена — время на границах.** Замыкание, которое трогает объект, из чьего метода оно +вызвано; цепочки pNext, держащие заимствование; порядок уничтожения полей. Каждое место +решается за полчаса, каждое решение оказывается не хуже исходного, но полчаса уходят на +разговор с компилятором, а не на задачу. + +**Экосистема разнородна.** `egui` и его Vulkan-бэкенд выпускаются раз в месяц и ломают +API; `ash` не менялся два года и не собирается. Замены vk-bootstrap нет вовсе. В C++ +FetchContent тянет девять библиотек по фиксированным тегам и не имеет общего кэша: каждый +свежий каталог сборки скачивает их заново. + +Выбор языка — за вами. Этот документ фиксирует только то, что удалось измерить и показать +на коде.