Порт редактора SimVulcan на Rust
Та же программа на ash + winit + egui: орбитальная камера, три плоскости сетки, загрузка .obj, три режима отображения, единый проход динамического рендеринга с глубинным вложением и submit через synchronization2. Шейдеры общие с C++-версией — build.rs компилирует их из shaders/ и вшивает SPIR-V в бинарь. Пять крейтов повторяют карту целей CMake, но инвариант «весь Vulkan в одном месте» теперь проверяет компилятор: у simv-mesh и simv-editor нет ash в зависимостях. Расхождения, вынужденные языком, а не вкусом: * App переехал в исполняемый крейт — в C++ граф целей цикличен (core владеет vk, vk принимает core::Window&), и держится это лишь на том, что simv_vk видит заголовки core, не линкуя цель; Cargo такой цикл отвергает; * состояние сцены возвращается из замыкания интерфейса, а не ставится сеттерами: замыкание вызывается из метода рендерера, трогать рендерер оттуда нельзя; * выгрузка меша на видеокарту уехала из середины записи кадра, где C++-версия зовёт vkDeviceWaitIdle уже после захвата образа цепочки показа. Vulkan-контекст и цепочка показа написаны руками: замены vk-bootstrap в экосистеме Rust нет. Обёртки Buffer и Image, лежащие в C++-версии без применения, здесь задействованы всеми потребителями. 13 тестов, из них три перенесены из Catch2 дословно. cargo clippy -D warnings чист, слой валидации молчит. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
//! Владелец времени жизни: окно, рендерер, камера, панели — перенос `simv::core::App`
|
||||
//! вместе с обвязкой из `main.cpp`.
|
||||
//!
|
||||
//! Живёт в исполняемом крейте, а не в `simv-core`, где ему соответствовал бы прообраз.
|
||||
//! Причина в графе зависимостей: в C++ `simv_core` содержит `App`, который владеет
|
||||
//! `vk::Renderer`, а `simv_vk` принимает `core::Window&` — то есть цели ссылаются друг
|
||||
//! на друга по кругу. Сходится это лишь потому, что `simv_vk` не линкует `simv_core`, а
|
||||
//! видит его заголовки через `target_include_directories`, и всё склеивается на
|
||||
//! компоновке исполняемого файла. Cargo цикл между крейтами отвергает, так что `App`
|
||||
//! пришлось поднять на уровень выше обоих.
|
||||
//!
|
||||
//! Второе отличие — сам цикл. У GLFW его пишет приложение
|
||||
//! (`while (!ShouldClose()) { PollEvents(); DrawFrame(); }`), у winit цикл владеет
|
||||
//! собой и зовёт приложение через `ApplicationHandler`. Кадр по-прежнему рисуется
|
||||
//! непрерывно: `about_to_wait` каждый раз просит перерисовку.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use winit::application::ApplicationHandler;
|
||||
use winit::event::WindowEvent;
|
||||
use winit::event_loop::{ActiveEventLoop, ControlFlow};
|
||||
use winit::window::WindowId;
|
||||
|
||||
use simv_core::{Window, WindowDesc};
|
||||
use simv_editor::{apply_mouse_input, Camera, DisplayMode, EditorUi, MeshLoadPanel};
|
||||
use simv_mesh::weld_vertices;
|
||||
use simv_vk::{FrameState, RenderMode, Renderer};
|
||||
|
||||
/// Допуск сварки вершин после загрузки — как в C++-версии.
|
||||
const WELD_TOLERANCE: f32 = 1e-4;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct App {
|
||||
window: Option<Window>,
|
||||
renderer: Option<Renderer>,
|
||||
camera: Camera,
|
||||
editor_ui: EditorUi,
|
||||
mesh_panel: MeshLoadPanel,
|
||||
/// Ошибка, из-за которой цикл был прерван. Возвращается из `main` кодом выхода.
|
||||
failure: Option<anyhow::Error>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// Забрать причину аварийного завершения, если она была.
|
||||
pub fn into_result(self) -> anyhow::Result<()> {
|
||||
match self.failure {
|
||||
Some(e) => Err(e),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn fail(&mut self, event_loop: &ActiveEventLoop, error: anyhow::Error) {
|
||||
log::error!(target: "core", "{error:#}");
|
||||
self.failure = Some(error);
|
||||
event_loop.exit();
|
||||
}
|
||||
|
||||
/// Отдать событие интерфейсу. `true` — egui забрал его себе.
|
||||
fn feed_ui(&mut self, event: &WindowEvent) -> bool {
|
||||
let Some(window) = self.window.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
let Some(renderer) = self.renderer.as_mut() else {
|
||||
return false;
|
||||
};
|
||||
renderer.on_window_event(window, event)
|
||||
}
|
||||
|
||||
fn draw(&mut self) -> anyhow::Result<()> {
|
||||
let Some(window) = self.window.as_ref() else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(renderer) = self.renderer.as_mut() else {
|
||||
return Ok(());
|
||||
};
|
||||
// Отношение сторон внутри замыкания не спросить — рендерер там уже одолжен,
|
||||
// но за кадр оно и не меняется.
|
||||
let aspect = renderer.aspect_ratio();
|
||||
|
||||
let camera = &mut self.camera;
|
||||
let editor_ui = &mut self.editor_ui;
|
||||
let mesh_panel = &mut self.mesh_panel;
|
||||
|
||||
renderer.draw_frame(window, |ctx| {
|
||||
editor_ui.draw(ctx, camera);
|
||||
mesh_panel.draw(ctx);
|
||||
apply_mouse_input(ctx, camera);
|
||||
FrameState {
|
||||
view_proj: camera.view_proj(aspect),
|
||||
render_mode: to_render_mode(editor_ui.display_mode()),
|
||||
grid_visible: editor_ui.grid_visible(),
|
||||
}
|
||||
})?;
|
||||
|
||||
// Готовый меш забирается ПОСЛЕ кадра. В C++ то же самое делалось прямо из
|
||||
// колбэка панели, то есть из середины `DrawFrame`, уже после захвата образа
|
||||
// цепочки показа: `SetMeshCpu` там дёргает `vkDeviceWaitIdle` внутри записи
|
||||
// кадра. Здесь так нельзя (рендерер одолжен), и получилось честнее.
|
||||
if let Some(mut mesh) = mesh_panel.take_loaded() {
|
||||
weld_vertices(&mut mesh, WELD_TOLERANCE);
|
||||
log::info!(
|
||||
target: "mesh",
|
||||
"меш готов: {} вершин, {} треугольников",
|
||||
mesh.vertex_count(),
|
||||
mesh.triangle_count()
|
||||
);
|
||||
renderer.set_mesh_cpu(&mesh)?;
|
||||
// Подогнать камеру под размер модели: она перенесена в начало координат.
|
||||
let radius = 0.5 * (mesh.bbox_max - mesh.bbox_min).length();
|
||||
if radius > 0.0 {
|
||||
camera.set_distance(radius * 2.5);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationHandler for App {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
if self.window.is_some() {
|
||||
return; // возврат из фона — окно уже есть
|
||||
}
|
||||
event_loop.set_control_flow(ControlFlow::Poll);
|
||||
|
||||
let window = match Window::new(event_loop, &WindowDesc::default()) {
|
||||
Ok(w) => w,
|
||||
Err(e) => return self.fail(event_loop, anyhow::anyhow!("не создать окно: {e}")),
|
||||
};
|
||||
let renderer = match Renderer::new(&window) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return self.fail(event_loop, anyhow::anyhow!("не запустить рендерер: {e}"))
|
||||
}
|
||||
};
|
||||
|
||||
let mesh_dir = find_mesh_directory();
|
||||
log::info!(target: "core", "каталог моделей: {}", mesh_dir.display());
|
||||
self.mesh_panel.set_mesh_directory(mesh_dir);
|
||||
|
||||
self.window = Some(window);
|
||||
self.renderer = Some(renderer);
|
||||
log::info!(target: "core", "SimVulcan запущен. Закройте окно, чтобы выйти.");
|
||||
}
|
||||
|
||||
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
|
||||
self.feed_ui(&event);
|
||||
|
||||
match event {
|
||||
WindowEvent::CloseRequested => event_loop.exit(),
|
||||
WindowEvent::RedrawRequested => {
|
||||
if let Err(e) = self.draw() {
|
||||
self.fail(event_loop, e);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
|
||||
if let Some(window) = &self.window {
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
|
||||
if let Some(renderer) = &self.renderer {
|
||||
renderer.wait_idle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Панель редактора не знает про `simv_vk` (крейт ей недоступен), поэтому разбор
|
||||
/// делается здесь. Добавите вариант в [`DisplayMode`] — компилятор укажет на этот
|
||||
/// `match`; в C++ на его месте `static_cast<vk::RenderMode>(int)`, который смолчит.
|
||||
fn to_render_mode(mode: DisplayMode) -> RenderMode {
|
||||
match mode {
|
||||
DisplayMode::Solid => RenderMode::Solid,
|
||||
DisplayMode::Wireframe => RenderMode::Wireframe,
|
||||
DisplayMode::SolidWire => RenderMode::SolidWire,
|
||||
}
|
||||
}
|
||||
|
||||
/// Найти `assets/meshes`, поднимаясь от исполняемого файла, а затем от текущего
|
||||
/// каталога.
|
||||
///
|
||||
/// C++-версия перебирает пять относительных путей вида `../../../../../assets/meshes`
|
||||
/// от текущего каталога — то есть работает, только если запускать из каталога бинаря.
|
||||
/// Здесь отсчёт идёт в первую очередь от самого бинаря, так что запуск из любого места
|
||||
/// находит модели. Шейдеров этот поиск не касается: они вшиты в исполняемый файл.
|
||||
fn find_mesh_directory() -> PathBuf {
|
||||
let mut roots: Vec<PathBuf> = Vec::new();
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
roots.extend(exe.ancestors().map(PathBuf::from));
|
||||
}
|
||||
if let Ok(cwd) = std::env::current_dir() {
|
||||
roots.extend(cwd.ancestors().map(PathBuf::from));
|
||||
}
|
||||
for root in roots {
|
||||
let candidate = root.join("assets").join("meshes");
|
||||
if candidate.is_dir() {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
PathBuf::from("assets/meshes")
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//! Точка входа редактора SimVulcan — перенос `src/app/main.cpp`.
|
||||
//!
|
||||
//! В C++ здесь же собирается вся обвязка редактора: поиск каталога моделей, обработчик
|
||||
//! загрузки, UI-колбэк. Всё это переехало в [`app::App`], потому что при модели
|
||||
//! событий winit его методы и есть тело цикла.
|
||||
|
||||
mod app;
|
||||
|
||||
use winit::event_loop::EventLoop;
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
simv_core::logger::init();
|
||||
|
||||
let event_loop = EventLoop::new()?;
|
||||
let mut app = app::App::default();
|
||||
event_loop.run_app(&mut app)?;
|
||||
|
||||
// Ошибка, из-за которой цикл прервался, возвращается сюда: в отличие от
|
||||
// C++-версии её не приходится ловить `catch (...)` — она пришла значением.
|
||||
app.into_result()
|
||||
}
|
||||
Reference in New Issue
Block a user