Сверка документации с кодом: исправлены расхождения

Сплошная проверка документов против дерева. Код не менялся.

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>
This commit is contained in:
2026-09-16 14:11:06 +03:00
co-authored by Claude Opus 5
parent fe5ca7f936
commit d7881496b0
4 changed files with 101 additions and 56 deletions
+11 -3
View File
@@ -11,16 +11,18 @@ reference grid planes through the origin, with `.obj` model loading and display.
at the origin.
* `.obj` loading with three display modes — solid (flat-shaded), wireframe, and
solid + wireframe overlay.
* ImGui interface (Viewport controls + Mesh load panel).
* ImGui interface — two windows, titled `Viewport` and `Mesh`.
All Vulkan code is isolated in `src/vk/`; the rest of the app is Vulkan-free.
## Targets
* Windows x64 (MSVC / clang-cl)
* Windows x64 (MSVC)
* Linux x64 (gcc / clang)
* macOS arm64 (Apple Silicon, via MoltenVK)
The presets do not pin a compiler — each inherits whatever the environment provides.
## Prerequisites
* Vulkan SDK 1.3.290+ (`https://vulkan.lunarg.com/`)
@@ -28,6 +30,10 @@ All Vulkan code is isolated in `src/vk/`; the rest of the app is Vulkan-free.
* Ninja
* C++20 compiler (MSVC 19.36+, gcc 11+, clang 14+)
Only CMake 3.26 and C++20 are enforced by the build. `find_package(Vulkan)` carries no
version argument, and the compiler minimums above are recommendations, not checks — the
de-facto SDK floor comes from the pinned volk and vk-bootstrap 1.3.295.
## Build
```sh
@@ -50,8 +56,10 @@ ctest --preset windows-msvc-debug
```
src/
core/ Logger, Window, App (orchestration, Vulkan-free)
vk/ All Vulkan: Context, Swapchain, Buffer, Image, Shader, GpuMesh,
vk/ All Vulkan: Context, Swapchain, Shader, GpuMesh,
Renderer (frame loop + depth + ImGui), MeshRenderer, GridRenderer
(Buffer and Image are also built here but used by nobody — the
renderers call VMA directly)
mesh/ CPU mesh: tinyobjloader + welding (no Vulkan)
editor/ Camera, mouse input, EditorUI + MeshLoadPanel (ImGui, no Vulkan)
app/ main.cpp
+48 -31
View File
@@ -80,15 +80,21 @@
| `SimVulcan.exe`, debug | 5.68 МБ | 28.97 МБ |
| каталог сборки | 78 МБ | 469 МБ |
| объявлено зависимостей | 9 | 15 |
| всего пакетов в графе | 10 | **76** |
| исходники зависимостей на диске | **775 МБ** на каждый каталог сборки | **80 МБ** в общем реестре |
| всего пакетов в графе | 9 | **82** |
| исходники зависимостей на диске | **~400 МБ** на каждый каталог сборки | **80 МБ** в общем реестре |
Пятикратная разница в размере бинаря — это в основном статически влинкованная стандартная
библиотека Rust и форматирование `core::fmt`; C++-версия тянет CRT из системы. Разница в
числе пакетов (76 против 10) впечатляет ровно до того момента, как посмотреть на объём:
76 крейтов Rust занимают в восемь раз меньше места, чем девять библиотек C++, потому что
числе пакетов (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 Гц, поэтому частота кадров у них
@@ -122,14 +128,16 @@ C++: egui пересобирает раскладку каждый кадр, ImG
* **35 вершин.** tinyobjloader отдаёт глобальный массив `attrib.vertices` целиком, включая
вершины, на которые не ссылается ни одна грань; tobj такие отбрасывает. Сварка потом
всё равно оставила бы их висеть в списке — на картинку они не влияют.
* **26 треугольников.** В `plane.obj` 121 грань с пятью и более вершинами. tobj режет
* **26 треугольников.** В `plane.obj` 119 граней с пятью и более вершинами. tobj режет
n-угольник веером, что даёт ровно `n − 2` треугольника — суммарно 20061, и это
совпадает с прямым подсчётом по файлу. tinyobjloader для `n ≥ 5` применяет
earcut-подобную триангуляцию и выбрасывает выродившиеся треугольники, отсюда 20035.
Разница 0.13% и целиком лежит в библиотеке чтения, а не в переносе.
Сварка вершин (`weld_vertices`) перенесена построчно, включая бакетирование округлением
и FNV-хеш; три её теста из Catch2 перенесены дословно и проходят.
и FNV-хеш. Все три случая Catch2 перенесены дословно и проходят; сверх них в порт
добавлены ещё два — бакетирование округлением, а не отбрасыванием, и габариты пустого
меша.
### Побочная находка: строка, которой не бывает
@@ -155,22 +163,25 @@ C++: egui пересобирает раскладку каждый кадр, ImG
| | всего | пусто | комм. | **кода** | всего | пусто | комм. | **кода** |
| журнал, окно (`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** |
| меш (`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** | *внутри модулей* | | | *≈150* |
| **итого** | **3181** | 604 | 143 | **2434** | **3853** | 399 | 492 | **2962** |
| тесты | 65 | 9 | 5 | **51** | *внутри модулей* | | | *176* |
| **итого** | **3181** | 604 | 143 | **2434** | **3870** | 400 | 494 | **2976** |
| система сборки | 431 | | | **399** | 232 | | | **197** |
Порт длиннее примерно на пятую часть, и перевес почти весь в Vulkan-слое: +346 строк кода
в `simv-vk` — это написанная руками замена vk-bootstrap. Приложение выросло с 66 до 156
строк, потому что в него переехал `App` из `simv_core` (см. расхождение 1) и поиск
каталога моделей стал честным обходом предков вместо пяти захардкоженных `../`.
Порт длиннее примерно на пятую часть, и большая часть перевеса — в 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) и поиск каталога
моделей стал честным обходом предков вместо пяти захардкоженных `../`.
Комментариев в порте втрое больше (492 против 143), и это перекос замера, а не свойство
Комментариев в порте втрое больше (494 против 143), и это перекос замера, а не свойство
языка: значительная их часть — пометки «здесь расхождение с C++-версией и вот почему»,
написанные ради этого документа. По строкам собственно кода разрыв — 2962 против 2434,
и он меньше, если вычесть тесты: они в Rust живут внутри модулей (≈150 строк), в C++ —
написанные ради этого документа. По строкам собственно кода разрыв — 2976 против 2434,
и он меньше, если вычесть тесты: они в Rust живут внутри модулей (176 строк), в C++ —
отдельной целью (51 строка).
## Структурные расхождения
@@ -179,10 +190,11 @@ C++: egui пересобирает раскладку каждый кадр, ImG
### 1. `App` переехал в исполняемый крейт
В C++ граф целей цикличен: `core::App` владеет `vk::Renderer`, а `vk::Renderer`
принимает `core::Window&`. Работает это только потому, что `simv_vk` **не линкует**
`simv_core`, а видит его заголовки через `target_include_directories(simv_vk PUBLIC ..)`,
и всё сходится на компоновке исполняемого файла. Cargo цикл между крейтами отвергает
В 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 потребовал перекладывать код, и заодно единственный,
@@ -270,8 +282,9 @@ renderer.draw_frame(window, |ctx| {
`include_bytes!`. Вместе с этим исчезли: функция `FindSpvPath` с перебором путей, два
POST_BUILD-шага копирования каталогов в `CMakeLists.txt`, требование запускать редактор
из его собственного каталога и целый класс ошибок «шейдер не найден» во время выполнения
— отсутствующий шейдер теперь ломает сборку. Из путевых зависимостей остался только
`assets/meshes`, и он ищется от бинаря, а не от текущего каталога.
— отсутствующий шейдер теперь ломает сборку. Путевых зависимостей осталось две:
`assets/meshes`, который ищется подъёмом по предкам бинаря и лишь затем — по предкам
текущего каталога, и `pipeline_cache.bin`, который пишется рядом с бинарём.
### 9. Цикл событий принадлежит библиотеке
@@ -333,8 +346,8 @@ let (f13, f12, f10) = {
};
```
**Rust помог — порядок уничтожения.** В C++ `Renderer::Impl::~Impl` — это тридцать строк
ручного разрушения в правильном порядке, и любая перестановка полей в объявлении структуры
**Rust помог — порядок уничтожения.** В C++ `Renderer::Impl::~Impl` — это двадцать две
строки ручного разрушения в правильном порядке, и любая перестановка полей в объявлении структуры
её не сломает, но и не поможет: связи нет. В Rust порядок полей **и есть** порядок
уничтожения, а `Drop` у каждой обёртки снимает вопрос «а это уже освободили?». Ловушка
осталась одна и она подписана в коде: `ash::Device` клонируется как таблица функций, а не
@@ -356,14 +369,18 @@ C++ тоже опасен, не притворяясь, что их нет.
## Итог
Ни один из замеров не даёт разницы в порядок величины, так что решение к таблице не
сводится. Что можно утверждать по итогам порта:
Кроме одной клетки, ни один замер не даёт разницы в порядок величины, так что решение к
таблице не сводится. Исключение — инкрементальная сборка release: 52.3 с против 4.4 с,
это 11.9×, и объясняется оно thin LTO, а не языком (разбор ниже). Следующие по величине
разрывы уже укладываются в порядок: исходники зависимостей ~5×, размер бинаря 4.9×.
Что можно утверждать по итогам порта:
**Порт занял примерно на пятую часть больше строк**, и почти весь перевес пришёлся на
`simv-vk`. Причина одна и она названа выше: замены vk-bootstrap нет, и 301 строка
`Context` с `Swapchain` превратилась в 606. Остальной Vulkan-слой — запись кадра, барьеры,
сборка конвейеров — совпадает почти строка в строку. Если проект растёт дальше именно в
Vulkan-часть, перевес разовый: инициализация пишется один раз.
**Порт занял примерно на пятую часть больше строк**, и бо́льшая часть перевеса пришлась на
`simv-vk`. Главная причина названа выше: замены vk-bootstrap нет, и 301 строка
`Context` с `Swapchain` превратилась в 606 — это 305 строк из 542 общего перевеса, то есть
чуть больше половины, а весь Vulkan-слой даёт 346 (64 %). Остальной Vulkan-слой — запись
кадра, барьеры, сборка конвейеров — совпадает почти строка в строку. Если проект растёт
дальше именно в Vulkan-часть, эта доля перевеса разовая: инициализация пишется один раз.
**Сборка: паритет везде, кроме одной клетки.** Полная сборка release у C++ вдвое быстрее
(114 с против 194 с), полная debug — наоборот, медленнее (112 с против 83 с),
+4 -2
View File
@@ -31,8 +31,10 @@
## Где какая математика (для ревизии)
- **Столкновение** — `collision.py`. KBC-N1: `f ← f − β(2Δs + γΔh)`, где `Δs = Ps·(f−feq)` —
проекция на сдвиг-моменты, `γ` — энтропийный лимитер. Чтобы поэкспериментировать:
переключить `cfg.collision="bgk"`, либо добавить TRT новой функцией и зарегистрировать в `get`.
проекция на сдвиг-моменты, `γ` — энтропийный лимитер. Оператор жёстко зафиксирован:
модуль объявляет `collide = kbc_collide`, поля `cfg.collision` и реестра операторов
не существует (см. раздел «Статус» ниже). Чтобы поэкспериментировать с BGK или TRT,
придётся вводить и то и другое — это осознанно не сделано.
- **Перенос** — `streaming.py`. Чистая пул-схема; на физику влияет только корректность сдвигов.
- **Силы** — `forces.py`. Обмен импульсом по линкам тела:
`F = Σ_links c_i (f_i^{после столкн.} + f_ī^{после стриминга})`. Второй член — из поля ПОСЛЕ
+38 -20
View File
@@ -10,7 +10,9 @@
## Требования
* Rust 1.82+ (проверялось на 1.97.1)
* Rust 1.95+ (проверялось на 1.97.1). В `Cargo.toml` объявлено `rust-version = "1.82"`,
но это значение недостижимо: залоченные `egui`, `egui-winit` и `epaint` 0.36.1 сами
требуют 1.95, и на 1.82 Cargo обрывается на разрешении зависимостей
* Vulkan SDK 1.3.290+ — нужен только `glslangValidator` для сборки шейдеров
(ищется в `%VULKAN_SDK%\Bin`, затем в `PATH`)
* Драйвер Vulkan 1.3 с `fillModeNonSolid`
@@ -24,8 +26,10 @@ cargo build --release
```
Запускать можно из любого каталога. Шейдеры вшиты в исполняемый файл на этапе сборки,
а `assets/meshes` ищется подъёмом по предкам от самого бинаря — в отличие от
C++-версии, которой нужен запуск из её собственного каталога.
а `assets/meshes` ищется подъёмом по предкам от самого бинаря и только затем — от
текущего каталога; в отличие от C++-версии, которой нужен запуск из её собственного
каталога. В отличие от неё же, `assets/` никуда не копируется: порт находит копию в
корне репозитория, поднимаясь от `target/<профиль>/`.
Рядом с исполняемым файлом создаётся `pipeline_cache.bin` — сериализованный
`VkPipelineCache`, он перечитывается при следующем запуске. Файл одноразовый: если
@@ -45,21 +49,34 @@ C++-версии, которой нужен запуск из её собств
cargo test
```
Чистая математика без видеокарты: габариты меша, сварка вершин, чтение `Cube.obj`,
пространство отсечения камеры. Тесты живут прямо в модулях (`#[cfg(test)] mod tests`),
отдельной цели под них нет.
Видеокарта не нужна: габариты меша, сварка вершин (три случая), чтение `Cube.obj` и его
путь ошибки, пространство отсечения камеры и ограничители наклона и приближения,
разбор категорий журнала — тринадцать тестов плюс один помеченный `#[ignore]`
диагностический, тот самый, которым получена таблица расхождения загрузчиков. Чистой
математикой это, впрочем, не является: тесты загрузчика читают настоящие файлы из
`assets/meshes` через `$CARGO_MANIFEST_DIR`, поэтому им нужен рабочий клон репозитория.
Тесты живут прямо в модулях (`#[cfg(test)] mod tests`), отдельной цели под них нет.
## Раскладка
Пять крейтов повторяют карту целей CMake из корня репозитория:
Ниже перечислены сторонние зависимости; вдобавок каждый крейт зависит от тех крейтов
рабочего пространства, что указаны в скобках.
```
crates/simv-core/ журнал с категориями, окно → log, chrono, winit
crates/simv-mesh/ чтение .obj, сварка вершин, габариты → glam, tobj
crates/simv-vk/ весь Vulkan + build.rs (GLSL → SPIR-V) → ash, gpu-allocator,
egui-ash-renderer
crates/simv-editor/ камера, ввод, панели → egui
crates/simv-app/ App и точка входа → всё вышеперечисленное
crates/simv-core/ журнал с категориями, окно → log, chrono, winit
crates/simv-mesh/ чтение .obj, сварка, габариты → glam, tobj, log, thiserror
(+ simv-core)
crates/simv-vk/ весь Vulkan + build.rs → ash, ash-window, raw-window-handle,
(GLSL → SPIR-V) gpu-allocator, egui, egui-winit,
egui-ash-renderer, winit, glam,
bytemuck, log, thiserror
(+ simv-core, simv-mesh)
crates/simv-editor/ камера, ввод, панели → egui, glam, log
(+ simv-core, simv-mesh)
crates/simv-app/ App и точка входа → четыре крейта выше
+ egui, winit, glam, log, anyhow
```
Главный инвариант проекта — «весь Vulkan живёт в одном месте» — здесь проверяет
@@ -67,7 +84,7 @@ crates/simv-app/ App и точка входа
так что нарушить границу нельзя даже по невнимательности.
Шейдеры общие с C++-версией: `build.rs` крейта `simv-vk` компилирует
`../../shaders/editor/*.{vert,frag}` тем же `glslangValidator -V --target-env vulkan1.3`
`../../../shaders/editor/*.{vert,frag}` тем же `glslangValidator -V --target-env vulkan1.3`
и кладёт SPIR-V в `OUT_DIR`, откуда он попадает в бинарь через `include_bytes!`.
## Чем отличается от C++-версии
@@ -78,15 +95,16 @@ Dear ImGui на C++, что обесценило бы сравнение.
Внутри отличия сведены в отдельный документ, здесь только самые заметные:
* **`App` живёт в исполняемом крейте**, а не в `simv-core`. В C++ граф целей цикличен
(`core::App` владеет `vk::Renderer`, `vk::Renderer` принимает `core::Window&`), и
держится это лишь на том, что `simv_vk` не линкует `simv_core`, а видит его заголовки.
Cargo цикл между крейтами отвергает.
* **`App` живёт в исполняемом крейте**, а не в `simv-core`. В C++ цикличны исходники
(`core::App` владеет `vk::Renderer`, `vk::Renderer` принимает `core::Window&`), тогда
как граф целей CMake ацикличен — потому CMake и молчит: `simv_vk` не линкует
`simv_core`, а лишь видит его заголовки, и символы `Window` находятся при сборке
исполняемого файла. Cargo такую схему отвергает.
* **Состояние сцены возвращается из замыкания интерфейса**, а не ставится сеттерами.
Замыкание вызывается из метода рендерера, поэтому трогать рендерер оттуда нельзя.
* **Загруженный меш выгружается на видеокарту после кадра**, а не из середины записи
команд, как в C++, где колбэк панели дёргает `vkDeviceWaitIdle` уже после захвата
образа цепочки показа.
* **Загруженный меш выгружается на видеокарту после кадра**, а не изнутри него, как в
C++, где колбэк панели дёргает `vkDeviceWaitIdle` уже после захвата образа цепочки
показа — хотя запись команд там ещё не началась, `vkBeginCommandBuffer` идёт позже.
* **`Buffer` и `Image` задействованы.** В C++ обе обёртки собираются в цель `simv_vk`, но
ими не пользуется никто.
* **Нет pImpl.** Приватные поля модуля дают ту же изоляцию, ради которой в C++ заведён