Compare commits
2
Commits
d6e1fa52b9
..
rust
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7881496b0 | ||
|
|
fe5ca7f936 |
@@ -60,3 +60,6 @@ target/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
desktop.ini
|
desktop.ini
|
||||||
|
|
||||||
|
# Общие заметки Claude Code: один файл на все ветки, вне версионирования
|
||||||
|
/CLAUDE.md
|
||||||
|
|||||||
@@ -1,202 +0,0 @@
|
|||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
**SimVulcan** is a minimal **Vulkan 1.3 / C++20** 3D model editor. It renders a
|
|
||||||
3D editor space — three reference grid planes (XY, XZ, YZ) through the origin plus
|
|
||||||
coloured X/Y/Z axes — viewed through an orbit camera, and loads/displays `.obj`
|
|
||||||
models with selectable display modes (solid, wireframe, solid + wireframe).
|
|
||||||
|
|
||||||
The C++ side runs no simulation: it is a focused rendering skeleton with an ImGui
|
|
||||||
interface (a Viewport control panel and a Mesh load panel). The repository *also*
|
|
||||||
carries an unrelated Python research prototype under `docs/theory/` — see
|
|
||||||
[Research prototype](#research-prototype-docstheory) below; it is not part of the
|
|
||||||
CMake build.
|
|
||||||
|
|
||||||
## Build
|
|
||||||
|
|
||||||
Prerequisites: **Vulkan SDK 1.3.290+** (provides `glslangValidator` for offline
|
|
||||||
shader compilation), **CMake 3.26+**, **Ninja**, a C++20 compiler (MSVC 19.36+,
|
|
||||||
gcc 11+, clang 14+). On macOS, Vulkan is via MoltenVK (Apple Silicon only).
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cmake --preset windows-msvc-release
|
|
||||||
cmake --build --preset windows-msvc-release
|
|
||||||
```
|
|
||||||
|
|
||||||
Presets: `windows-msvc-debug`, `windows-msvc-release`, `linux-gcc-release`,
|
|
||||||
`linux-clang-release`, `macos-arm64-release` (all Ninja, one dir per preset under
|
|
||||||
`build/<presetName>/`). The first configure fetches dependencies via FetchContent
|
|
||||||
(GLFW, GLM, volk, vk-bootstrap, VulkanMemoryAllocator, Dear ImGui, spdlog,
|
|
||||||
tinyobjloader, Catch2) and needs network access. `VK_NO_PROTOTYPES` is set
|
|
||||||
project-wide; Vulkan entry points load through **volk**.
|
|
||||||
|
|
||||||
Warnings come from `simv_set_warnings` (`/W4 /permissive-`, or
|
|
||||||
`-Wall -Wextra -Wpedantic -Wshadow -Wold-style-cast …`); they are **not** errors.
|
|
||||||
`cmake/Sanitizers.cmake` defines `simv_enable_sanitizers` (Debug-only ASan/UBSan)
|
|
||||||
but no target currently calls it — wire it in manually when chasing memory bugs.
|
|
||||||
|
|
||||||
## Running
|
|
||||||
|
|
||||||
The executable resolves SPIR-V relative to the working directory (`FindSpvPath`
|
|
||||||
probes `spirv/<rel>` and `current_path()/spirv/<rel>`). The `SimVulcan` POST_BUILD
|
|
||||||
step copies the compiled `spirv/` tree and `assets/` next to the executable, so
|
|
||||||
**run from the executable's own directory** (`build/<preset>/src/app/`). `main.cpp`
|
|
||||||
also probes several `../` ancestors for `assets/meshes`. Meshes load at runtime
|
|
||||||
through the ImGui Mesh panel.
|
|
||||||
|
|
||||||
Running writes two files into the CWD: `pipeline_cache.bin` (serialised
|
|
||||||
`VkPipelineCache`, reloaded on the next start) and ImGui's `imgui.ini`. Both are
|
|
||||||
disposable — delete them if pipeline creation or the panel layout misbehaves.
|
|
||||||
|
|
||||||
`ContextOptions::enableValidation` / `enableDebugUtils` default to **true** in
|
|
||||||
every build config, so the Khronos validation layer is requested even in Release;
|
|
||||||
messages (error + warning severity) go through spdlog.
|
|
||||||
|
|
||||||
`run.bat` at the repo root is **stale**: it launches
|
|
||||||
`build/vs2022/src/app/Release/SimVulcan.exe`, a path the Ninja presets never
|
|
||||||
produce. Do not point users at it without fixing the path first.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
Catch2 unit tests, pure CPU/math (mesh bounds + welding). No GPU required.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cmake --build --preset windows-msvc-debug --target simv_tests
|
|
||||||
ctest --preset windows-msvc-debug
|
|
||||||
```
|
|
||||||
|
|
||||||
`windows-msvc-debug` is the only preset with a `testPreset` — for the other
|
|
||||||
configs, invoke `ctest` in `build/<preset>/` directly.
|
|
||||||
|
|
||||||
Single test / subset — either through CTest (each `TEST_CASE` is registered
|
|
||||||
individually by `catch_discover_tests`):
|
|
||||||
|
|
||||||
```sh
|
|
||||||
ctest --preset windows-msvc-debug -R "WeldVertices" -V
|
|
||||||
```
|
|
||||||
|
|
||||||
or by running the binary with a Catch2 name or tag filter:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
./build/windows-msvc-debug/tests/simv_tests.exe "[decimator]"
|
|
||||||
./build/windows-msvc-debug/tests/simv_tests.exe --list-tests
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Library / target map
|
|
||||||
|
|
||||||
```
|
|
||||||
SimVulcan (exe) → simv_core, simv_vk, simv_mesh, simv_editor
|
|
||||||
simv_core → simv_vk (App owns Window + vk::Renderer; no Vulkan calls)
|
|
||||||
simv_editor → simv_core, simv_mesh (Camera, input, ImGui panels; no Vulkan)
|
|
||||||
simv_mesh → simv_core (CPU mesh only; no Vulkan)
|
|
||||||
simv_vk → third-party (volk, vk-bootstrap, VMA, GLFW, GLM, spdlog, imgui)
|
|
||||||
simv_shaders → glslangValidator (GLSL → SPIR-V)
|
|
||||||
```
|
|
||||||
|
|
||||||
Namespaces follow directories: `simv::core`, `simv::vk`, `simv::mesh`,
|
|
||||||
`simv::editor`. Each library exports `src/` as its include root, so includes are
|
|
||||||
written module-qualified (`#include "mesh/Mesh.h"`, `#include "vk/Renderer.h"`).
|
|
||||||
|
|
||||||
### Frame loop
|
|
||||||
|
|
||||||
`main.cpp` creates `core::App`, which owns the `core::Window` and a
|
|
||||||
`vk::Renderer`, then runs the loop. Each frame `Renderer::DrawFrame` calls the UI
|
|
||||||
callback (between ImGui NewFrame/Render), then records the scene: grid + mesh into
|
|
||||||
one dynamic-rendering pass with a depth attachment, followed by ImGui, then
|
|
||||||
presents (2 frames in flight, sync2 submits).
|
|
||||||
|
|
||||||
`main.cpp` wires the editor via the UI callback: it draws the panels, applies
|
|
||||||
mouse input to the `editor::Camera`, and pushes the resulting state into the
|
|
||||||
renderer (`SetViewProj`, `SetRenderMode`, `SetGridVisible`). Mesh loads go through
|
|
||||||
`MeshLoadPanel`'s callback → `Renderer::SetMeshCpu`.
|
|
||||||
|
|
||||||
### Mesh load path
|
|
||||||
|
|
||||||
`MeshLoadPanel` lists `*.obj` in the mesh directory and kicks off
|
|
||||||
`mesh::LoadObjAsync` (worker thread, `std::future`). The future is **drained on
|
|
||||||
the main thread** at the top of `MeshLoadPanel::Draw`, so the `OnLoaded` callback
|
|
||||||
— and therefore the GPU upload — always runs on the render thread. Loader
|
|
||||||
exceptions surface as the panel's status string.
|
|
||||||
|
|
||||||
`main.cpp`'s `OnLoaded` welds the mesh (`WeldVertices`, tolerance `1e-4`), logs
|
|
||||||
the counts, uploads via `SetMeshCpu`, and reframes the camera to the bbox radius.
|
|
||||||
Despite the file name, `mesh/MeshDecimator.h` implements **only** spatial-hash
|
|
||||||
vertex welding (collapse near-duplicates, drop degenerate triangles, recompute
|
|
||||||
bounds) — there is no LOD/decimation.
|
|
||||||
|
|
||||||
### Non-obvious invariants (read before editing)
|
|
||||||
|
|
||||||
- **All Vulkan lives in `src/vk/`.** `core/`, `mesh/`, `editor/` and `app/` make no
|
|
||||||
Vulkan API calls. `vk::Renderer`'s public header is deliberately Vulkan-free
|
|
||||||
(pImpl + glm/`RenderMode` only) so `App` can own it without pulling in volk. Keep
|
|
||||||
it that way — do not leak `Vk*` types into the public interfaces of those modules.
|
|
||||||
- **Single render pass with depth.** Scene (grid + mesh) and ImGui draw into one
|
|
||||||
`vkCmdBeginRendering` pass that has both a colour and a `D32_SFLOAT` depth
|
|
||||||
attachment. The ImGui backend is initialised with `depthAttachmentFormat` set so
|
|
||||||
its pipeline matches the pass; the depth buffer is recreated with the swapchain.
|
|
||||||
- **Wireframe needs `fillModeNonSolid`.** `MeshRenderer` builds a fill pipeline and
|
|
||||||
a `VK_POLYGON_MODE_LINE` pipeline; the line pipeline uses a small depth bias so
|
|
||||||
the overlay sits on top of the fill. The device feature is requested in `Context`.
|
|
||||||
Culling is off (`VK_CULL_MODE_NONE`) — loaded models may have mixed winding.
|
|
||||||
- **Camera is fixed on the origin.** `editor::Camera` orbits (yaw/pitch/distance)
|
|
||||||
the world origin; loaded models are recentred there via a translate-only model
|
|
||||||
matrix. Projection uses Vulkan clip space (`GLM_FORCE_DEPTH_ZERO_TO_ONE` + Y flip,
|
|
||||||
isolated to `Camera.cpp`).
|
|
||||||
- **Buffers.** `vk::GpuMesh` owns the model's vertex/index buffers (staged upload on
|
|
||||||
the transfer queue). `GridRenderer` builds a static host-visible line buffer once.
|
|
||||||
Both scene renderers use only a push constant — no descriptor sets.
|
|
||||||
- **Push-constant layout is a cross-file contract.** `MeshRenderer.cpp`'s anonymous
|
|
||||||
`MeshPC { mat4 mvp; vec4 color; }` must stay byte-identical to the
|
|
||||||
`push_constant` block in `mesh.vert`/`mesh.frag`; `color.a` is a *flag*, not
|
|
||||||
alpha (1 = flat-shaded, 0 = constant colour for wireframe). `GridRenderer` pushes
|
|
||||||
a bare `mat4` (vertex stage only). Change either side and you must change both.
|
|
||||||
- **Swapchain recreation rebuilds sync objects.** `RecreateSwapDependent` recreates
|
|
||||||
the per-frame `imageAvailable` semaphores (a failed acquire can leave one
|
|
||||||
signalled) *and* the per-swapchain-image `renderFinished` semaphores (the image
|
|
||||||
count may change), then re-ensures both pipelines. Keep that ordering if you
|
|
||||||
touch resize handling.
|
|
||||||
- **Mesh upload stalls the device.** `SetMeshCpu`/`ClearMesh` call `WaitIdle`
|
|
||||||
before touching `GpuMesh` — acceptable because loads are rare; do not copy that
|
|
||||||
pattern into per-frame paths.
|
|
||||||
|
|
||||||
### Shaders
|
|
||||||
|
|
||||||
GLSL under `shaders/editor/` (`mesh.{vert,frag}`, `grid.{vert,frag}`). `simv_shaders`
|
|
||||||
compiles each to `build/<preset>/spirv/editor/<name>.spv` targeting `vulkan1.3`,
|
|
||||||
with `shaders/` as the `-I` root (so `#include "common/foo.glsl"` would resolve).
|
|
||||||
Adding a shader under that globbed dir is picked up automatically
|
|
||||||
(`CONFIGURE_DEPENDS`). `mesh.frag` reconstructs a flat normal from screen-space
|
|
||||||
derivatives, so the vertex stream carries only positions (tight `float3`, one
|
|
||||||
binding, one attribute).
|
|
||||||
|
|
||||||
## Logging
|
|
||||||
|
|
||||||
`simv::core::Logger` (spdlog-backed, singleton) with category-based throttling.
|
|
||||||
Log via `LogFmt(LogCategory, LogLevel, fmt, args...)`. Categories: `Core`,
|
|
||||||
`Vulkan`, `MeshIO`, `UI`, `Test`. Per-category level, throttle interval and
|
|
||||||
on/off are settable at runtime (`SetMinLevel` / `SetThrottle` / `SetEnabled`).
|
|
||||||
Some low-level Vulkan code still calls `spdlog::` directly.
|
|
||||||
|
|
||||||
## Research prototype (`docs/theory/`)
|
|
||||||
|
|
||||||
Separate from the C++ application and from the CMake build: a Python **Lattice
|
|
||||||
Boltzmann (D2Q9, KBC-N1)** research codebase — cylinder flow with a ×2 nested AMR
|
|
||||||
patch and SDF+Bouzidi boundaries. **GPU/CuPy only, no CPU fallback.** Its
|
|
||||||
documentation and the running experiment log are in Russian.
|
|
||||||
|
|
||||||
- `docs/theory/solver_2x_sdf/` — the component-split solver (`run.py`,
|
|
||||||
`run_blockage.py`, `run_factors.py`); its `README.md` is the authoritative
|
|
||||||
status/experiment log and maps every file to the physics it owns.
|
|
||||||
- `docs/theory/demos_gpu/` — demo runs that produce the notebook's figures/GIFs;
|
|
||||||
they import physics from `solver_2x_sdf`, never duplicate it.
|
|
||||||
- `docs/theory/kbc_lbm.ipynb` — the write-up; it embeds pre-rendered artefacts and
|
|
||||||
does not execute the simulations.
|
|
||||||
- `docs/origins/` — source PDFs behind the theory.
|
|
||||||
|
|
||||||
Do not fold this into the CMake build, and do not treat it as dead code — it is
|
|
||||||
active research with a documented experiment history.
|
|
||||||
@@ -11,16 +11,18 @@ reference grid planes through the origin, with `.obj` model loading and display.
|
|||||||
at the origin.
|
at the origin.
|
||||||
* `.obj` loading with three display modes — solid (flat-shaded), wireframe, and
|
* `.obj` loading with three display modes — solid (flat-shaded), wireframe, and
|
||||||
solid + wireframe overlay.
|
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.
|
All Vulkan code is isolated in `src/vk/`; the rest of the app is Vulkan-free.
|
||||||
|
|
||||||
## Targets
|
## Targets
|
||||||
|
|
||||||
* Windows x64 (MSVC / clang-cl)
|
* Windows x64 (MSVC)
|
||||||
* Linux x64 (gcc / clang)
|
* Linux x64 (gcc / clang)
|
||||||
* macOS arm64 (Apple Silicon, via MoltenVK)
|
* macOS arm64 (Apple Silicon, via MoltenVK)
|
||||||
|
|
||||||
|
The presets do not pin a compiler — each inherits whatever the environment provides.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
* Vulkan SDK 1.3.290+ (`https://vulkan.lunarg.com/`)
|
* 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
|
* Ninja
|
||||||
* C++20 compiler (MSVC 19.36+, gcc 11+, clang 14+)
|
* 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
|
## Build
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -50,8 +56,10 @@ ctest --preset windows-msvc-debug
|
|||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
core/ Logger, Window, App (orchestration, Vulkan-free)
|
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
|
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)
|
mesh/ CPU mesh: tinyobjloader + welding (no Vulkan)
|
||||||
editor/ Camera, mouse input, EditorUI + MeshLoadPanel (ImGui, no Vulkan)
|
editor/ Camera, mouse input, EditorUI + MeshLoadPanel (ImGui, no Vulkan)
|
||||||
app/ main.cpp
|
app/ main.cpp
|
||||||
|
|||||||
+48
-31
@@ -80,15 +80,21 @@
|
|||||||
| `SimVulcan.exe`, debug | 5.68 МБ | 28.97 МБ |
|
| `SimVulcan.exe`, debug | 5.68 МБ | 28.97 МБ |
|
||||||
| каталог сборки | 78 МБ | 469 МБ |
|
| каталог сборки | 78 МБ | 469 МБ |
|
||||||
| объявлено зависимостей | 9 | 15 |
|
| объявлено зависимостей | 9 | 15 |
|
||||||
| всего пакетов в графе | 10 | **76** |
|
| всего пакетов в графе | 9 | **82** |
|
||||||
| исходники зависимостей на диске | **775 МБ** на каждый каталог сборки | **80 МБ** в общем реестре |
|
| исходники зависимостей на диске | **~400 МБ** на каждый каталог сборки | **80 МБ** в общем реестре |
|
||||||
|
|
||||||
Пятикратная разница в размере бинаря — это в основном статически влинкованная стандартная
|
Пятикратная разница в размере бинаря — это в основном статически влинкованная стандартная
|
||||||
библиотека Rust и форматирование `core::fmt`; C++-версия тянет CRT из системы. Разница в
|
библиотека Rust и форматирование `core::fmt`; C++-версия тянет CRT из системы. Разница в
|
||||||
числе пакетов (76 против 10) впечатляет ровно до того момента, как посмотреть на объём:
|
числе пакетов (82 против 9) впечатляет ровно до того момента, как посмотреть на объём:
|
||||||
76 крейтов Rust занимают в восемь раз меньше места, чем девять библиотек C++, потому что
|
82 крейта Rust занимают в пять раз меньше места, чем девять библиотек C++, потому что
|
||||||
`.crate` — это архив выпуска, а `FetchContent` — клон с историей.
|
`.crate` — это архив выпуска, а `FetchContent` — клон с историей.
|
||||||
|
|
||||||
|
> **Поправка.** В первой редакции здесь стояли «775 МБ» и «10 пакетов». Обе цифры сняты
|
||||||
|
> с каталога `build/`, сконфигурированного из другого дерева исходников: его `_deps/`
|
||||||
|
> содержит 262 МБ `nlohmann_json`, которого этот проект не объявляет. Девять объявленных
|
||||||
|
> репозиториев дают около 400 МБ исходников и около 510 МБ всего `_deps` после сборки.
|
||||||
|
> Число пакетов Rust получено `cargo tree -e normal` по текущему `Cargo.lock`.
|
||||||
|
|
||||||
### Выполнение
|
### Выполнение
|
||||||
|
|
||||||
Обе версии показывают в режиме FIFO на экране 60 Гц, поэтому частота кадров у них
|
Обе версии показывают в режиме FIFO на экране 60 Гц, поэтому частота кадров у них
|
||||||
@@ -122,14 +128,16 @@ C++: egui пересобирает раскладку каждый кадр, ImG
|
|||||||
* **35 вершин.** tinyobjloader отдаёт глобальный массив `attrib.vertices` целиком, включая
|
* **35 вершин.** tinyobjloader отдаёт глобальный массив `attrib.vertices` целиком, включая
|
||||||
вершины, на которые не ссылается ни одна грань; tobj такие отбрасывает. Сварка потом
|
вершины, на которые не ссылается ни одна грань; tobj такие отбрасывает. Сварка потом
|
||||||
всё равно оставила бы их висеть в списке — на картинку они не влияют.
|
всё равно оставила бы их висеть в списке — на картинку они не влияют.
|
||||||
* **26 треугольников.** В `plane.obj` 121 грань с пятью и более вершинами. tobj режет
|
* **26 треугольников.** В `plane.obj` 119 граней с пятью и более вершинами. tobj режет
|
||||||
n-угольник веером, что даёт ровно `n − 2` треугольника — суммарно 20061, и это
|
n-угольник веером, что даёт ровно `n − 2` треугольника — суммарно 20061, и это
|
||||||
совпадает с прямым подсчётом по файлу. tinyobjloader для `n ≥ 5` применяет
|
совпадает с прямым подсчётом по файлу. tinyobjloader для `n ≥ 5` применяет
|
||||||
earcut-подобную триангуляцию и выбрасывает выродившиеся треугольники, отсюда 20035.
|
earcut-подобную триангуляцию и выбрасывает выродившиеся треугольники, отсюда 20035.
|
||||||
Разница 0.13% и целиком лежит в библиотеке чтения, а не в переносе.
|
Разница 0.13% и целиком лежит в библиотеке чтения, а не в переносе.
|
||||||
|
|
||||||
Сварка вершин (`weld_vertices`) перенесена построчно, включая бакетирование округлением
|
Сварка вершин (`weld_vertices`) перенесена построчно, включая бакетирование округлением
|
||||||
и FNV-хеш; три её теста из Catch2 перенесены дословно и проходят.
|
и FNV-хеш. Все три случая Catch2 перенесены дословно и проходят; сверх них в порт
|
||||||
|
добавлены ещё два — бакетирование округлением, а не отбрасыванием, и габариты пустого
|
||||||
|
меша.
|
||||||
|
|
||||||
### Побочная находка: строка, которой не бывает
|
### Побочная находка: строка, которой не бывает
|
||||||
|
|
||||||
@@ -155,22 +163,25 @@ C++: egui пересобирает раскладку каждый кадр, ImG
|
|||||||
| | всего | пусто | комм. | **кода** | всего | пусто | комм. | **кода** |
|
| | всего | пусто | комм. | **кода** | всего | пусто | комм. | **кода** |
|
||||||
| журнал, окно (`core`) | 370 | 83 | 11 | **276** | 310 | 36 | 62 | **212** |
|
| журнал, окно (`core`) | 370 | 83 | 11 | **276** | 310 | 36 | 62 | **212** |
|
||||||
| Vulkan (`vk`) | 2120 | 380 | 88 | **1652** | 2491 | 238 | 255 | **1998** |
|
| 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** |
|
| редактор (`editor`) | 305 | 68 | 20 | **217** | 431 | 48 | 62 | **321** |
|
||||||
| приложение (`app`) | 85 | 14 | 5 | **66** | 226 | 27 | 43 | **156** |
|
| приложение (`app`) | 85 | 14 | 5 | **66** | 226 | 27 | 43 | **156** |
|
||||||
| тесты | 65 | 9 | 5 | **51** | *внутри модулей* | | | *≈150* |
|
| тесты | 65 | 9 | 5 | **51** | *внутри модулей* | | | *176* |
|
||||||
| **итого** | **3181** | 604 | 143 | **2434** | **3853** | 399 | 492 | **2962** |
|
| **итого** | **3181** | 604 | 143 | **2434** | **3870** | 400 | 494 | **2976** |
|
||||||
| система сборки | 431 | | | **399** | 232 | | | **197** |
|
| система сборки | 431 | | | **399** | 232 | | | **197** |
|
||||||
|
|
||||||
Порт длиннее примерно на пятую часть, и перевес почти весь в Vulkan-слое: +346 строк кода
|
Порт длиннее примерно на пятую часть, и большая часть перевеса — в Vulkan-слое: +346
|
||||||
в `simv-vk` — это написанная руками замена vk-bootstrap. Приложение выросло с 66 до 156
|
строк кода в `simv-vk` — это написанная руками замена vk-bootstrap. Но «почти весь» было
|
||||||
строк, потому что в него переехал `App` из `simv_core` (см. расхождение 1) и поиск
|
бы преувеличением: из 542 строк общего перевеса на Vulkan-слой приходится 346 (64 %), а
|
||||||
каталога моделей стал честным обходом предков вместо пяти захардкоженных `../`.
|
внутри него на `Context` + `Swapchain` — 305 (56 % от общего). Остальное настоящее: меш
|
||||||
|
+117, редактор +104, приложение +90, `core` −64. Приложение выросло с 66 до 156 строк,
|
||||||
|
потому что в него переехал `App` из `simv_core` (см. расхождение 1) и поиск каталога
|
||||||
|
моделей стал честным обходом предков вместо пяти захардкоженных `../`.
|
||||||
|
|
||||||
Комментариев в порте втрое больше (492 против 143), и это перекос замера, а не свойство
|
Комментариев в порте втрое больше (494 против 143), и это перекос замера, а не свойство
|
||||||
языка: значительная их часть — пометки «здесь расхождение с C++-версией и вот почему»,
|
языка: значительная их часть — пометки «здесь расхождение с C++-версией и вот почему»,
|
||||||
написанные ради этого документа. По строкам собственно кода разрыв — 2962 против 2434,
|
написанные ради этого документа. По строкам собственно кода разрыв — 2976 против 2434,
|
||||||
и он меньше, если вычесть тесты: они в Rust живут внутри модулей (≈150 строк), в C++ —
|
и он меньше, если вычесть тесты: они в Rust живут внутри модулей (176 строк), в C++ —
|
||||||
отдельной целью (51 строка).
|
отдельной целью (51 строка).
|
||||||
|
|
||||||
## Структурные расхождения
|
## Структурные расхождения
|
||||||
@@ -179,10 +190,11 @@ C++: egui пересобирает раскладку каждый кадр, ImG
|
|||||||
|
|
||||||
### 1. `App` переехал в исполняемый крейт
|
### 1. `App` переехал в исполняемый крейт
|
||||||
|
|
||||||
В C++ граф целей цикличен: `core::App` владеет `vk::Renderer`, а `vk::Renderer`
|
В C++ цикличны исходники: `core::App` владеет `vk::Renderer`, а `vk::Renderer`
|
||||||
принимает `core::Window&`. Работает это только потому, что `simv_vk` **не линкует**
|
принимает `core::Window&`. Граф целей CMake при этом ацикличен — потому CMake ни на что
|
||||||
`simv_core`, а видит его заголовки через `target_include_directories(simv_vk PUBLIC ..)`,
|
и не жалуется: `simv_vk` **не линкует** `simv_core`, а видит его заголовки через
|
||||||
и всё сходится на компоновке исполняемого файла. Cargo цикл между крейтами отвергает
|
`target_include_directories(simv_vk PUBLIC ..)`, и всё сходится на компоновке
|
||||||
|
исполняемого файла. Cargo цикл между крейтами отвергает
|
||||||
сразу, поэтому `App` (25 строк) поднят на уровень выше обоих — в `simv-app`.
|
сразу, поэтому `App` (25 строк) поднят на уровень выше обоих — в `simv-app`.
|
||||||
|
|
||||||
Это единственный пункт, где Rust потребовал перекладывать код, и заодно единственный,
|
Это единственный пункт, где Rust потребовал перекладывать код, и заодно единственный,
|
||||||
@@ -270,8 +282,9 @@ renderer.draw_frame(window, |ctx| {
|
|||||||
`include_bytes!`. Вместе с этим исчезли: функция `FindSpvPath` с перебором путей, два
|
`include_bytes!`. Вместе с этим исчезли: функция `FindSpvPath` с перебором путей, два
|
||||||
POST_BUILD-шага копирования каталогов в `CMakeLists.txt`, требование запускать редактор
|
POST_BUILD-шага копирования каталогов в `CMakeLists.txt`, требование запускать редактор
|
||||||
из его собственного каталога и целый класс ошибок «шейдер не найден» во время выполнения
|
из его собственного каталога и целый класс ошибок «шейдер не найден» во время выполнения
|
||||||
— отсутствующий шейдер теперь ломает сборку. Из путевых зависимостей остался только
|
— отсутствующий шейдер теперь ломает сборку. Путевых зависимостей осталось две:
|
||||||
`assets/meshes`, и он ищется от бинаря, а не от текущего каталога.
|
`assets/meshes`, который ищется подъёмом по предкам бинаря и лишь затем — по предкам
|
||||||
|
текущего каталога, и `pipeline_cache.bin`, который пишется рядом с бинарём.
|
||||||
|
|
||||||
### 9. Цикл событий принадлежит библиотеке
|
### 9. Цикл событий принадлежит библиотеке
|
||||||
|
|
||||||
@@ -333,8 +346,8 @@ let (f13, f12, f10) = {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
**Rust помог — порядок уничтожения.** В C++ `Renderer::Impl::~Impl` — это тридцать строк
|
**Rust помог — порядок уничтожения.** В C++ `Renderer::Impl::~Impl` — это двадцать две
|
||||||
ручного разрушения в правильном порядке, и любая перестановка полей в объявлении структуры
|
строки ручного разрушения в правильном порядке, и любая перестановка полей в объявлении структуры
|
||||||
её не сломает, но и не поможет: связи нет. В Rust порядок полей **и есть** порядок
|
её не сломает, но и не поможет: связи нет. В Rust порядок полей **и есть** порядок
|
||||||
уничтожения, а `Drop` у каждой обёртки снимает вопрос «а это уже освободили?». Ловушка
|
уничтожения, а `Drop` у каждой обёртки снимает вопрос «а это уже освободили?». Ловушка
|
||||||
осталась одна и она подписана в коде: `ash::Device` клонируется как таблица функций, а не
|
осталась одна и она подписана в коде: `ash::Device` клонируется как таблица функций, а не
|
||||||
@@ -356,14 +369,18 @@ C++ тоже опасен, не притворяясь, что их нет.
|
|||||||
|
|
||||||
## Итог
|
## Итог
|
||||||
|
|
||||||
Ни один из замеров не даёт разницы в порядок величины, так что решение к таблице не
|
Кроме одной клетки, ни один замер не даёт разницы в порядок величины, так что решение к
|
||||||
сводится. Что можно утверждать по итогам порта:
|
таблице не сводится. Исключение — инкрементальная сборка release: 52.3 с против 4.4 с,
|
||||||
|
это 11.9×, и объясняется оно thin LTO, а не языком (разбор ниже). Следующие по величине
|
||||||
|
разрывы уже укладываются в порядок: исходники зависимостей ~5×, размер бинаря 4.9×.
|
||||||
|
Что можно утверждать по итогам порта:
|
||||||
|
|
||||||
**Порт занял примерно на пятую часть больше строк**, и почти весь перевес пришёлся на
|
**Порт занял примерно на пятую часть больше строк**, и бо́льшая часть перевеса пришлась на
|
||||||
`simv-vk`. Причина одна и она названа выше: замены vk-bootstrap нет, и 301 строка
|
`simv-vk`. Главная причина названа выше: замены vk-bootstrap нет, и 301 строка
|
||||||
`Context` с `Swapchain` превратилась в 606. Остальной Vulkan-слой — запись кадра, барьеры,
|
`Context` с `Swapchain` превратилась в 606 — это 305 строк из 542 общего перевеса, то есть
|
||||||
сборка конвейеров — совпадает почти строка в строку. Если проект растёт дальше именно в
|
чуть больше половины, а весь Vulkan-слой даёт 346 (64 %). Остальной Vulkan-слой — запись
|
||||||
Vulkan-часть, перевес разовый: инициализация пишется один раз.
|
кадра, барьеры, сборка конвейеров — совпадает почти строка в строку. Если проект растёт
|
||||||
|
дальше именно в Vulkan-часть, эта доля перевеса разовая: инициализация пишется один раз.
|
||||||
|
|
||||||
**Сборка: паритет везде, кроме одной клетки.** Полная сборка release у C++ вдвое быстрее
|
**Сборка: паритет везде, кроме одной клетки.** Полная сборка release у C++ вдвое быстрее
|
||||||
(114 с против 194 с), полная debug — наоборот, медленнее (112 с против 83 с),
|
(114 с против 194 с), полная debug — наоборот, медленнее (112 с против 83 с),
|
||||||
|
|||||||
@@ -31,8 +31,10 @@
|
|||||||
## Где какая математика (для ревизии)
|
## Где какая математика (для ревизии)
|
||||||
|
|
||||||
- **Столкновение** — `collision.py`. KBC-N1: `f ← f − β(2Δs + γΔh)`, где `Δs = Ps·(f−feq)` —
|
- **Столкновение** — `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`. Чистая пул-схема; на физику влияет только корректность сдвигов.
|
- **Перенос** — `streaming.py`. Чистая пул-схема; на физику влияет только корректность сдвигов.
|
||||||
- **Силы** — `forces.py`. Обмен импульсом по линкам тела:
|
- **Силы** — `forces.py`. Обмен импульсом по линкам тела:
|
||||||
`F = Σ_links c_i (f_i^{после столкн.} + f_ī^{после стриминга})`. Второй член — из поля ПОСЛЕ
|
`F = Σ_links c_i (f_i^{после столкн.} + f_ī^{после стриминга})`. Второй член — из поля ПОСЛЕ
|
||||||
|
|||||||
+38
-20
@@ -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 1.3.290+ — нужен только `glslangValidator` для сборки шейдеров
|
||||||
(ищется в `%VULKAN_SDK%\Bin`, затем в `PATH`)
|
(ищется в `%VULKAN_SDK%\Bin`, затем в `PATH`)
|
||||||
* Драйвер Vulkan 1.3 с `fillModeNonSolid`
|
* Драйвер Vulkan 1.3 с `fillModeNonSolid`
|
||||||
@@ -24,8 +26,10 @@ cargo build --release
|
|||||||
```
|
```
|
||||||
|
|
||||||
Запускать можно из любого каталога. Шейдеры вшиты в исполняемый файл на этапе сборки,
|
Запускать можно из любого каталога. Шейдеры вшиты в исполняемый файл на этапе сборки,
|
||||||
а `assets/meshes` ищется подъёмом по предкам от самого бинаря — в отличие от
|
а `assets/meshes` ищется подъёмом по предкам от самого бинаря и только затем — от
|
||||||
C++-версии, которой нужен запуск из её собственного каталога.
|
текущего каталога; в отличие от C++-версии, которой нужен запуск из её собственного
|
||||||
|
каталога. В отличие от неё же, `assets/` никуда не копируется: порт находит копию в
|
||||||
|
корне репозитория, поднимаясь от `target/<профиль>/`.
|
||||||
|
|
||||||
Рядом с исполняемым файлом создаётся `pipeline_cache.bin` — сериализованный
|
Рядом с исполняемым файлом создаётся `pipeline_cache.bin` — сериализованный
|
||||||
`VkPipelineCache`, он перечитывается при следующем запуске. Файл одноразовый: если
|
`VkPipelineCache`, он перечитывается при следующем запуске. Файл одноразовый: если
|
||||||
@@ -45,21 +49,34 @@ C++-версии, которой нужен запуск из её собств
|
|||||||
cargo test
|
cargo test
|
||||||
```
|
```
|
||||||
|
|
||||||
Чистая математика без видеокарты: габариты меша, сварка вершин, чтение `Cube.obj`,
|
Видеокарта не нужна: габариты меша, сварка вершин (три случая), чтение `Cube.obj` и его
|
||||||
пространство отсечения камеры. Тесты живут прямо в модулях (`#[cfg(test)] mod tests`),
|
путь ошибки, пространство отсечения камеры и ограничители наклона и приближения,
|
||||||
отдельной цели под них нет.
|
разбор категорий журнала — тринадцать тестов плюс один помеченный `#[ignore]`
|
||||||
|
диагностический, тот самый, которым получена таблица расхождения загрузчиков. Чистой
|
||||||
|
математикой это, впрочем, не является: тесты загрузчика читают настоящие файлы из
|
||||||
|
`assets/meshes` через `$CARGO_MANIFEST_DIR`, поэтому им нужен рабочий клон репозитория.
|
||||||
|
Тесты живут прямо в модулях (`#[cfg(test)] mod tests`), отдельной цели под них нет.
|
||||||
|
|
||||||
## Раскладка
|
## Раскладка
|
||||||
|
|
||||||
Пять крейтов повторяют карту целей CMake из корня репозитория:
|
Пять крейтов повторяют карту целей CMake из корня репозитория:
|
||||||
|
|
||||||
|
Ниже перечислены сторонние зависимости; вдобавок каждый крейт зависит от тех крейтов
|
||||||
|
рабочего пространства, что указаны в скобках.
|
||||||
|
|
||||||
```
|
```
|
||||||
crates/simv-core/ журнал с категориями, окно → log, chrono, winit
|
crates/simv-core/ журнал с категориями, окно → log, chrono, winit
|
||||||
crates/simv-mesh/ чтение .obj, сварка вершин, габариты → glam, tobj
|
crates/simv-mesh/ чтение .obj, сварка, габариты → glam, tobj, log, thiserror
|
||||||
crates/simv-vk/ весь Vulkan + build.rs (GLSL → SPIR-V) → ash, gpu-allocator,
|
(+ simv-core)
|
||||||
egui-ash-renderer
|
crates/simv-vk/ весь Vulkan + build.rs → ash, ash-window, raw-window-handle,
|
||||||
crates/simv-editor/ камера, ввод, панели → egui
|
(GLSL → SPIR-V) gpu-allocator, egui, egui-winit,
|
||||||
crates/simv-app/ App и точка входа → всё вышеперечисленное
|
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 живёт в одном месте» — здесь проверяет
|
Главный инвариант проекта — «весь Vulkan живёт в одном месте» — здесь проверяет
|
||||||
@@ -67,7 +84,7 @@ crates/simv-app/ App и точка входа
|
|||||||
так что нарушить границу нельзя даже по невнимательности.
|
так что нарушить границу нельзя даже по невнимательности.
|
||||||
|
|
||||||
Шейдеры общие с C++-версией: `build.rs` крейта `simv-vk` компилирует
|
Шейдеры общие с 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!`.
|
и кладёт SPIR-V в `OUT_DIR`, откуда он попадает в бинарь через `include_bytes!`.
|
||||||
|
|
||||||
## Чем отличается от C++-версии
|
## Чем отличается от C++-версии
|
||||||
@@ -78,15 +95,16 @@ Dear ImGui на C++, что обесценило бы сравнение.
|
|||||||
|
|
||||||
Внутри отличия сведены в отдельный документ, здесь только самые заметные:
|
Внутри отличия сведены в отдельный документ, здесь только самые заметные:
|
||||||
|
|
||||||
* **`App` живёт в исполняемом крейте**, а не в `simv-core`. В C++ граф целей цикличен
|
* **`App` живёт в исполняемом крейте**, а не в `simv-core`. В C++ цикличны исходники
|
||||||
(`core::App` владеет `vk::Renderer`, `vk::Renderer` принимает `core::Window&`), и
|
(`core::App` владеет `vk::Renderer`, `vk::Renderer` принимает `core::Window&`), тогда
|
||||||
держится это лишь на том, что `simv_vk` не линкует `simv_core`, а видит его заголовки.
|
как граф целей CMake ацикличен — потому CMake и молчит: `simv_vk` не линкует
|
||||||
Cargo цикл между крейтами отвергает.
|
`simv_core`, а лишь видит его заголовки, и символы `Window` находятся при сборке
|
||||||
|
исполняемого файла. Cargo такую схему отвергает.
|
||||||
* **Состояние сцены возвращается из замыкания интерфейса**, а не ставится сеттерами.
|
* **Состояние сцены возвращается из замыкания интерфейса**, а не ставится сеттерами.
|
||||||
Замыкание вызывается из метода рендерера, поэтому трогать рендерер оттуда нельзя.
|
Замыкание вызывается из метода рендерера, поэтому трогать рендерер оттуда нельзя.
|
||||||
* **Загруженный меш выгружается на видеокарту после кадра**, а не из середины записи
|
* **Загруженный меш выгружается на видеокарту после кадра**, а не изнутри него, как в
|
||||||
команд, как в C++, где колбэк панели дёргает `vkDeviceWaitIdle` уже после захвата
|
C++, где колбэк панели дёргает `vkDeviceWaitIdle` уже после захвата образа цепочки
|
||||||
образа цепочки показа.
|
показа — хотя запись команд там ещё не началась, `vkBeginCommandBuffer` идёт позже.
|
||||||
* **`Buffer` и `Image` задействованы.** В C++ обе обёртки собираются в цель `simv_vk`, но
|
* **`Buffer` и `Image` задействованы.** В C++ обе обёртки собираются в цель `simv_vk`, но
|
||||||
ими не пользуется никто.
|
ими не пользуется никто.
|
||||||
* **Нет pImpl.** Приватные поля модуля дают ту же изоляцию, ради которой в C++ заведён
|
* **Нет pImpl.** Приватные поля модуля дают ту же изоляцию, ради которой в C++ заведён
|
||||||
|
|||||||
Reference in New Issue
Block a user