diff --git a/README.md b/README.md index e9ab7fd..6e581d7 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/rust_vs_cpp.md b/docs/rust_vs_cpp.md index 8ec3ef9..fd1c2f9 100644 --- a/docs/rust_vs_cpp.md +++ b/docs/rust_vs_cpp.md @@ -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 с), diff --git a/docs/theory/solver_2x_sdf/README.md b/docs/theory/solver_2x_sdf/README.md index 43c341f..590ac8c 100644 --- a/docs/theory/solver_2x_sdf/README.md +++ b/docs/theory/solver_2x_sdf/README.md @@ -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_ī^{после стриминга})`. Второй член — из поля ПОСЛЕ diff --git a/rust/README.md b/rust/README.md index bc7a081..81e6aed 100644 --- a/rust/README.md +++ b/rust/README.md @@ -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++ заведён