Сплошная проверка документов против дерева. Код не менялся. docs/rust_vs_cpp.md — числа, снятые с чужого каталога сборки: - «775 МБ исходников зависимостей» и «10 пакетов в графе C++» получены на build/, сконфигурированном из другого дерева (его _deps содержит 262 МБ nlohmann_json, который проект не объявляет). На деле девять объявленных репозиториев дают ~400 МБ исходников; - пакетов в графе Rust 82, а не 76 (cargo tree по текущему Cargo.lock); - итог по строкам 2976/494, а не 2962/492: строка «меш» устарела на 17 строк игнорируемого диагностического теста, тесты в модулях — 176; - «перевес почти весь в Vulkan-слое» — на деле 346 из 542 (64 %), из них 305 на Context и Swapchain; остальное приходится на меш, редактор и приложение; - «ни один замер не даёт разницы в порядок величины» неверно: инкрементальная release — 52.3 против 4.4 с, это 11.9x; - граней с пятью и более вершинами в plane.obj 119, а не 121; - из Catch2 перенесены все три случая, и добавлено ещё два, а не «три перенесены дословно»; - деструктор Renderer::Impl — 22 строки, а не тридцать; - путевых зависимостей у порта две: assets/meshes с откатом на текущий каталог и pipeline_cache.bin рядом с бинарём; - граф целей CMake ацикличен, цикл существует на уровне исходников — именно поэтому CMake и молчит. rust/README.md: - объявленный rust-version = "1.82" недостижим: залоченные egui, egui-winit и epaint 0.36.1 требуют 1.95; - перечни зависимостей крейтов были неполны, приведены целиком; - build.rs читает ../../../shaders, а не ../../shaders; - тесты не «чистая математика»: тесты загрузчика читают файлы из assets/meshes и требуют клон репозитория; - assets/ порту никто не копирует, он находит корневую копию сам; - те же поправки про цикл в CMake и про момент ожидания простоя. README.md: - панель называется Mesh, а не «Mesh load»; - пресета clang-cl не существует, компилятор пресеты не фиксируют; - версии Vulkan SDK и компиляторов сборкой не проверяются; - Buffer и Image перечислены среди рабочих модулей vk/, хотя ими не пользуется никто. docs/theory/solver_2x_sdf/README.md: - предлагался несуществующий переключатель cfg.collision="bgk" и реестр операторов get; тот же файл двумя разделами ниже говорит, что оператор зафиксирован. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
417 lines
35 KiB
Markdown
417 lines
35 KiB
Markdown
# 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 |
|
||
| всего пакетов в графе | 9 | **82** |
|
||
| исходники зависимостей на диске | **~400 МБ** на каждый каталог сборки | **80 МБ** в общем реестре |
|
||
|
||
Пятикратная разница в размере бинаря — это в основном статически влинкованная стандартная
|
||
библиотека Rust и форматирование `core::fmt`; C++-версия тянет CRT из системы. Разница в
|
||
числе пакетов (82 против 9) впечатляет ровно до того момента, как посмотреть на объём:
|
||
82 крейта Rust занимают в пять раз меньше места, чем девять библиотек C++, потому что
|
||
`.crate` — это архив выпуска, а `FetchContent` — клон с историей.
|
||
|
||
> **Поправка.** В первой редакции здесь стояли «775 МБ» и «10 пакетов». Обе цифры сняты
|
||
> с каталога `build/`, сконфигурированного из другого дерева исходников: его `_deps/`
|
||
> содержит 262 МБ `nlohmann_json`, которого этот проект не объявляет. Девять объявленных
|
||
> репозиториев дают около 400 МБ исходников и около 510 МБ всего `_deps` после сборки.
|
||
> Число пакетов Rust получено `cargo tree -e normal` по текущему `Cargo.lock`.
|
||
|
||
### Выполнение
|
||
|
||
Обе версии показывают в режиме 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` 119 граней с пятью и более вершинами. 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** | 412 | 51 | 72 | **289** |
|
||
| редактор (`editor`) | 305 | 68 | 20 | **217** | 431 | 48 | 62 | **321** |
|
||
| приложение (`app`) | 85 | 14 | 5 | **66** | 226 | 27 | 43 | **156** |
|
||
| тесты | 65 | 9 | 5 | **51** | *внутри модулей* | | | *176* |
|
||
| **итого** | **3181** | 604 | 143 | **2434** | **3870** | 400 | 494 | **2976** |
|
||
| система сборки | 431 | | | **399** | 232 | | | **197** |
|
||
|
||
Порт длиннее примерно на пятую часть, и большая часть перевеса — в Vulkan-слое: +346
|
||
строк кода в `simv-vk` — это написанная руками замена vk-bootstrap. Но «почти весь» было
|
||
бы преувеличением: из 542 строк общего перевеса на Vulkan-слой приходится 346 (64 %), а
|
||
внутри него на `Context` + `Swapchain` — 305 (56 % от общего). Остальное настоящее: меш
|
||
+117, редактор +104, приложение +90, `core` −64. Приложение выросло с 66 до 156 строк,
|
||
потому что в него переехал `App` из `simv_core` (см. расхождение 1) и поиск каталога
|
||
моделей стал честным обходом предков вместо пяти захардкоженных `../`.
|
||
|
||
Комментариев в порте втрое больше (494 против 143), и это перекос замера, а не свойство
|
||
языка: значительная их часть — пометки «здесь расхождение с C++-версией и вот почему»,
|
||
написанные ради этого документа. По строкам собственно кода разрыв — 2976 против 2434,
|
||
и он меньше, если вычесть тесты: они в Rust живут внутри модулей (176 строк), в C++ —
|
||
отдельной целью (51 строка).
|
||
|
||
## Структурные расхождения
|
||
|
||
Каждое — с причиной. Ни одно не сделано «чтобы было красивее».
|
||
|
||
### 1. `App` переехал в исполняемый крейт
|
||
|
||
В C++ цикличны исходники: `core::App` владеет `vk::Renderer`, а `vk::Renderer`
|
||
принимает `core::Window&`. Граф целей CMake при этом ацикличен — потому CMake ни на что
|
||
и не жалуется: `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 <volk.h>` в
|
||
`mesh/ObjLoader.cpp`, все заголовки видны. В порте `simv-mesh` и `simv-editor` просто не
|
||
имеют `ash` в зависимостях: нарушение не соберётся.
|
||
|
||
Побочный эффект: `EditorUI` не может назвать тип `vk::RenderMode`. C++ обходит ту же
|
||
границу, возвращая из панели голый `int`, который `main.cpp` приводит
|
||
`static_cast<vk::RenderMode>` — связь двух перечислений там держится на честном слове.
|
||
В порте у панели свой `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<void()>`, а замыкание из `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<Mesh>`, и исключение из рабочего потока
|
||
всплывает при `get()`. В порте поток шлёт `Result<Mesh, ObjError>` по каналу, панель
|
||
опрашивает `try_recv()`. Разница не в количестве кода (его столько же), а в том, что
|
||
ошибка стала значением: пропустить её нельзя, а тип в сигнатуре перечисляет, что вообще
|
||
может пойти не так.
|
||
|
||
### 8. SPIR-V вшит в исполняемый файл
|
||
|
||
`build.rs` компилирует шейдеры в `OUT_DIR`, откуда они попадают в бинарь через
|
||
`include_bytes!`. Вместе с этим исчезли: функция `FindSpvPath` с перебором путей, два
|
||
POST_BUILD-шага копирования каталогов в `CMakeLists.txt`, требование запускать редактор
|
||
из его собственного каталога и целый класс ошибок «шейдер не найден» во время выполнения
|
||
— отсутствующий шейдер теперь ломает сборку. Путевых зависимостей осталось две:
|
||
`assets/meshes`, который ищется подъёмом по предкам бинаря и лишь затем — по предкам
|
||
текущего каталога, и `pipeline_cache.bin`, который пишется рядом с бинарём.
|
||
|
||
### 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`. В порте всё это написано руками: перебор устройств, чтение
|
||
`PhysicalDeviceFeatures2` с цепочкой `Vulkan12/13Features` и сверка девяти возможностей,
|
||
разбор флагов семейств очередей с проверкой поддержки показа на поверхность, выбор
|
||
формата с откатом на первый доступный, зажим размера и числа образов по возможностям
|
||
поверхности, создание представлений.
|
||
|
||
Счёт по одним и тем же модулям (`Context` и `Swapchain` целиком, строки кода без пустых и
|
||
комментариев): **301 строка в C++ против 606 в порте**. Эти 305 строк разницы и есть
|
||
главная причина, по которой `simv-vk` вышел длиннее прообраза, — весь остальной перевес
|
||
Vulkan-слоя укладывается в четыре десятка строк.
|
||
|
||
**Ритм выпусков разный.** `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<GpuMesh>`, и рассогласовать нечего.
|
||
|
||
**Ничья — сам Vulkan.** Записи `vkCmdBeginRendering`, барьеры sync2, сборка конвейеров
|
||
выглядят в обеих версиях почти одинаково, строка в строку. `ash` не пытается быть
|
||
безопасным поверх Vulkan, и это правильно: `unsafe`-блоки локализуют ровно те места, где
|
||
C++ тоже опасен, не притворяясь, что их нет.
|
||
|
||
## Итог
|
||
|
||
Кроме одной клетки, ни один замер не даёт разницы в порядок величины, так что решение к
|
||
таблице не сводится. Исключение — инкрементальная сборка release: 52.3 с против 4.4 с,
|
||
это 11.9×, и объясняется оно thin LTO, а не языком (разбор ниже). Следующие по величине
|
||
разрывы уже укладываются в порядок: исходники зависимостей ~5×, размер бинаря 4.9×.
|
||
Что можно утверждать по итогам порта:
|
||
|
||
**Порт занял примерно на пятую часть больше строк**, и бо́льшая часть перевеса пришлась на
|
||
`simv-vk`. Главная причина названа выше: замены vk-bootstrap нет, и 301 строка
|
||
`Context` с `Swapchain` превратилась в 606 — это 305 строк из 542 общего перевеса, то есть
|
||
чуть больше половины, а весь Vulkan-слой даёт 346 (64 %). Остальной 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 тянет девять библиотек по фиксированным тегам и не имеет общего кэша: каждый
|
||
свежий каталог сборки скачивает их заново.
|
||
|
||
Выбор языка — за вами. Этот документ фиксирует только то, что удалось измерить и показать
|
||
на коде.
|