Порт редактора 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,26 @@
|
||||
# Аналог цели simv_vk: единственный крейт, которому позволено видеть ash.
|
||||
# build.rs компилирует общие с C++-версией шейдеры из ../../shaders в SPIR-V.
|
||||
[package]
|
||||
name = "simv-vk"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
description = "Весь Vulkan: контекст, swapchain, конвейеры, кадр"
|
||||
build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
simv-core.workspace = true
|
||||
simv-mesh.workspace = true
|
||||
ash.workspace = true
|
||||
ash-window.workspace = true
|
||||
raw-window-handle.workspace = true
|
||||
gpu-allocator.workspace = true
|
||||
egui.workspace = true
|
||||
egui-winit.workspace = true
|
||||
egui-ash-renderer.workspace = true
|
||||
winit.workspace = true
|
||||
glam.workspace = true
|
||||
bytemuck.workspace = true
|
||||
log.workspace = true
|
||||
thiserror.workspace = true
|
||||
@@ -0,0 +1,95 @@
|
||||
//! Компиляция GLSL в SPIR-V на этапе сборки — аналог `cmake/ShaderCompile.cmake`.
|
||||
//!
|
||||
//! Шейдеры берутся из общего с C++-версией каталога `shaders/` в корне репозитория:
|
||||
//! обе сборки компилируют одни и те же файлы теми же ключами, поэтому расхождение в
|
||||
//! картинке нельзя списать на шейдер.
|
||||
//!
|
||||
//! В отличие от CMake результат не копируется рядом с исполняемым файлом, а зашивается
|
||||
//! в бинарь через `include_bytes!`. Отсюда исчезают и `FindSpvPath`, и POST_BUILD-копия,
|
||||
//! и требование «запускать из каталога исполняемого файла».
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
let manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
|
||||
// crates/simv-vk → rust → корень репозитория.
|
||||
let shader_root = manifest.join("../../../shaders");
|
||||
let shader_root = shader_root.canonicalize().unwrap_or(shader_root);
|
||||
let editor_dir = shader_root.join("editor");
|
||||
|
||||
println!("cargo:rerun-if-env-changed=VULKAN_SDK");
|
||||
println!("cargo:rerun-if-changed={}", editor_dir.display());
|
||||
|
||||
let compiler = find_glslang();
|
||||
|
||||
let mut sources: Vec<PathBuf> = std::fs::read_dir(&editor_dir)
|
||||
.unwrap_or_else(|e| panic!("не читается каталог шейдеров {}: {e}", editor_dir.display()))
|
||||
.filter_map(|entry| entry.ok().map(|e| e.path()))
|
||||
.filter(|p| {
|
||||
matches!(
|
||||
p.extension().and_then(|e| e.to_str()),
|
||||
Some("vert") | Some("frag")
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
// Порядок обхода каталога зависит от файловой системы — сортируем, чтобы сборка
|
||||
// была воспроизводимой.
|
||||
sources.sort();
|
||||
|
||||
assert!(
|
||||
!sources.is_empty(),
|
||||
"в {} нет ни одного .vert/.frag",
|
||||
editor_dir.display()
|
||||
);
|
||||
|
||||
let out_editor = out_dir.join("editor");
|
||||
std::fs::create_dir_all(&out_editor).unwrap();
|
||||
|
||||
for src in &sources {
|
||||
println!("cargo:rerun-if-changed={}", src.display());
|
||||
let name = src.file_name().unwrap().to_string_lossy().to_string();
|
||||
let dst = out_editor.join(format!("{name}.spv"));
|
||||
|
||||
let output = Command::new(&compiler)
|
||||
.arg("-V")
|
||||
.args(["--target-env", "vulkan1.3"])
|
||||
// glslangValidator требует -I<путь> без пробела.
|
||||
.arg(format!("-I{}", shader_root.display()))
|
||||
.arg("-o")
|
||||
.arg(&dst)
|
||||
.arg(src)
|
||||
.output()
|
||||
.unwrap_or_else(|e| panic!("не запускается {}: {e}", compiler.display()));
|
||||
|
||||
if !output.status.success() {
|
||||
panic!(
|
||||
"GLSL → SPIR-V не удалось для {}:\n{}{}",
|
||||
src.display(),
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ищем компилятор там же, где его ищет CMake: сначала в Vulkan SDK, потом в PATH.
|
||||
fn find_glslang() -> PathBuf {
|
||||
let exe = if cfg!(windows) {
|
||||
"glslangValidator.exe"
|
||||
} else {
|
||||
"glslangValidator"
|
||||
};
|
||||
if let Ok(sdk) = std::env::var("VULKAN_SDK") {
|
||||
let candidate = Path::new(&sdk).join("Bin").join(exe);
|
||||
if candidate.exists() {
|
||||
return candidate;
|
||||
}
|
||||
let candidate = Path::new(&sdk).join("bin").join(exe);
|
||||
if candidate.exists() {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
PathBuf::from(exe)
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
//! Буфер с памятью из аллокатора — перенос `simv::vk::Buffer`.
|
||||
//!
|
||||
//! Расхождение с C++-версией. Там `vk::Buffer` есть, но им не пользуется никто:
|
||||
//! `GridRenderer`, `GpuMesh` и глубинное вложение `Renderer` создают ресурсы напрямую
|
||||
//! вызовами VMA, а сама обёртка так и лежит в цели `simv_vk` мёртвым грузом. Здесь она
|
||||
//! задействована всеми тремя, потому что порт писался, когда потребители уже известны,
|
||||
//! а с gpu-allocator обёртка — самый короткий путь: она же и держит `Allocation`,
|
||||
//! который иначе пришлось бы освобождать руками в каждом `Drop`.
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use ash::vk;
|
||||
use gpu_allocator::vulkan::{Allocation, AllocationCreateDesc, AllocationScheme, Allocator};
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::error::{Ctx, Result, VkError};
|
||||
|
||||
/// Где живёт память буфера.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum MemoryLocation {
|
||||
/// Только для видеокарты: быстрая, недоступна процессору.
|
||||
DeviceLocal,
|
||||
/// Видима процессору, пишется последовательно (загрузка на видеокарту).
|
||||
HostVisible,
|
||||
/// Видима процессору, читается в произвольном порядке (чтение с видеокарты).
|
||||
HostReadback,
|
||||
}
|
||||
|
||||
impl From<MemoryLocation> for gpu_allocator::MemoryLocation {
|
||||
fn from(value: MemoryLocation) -> Self {
|
||||
match value {
|
||||
MemoryLocation::DeviceLocal => gpu_allocator::MemoryLocation::GpuOnly,
|
||||
MemoryLocation::HostVisible => gpu_allocator::MemoryLocation::CpuToGpu,
|
||||
MemoryLocation::HostReadback => gpu_allocator::MemoryLocation::GpuToCpu,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Buffer {
|
||||
device: ash::Device,
|
||||
allocator: Arc<Mutex<Allocator>>,
|
||||
handle: vk::Buffer,
|
||||
/// `Option` ради `Drop`: память возвращается аллокатору по значению.
|
||||
allocation: Option<Allocation>,
|
||||
size: vk::DeviceSize,
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
pub fn new(
|
||||
ctx: &Context,
|
||||
name: &str,
|
||||
size: vk::DeviceSize,
|
||||
usage: vk::BufferUsageFlags,
|
||||
location: MemoryLocation,
|
||||
) -> Result<Self> {
|
||||
let device = ctx.device().clone();
|
||||
let ci = vk::BufferCreateInfo::default()
|
||||
.size(size)
|
||||
.usage(usage)
|
||||
.sharing_mode(vk::SharingMode::EXCLUSIVE);
|
||||
let handle = unsafe { device.create_buffer(&ci, None) }.ctx("vkCreateBuffer")?;
|
||||
let requirements = unsafe { device.get_buffer_memory_requirements(handle) };
|
||||
|
||||
let allocator = ctx.allocator().clone();
|
||||
let allocation = {
|
||||
let mut guard = allocator.lock().expect("аллокатор отравлен паникой");
|
||||
guard.allocate(&AllocationCreateDesc {
|
||||
name,
|
||||
requirements,
|
||||
location: location.into(),
|
||||
linear: true,
|
||||
allocation_scheme: AllocationScheme::GpuAllocatorManaged,
|
||||
})
|
||||
};
|
||||
let allocation = match allocation {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
unsafe { device.destroy_buffer(handle, None) };
|
||||
return Err(VkError::Other(format!("gpu-allocator: {e}")));
|
||||
}
|
||||
};
|
||||
|
||||
unsafe { device.bind_buffer_memory(handle, allocation.memory(), allocation.offset()) }
|
||||
.ctx("vkBindBufferMemory")?;
|
||||
|
||||
Ok(Self {
|
||||
device,
|
||||
allocator,
|
||||
handle,
|
||||
allocation: Some(allocation),
|
||||
size,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn handle(&self) -> vk::Buffer {
|
||||
self.handle
|
||||
}
|
||||
pub fn size(&self) -> vk::DeviceSize {
|
||||
self.size
|
||||
}
|
||||
|
||||
/// Постоянно отображённая память, если она вообще видима процессору.
|
||||
///
|
||||
/// В отличие от C++-версии здесь нет пары `Map`/`Unmap`: gpu-allocator держит
|
||||
/// отображение всё время жизни выделения, а срез с временем жизни буфера не даёт
|
||||
/// уронить указатель раньше самого буфера.
|
||||
pub fn mapped(&mut self) -> Option<&mut [u8]> {
|
||||
self.allocation.as_mut()?.mapped_slice_mut()
|
||||
}
|
||||
|
||||
/// Записать байты по смещению. Возвращает ошибку, если память недоступна
|
||||
/// процессору или запись не помещается.
|
||||
pub fn write(&mut self, src: &[u8], offset: usize) -> Result<()> {
|
||||
let size = self.size as usize;
|
||||
let Some(dst) = self.mapped() else {
|
||||
return Err(VkError::Other(String::from(
|
||||
"запись в буфер, недоступный процессору",
|
||||
)));
|
||||
};
|
||||
if offset + src.len() > size {
|
||||
return Err(VkError::Other(format!(
|
||||
"запись {} байт по смещению {offset} не помещается в буфер {size} байт",
|
||||
src.len()
|
||||
)));
|
||||
}
|
||||
dst[offset..offset + src.len()].copy_from_slice(src);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Buffer {
|
||||
fn drop(&mut self) {
|
||||
if let Some(allocation) = self.allocation.take() {
|
||||
let _ = self
|
||||
.allocator
|
||||
.lock()
|
||||
.map(|mut guard| guard.free(allocation));
|
||||
}
|
||||
unsafe { self.device.destroy_buffer(self.handle, None) };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,570 @@
|
||||
//! Инстанс, устройство, очереди, аллокатор и кэш конвейеров — перенос
|
||||
//! `simv::vk::Context`.
|
||||
//!
|
||||
//! Самый самостоятельный кусок порта. В C++ всё это строит vk-bootstrap: выбор
|
||||
//! физического устройства по требуемым возможностям, поиск семейств очередей,
|
||||
//! проверка наличия слоя валидации. Замены vk-bootstrap в экосистеме Rust нет, так что
|
||||
//! ниже примерно двести пятьдесят строк, которых в C++-версии просто не существует.
|
||||
|
||||
use std::ffi::{c_char, c_void, CStr};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use ash::vk;
|
||||
use gpu_allocator::vulkan::{Allocator, AllocatorCreateDesc};
|
||||
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
|
||||
|
||||
use crate::error::{Ctx, Result, VkError};
|
||||
|
||||
/// Пожелания к контексту. Значения по умолчанию совпадают с C++-версией, включая
|
||||
/// включённую валидацию во всех конфигурациях сборки.
|
||||
pub struct ContextOptions {
|
||||
pub enable_validation: bool,
|
||||
pub enable_debug_utils: bool,
|
||||
pub require_async_compute: bool,
|
||||
pub require_async_transfer: bool,
|
||||
}
|
||||
|
||||
impl Default for ContextOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enable_validation: true,
|
||||
enable_debug_utils: true,
|
||||
require_async_compute: true,
|
||||
require_async_transfer: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const VALIDATION_LAYER: &CStr = c"VK_LAYER_KHRONOS_validation";
|
||||
|
||||
/// Vulkan 1.3: инстанс, поверхность, физическое и логическое устройство, очереди,
|
||||
/// аллокатор памяти и кэш конвейеров.
|
||||
///
|
||||
/// Владеет всем перечисленным и разрушает в обратном порядке в `Drop`. Всё, что
|
||||
/// клонирует `ash::Device` (а это делают `GpuMesh`, `MeshRenderer`, `GridRenderer`),
|
||||
/// обязано быть уничтожено раньше контекста: клон `Device` — это копия таблицы
|
||||
/// функций, а не владеющая ссылка, и пережить `destroy_device` он не может.
|
||||
pub struct Context {
|
||||
device_name: String,
|
||||
|
||||
graphics_queue: vk::Queue,
|
||||
compute_queue: vk::Queue,
|
||||
transfer_queue: vk::Queue,
|
||||
graphics_family: u32,
|
||||
compute_family: u32,
|
||||
transfer_family: u32,
|
||||
|
||||
pipeline_cache: vk::PipelineCache,
|
||||
/// `Option` только ради `Drop`: аллокатор обязан умереть до устройства.
|
||||
allocator: Option<Arc<Mutex<Allocator>>>,
|
||||
|
||||
swapchain_loader: ash::khr::swapchain::Device,
|
||||
device: ash::Device,
|
||||
physical_device: vk::PhysicalDevice,
|
||||
|
||||
surface: vk::SurfaceKHR,
|
||||
surface_loader: ash::khr::surface::Instance,
|
||||
debug: Option<(ash::ext::debug_utils::Instance, vk::DebugUtilsMessengerEXT)>,
|
||||
instance: ash::Instance,
|
||||
/// `Option` ради `Drop`: при динамической загрузке `Entry` держит саму
|
||||
/// библиотеку Vulkan, поэтому уйти она обязана последней — иначе
|
||||
/// `destroy_instance` вызовет указатель в уже выгруженной DLL.
|
||||
entry: Option<ash::Entry>,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn new(window: &simv_core::Window, opts: &ContextOptions) -> Result<Self> {
|
||||
let entry = unsafe { ash::Entry::load() }?;
|
||||
|
||||
let display_handle = window.raw().display_handle()?.as_raw();
|
||||
let window_handle = window.raw().window_handle()?.as_raw();
|
||||
|
||||
// ── Инстанс ──────────────────────────────────────────────────────────
|
||||
let mut extensions: Vec<*const c_char> =
|
||||
ash_window::enumerate_required_extensions(display_handle)
|
||||
.ctx("ash_window::enumerate_required_extensions")?
|
||||
.to_vec();
|
||||
if opts.enable_debug_utils {
|
||||
extensions.push(ash::ext::debug_utils::NAME.as_ptr());
|
||||
}
|
||||
|
||||
// Проверка наличия слоя обязательна: vk-bootstrap её делал, а без неё машина
|
||||
// без установленного SDK не запустила бы приложение вовсе.
|
||||
let mut layers: Vec<*const c_char> = Vec::new();
|
||||
if opts.enable_validation {
|
||||
let available = unsafe { entry.enumerate_instance_layer_properties() }
|
||||
.ctx("vkEnumerateInstanceLayerProperties")?;
|
||||
let has_validation = available
|
||||
.iter()
|
||||
.any(|l| l.layer_name_as_c_str() == Ok(VALIDATION_LAYER));
|
||||
if has_validation {
|
||||
layers.push(VALIDATION_LAYER.as_ptr());
|
||||
} else {
|
||||
log::warn!(target: "vk", "слой валидации не установлен — иду без него");
|
||||
}
|
||||
}
|
||||
|
||||
let app_info = vk::ApplicationInfo::default()
|
||||
.application_name(c"SimVulcan")
|
||||
.engine_name(c"simv")
|
||||
.api_version(vk::API_VERSION_1_3);
|
||||
let instance_ci = vk::InstanceCreateInfo::default()
|
||||
.application_info(&app_info)
|
||||
.enabled_extension_names(&extensions)
|
||||
.enabled_layer_names(&layers);
|
||||
let instance =
|
||||
unsafe { entry.create_instance(&instance_ci, None) }.ctx("vkCreateInstance")?;
|
||||
|
||||
// Любой ранний выход дальше обязан прибрать уже созданное: `Drop` собирается
|
||||
// только на полностью построенном контексте. Отсюда явный разбор ошибки.
|
||||
match Self::build(&entry, &instance, display_handle, window_handle, opts) {
|
||||
Ok(mut ctx) => {
|
||||
ctx.entry = Some(entry);
|
||||
Ok(ctx)
|
||||
}
|
||||
Err(e) => {
|
||||
unsafe { instance.destroy_instance(None) };
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Всё, что строится поверх готового инстанса.
|
||||
fn build(
|
||||
entry: &ash::Entry,
|
||||
instance: &ash::Instance,
|
||||
display_handle: raw_window_handle::RawDisplayHandle,
|
||||
window_handle: raw_window_handle::RawWindowHandle,
|
||||
opts: &ContextOptions,
|
||||
) -> Result<Self> {
|
||||
// ── Сообщения валидации ──────────────────────────────────────────────
|
||||
let debug = if opts.enable_debug_utils {
|
||||
let loader = ash::ext::debug_utils::Instance::new(entry, instance);
|
||||
let ci = vk::DebugUtilsMessengerCreateInfoEXT::default()
|
||||
.message_severity(
|
||||
vk::DebugUtilsMessageSeverityFlagsEXT::ERROR
|
||||
| vk::DebugUtilsMessageSeverityFlagsEXT::WARNING,
|
||||
)
|
||||
.message_type(
|
||||
vk::DebugUtilsMessageTypeFlagsEXT::GENERAL
|
||||
| vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION
|
||||
| vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE,
|
||||
)
|
||||
.pfn_user_callback(Some(debug_callback));
|
||||
let messenger = unsafe { loader.create_debug_utils_messenger(&ci, None) }
|
||||
.ctx("vkCreateDebugUtilsMessengerEXT")?;
|
||||
Some((loader, messenger))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// ── Поверхность ──────────────────────────────────────────────────────
|
||||
let surface_loader = ash::khr::surface::Instance::new(entry, instance);
|
||||
let surface = unsafe {
|
||||
ash_window::create_surface(entry, instance, display_handle, window_handle, None)
|
||||
}
|
||||
.ctx("vkCreate*SurfaceKHR")?;
|
||||
|
||||
// ── Физическое устройство ────────────────────────────────────────────
|
||||
let chosen = select_physical_device(instance, &surface_loader, surface)?;
|
||||
let device_name = chosen.name.clone();
|
||||
let physical_device = chosen.handle;
|
||||
|
||||
// ── Логическое устройство ────────────────────────────────────────────
|
||||
let mut families = vec![chosen.graphics_family];
|
||||
for f in [chosen.compute_family, chosen.transfer_family]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
if !families.contains(&f) {
|
||||
families.push(f);
|
||||
}
|
||||
}
|
||||
let priority = [1.0f32];
|
||||
let queue_cis: Vec<_> = families
|
||||
.iter()
|
||||
.map(|&family| {
|
||||
vk::DeviceQueueCreateInfo::default()
|
||||
.queue_family_index(family)
|
||||
.queue_priorities(&priority)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let device_extensions = [ash::khr::swapchain::NAME.as_ptr()];
|
||||
// Ровно тот набор, который включает C++-версия (Context.cpp: feat10/12/13).
|
||||
let mut features13 = vk::PhysicalDeviceVulkan13Features::default()
|
||||
.synchronization2(true)
|
||||
.dynamic_rendering(true);
|
||||
let mut features12 = vk::PhysicalDeviceVulkan12Features::default()
|
||||
.timeline_semaphore(true)
|
||||
.descriptor_indexing(true)
|
||||
.buffer_device_address(true)
|
||||
.shader_subgroup_extended_types(true)
|
||||
.scalar_block_layout(true)
|
||||
.runtime_descriptor_array(true);
|
||||
// fillModeNonSolid — без него не собрать каркасный конвейер.
|
||||
let features10 = vk::PhysicalDeviceFeatures::default().fill_mode_non_solid(true);
|
||||
let mut features2 = vk::PhysicalDeviceFeatures2::default()
|
||||
.features(features10)
|
||||
.push_next(&mut features13)
|
||||
.push_next(&mut features12);
|
||||
|
||||
let device_ci = vk::DeviceCreateInfo::default()
|
||||
.queue_create_infos(&queue_cis)
|
||||
.enabled_extension_names(&device_extensions)
|
||||
.push_next(&mut features2);
|
||||
|
||||
let device = unsafe { instance.create_device(physical_device, &device_ci, None) }
|
||||
.ctx("vkCreateDevice")?;
|
||||
|
||||
// ── Очереди ──────────────────────────────────────────────────────────
|
||||
let graphics_family = chosen.graphics_family;
|
||||
let graphics_queue = unsafe { device.get_device_queue(graphics_family, 0) };
|
||||
|
||||
let (compute_family, compute_queue) = match chosen.compute_family {
|
||||
Some(f) if opts.require_async_compute => (f, unsafe { device.get_device_queue(f, 0) }),
|
||||
_ => {
|
||||
log::warn!(target: "vk", "выделенной вычислительной очереди нет — беру графическую");
|
||||
(graphics_family, graphics_queue)
|
||||
}
|
||||
};
|
||||
let (transfer_family, transfer_queue) = match chosen.transfer_family {
|
||||
Some(f) if opts.require_async_transfer => (f, unsafe { device.get_device_queue(f, 0) }),
|
||||
_ => {
|
||||
log::warn!(target: "vk", "выделенной очереди передачи нет — беру графическую");
|
||||
(graphics_family, graphics_queue)
|
||||
}
|
||||
};
|
||||
|
||||
// ── Аллокатор ────────────────────────────────────────────────────────
|
||||
let allocator = Allocator::new(&AllocatorCreateDesc {
|
||||
instance: instance.clone(),
|
||||
device: device.clone(),
|
||||
physical_device,
|
||||
debug_settings: Default::default(),
|
||||
buffer_device_address: true,
|
||||
allocation_sizes: Default::default(),
|
||||
})
|
||||
.map_err(|e| VkError::Other(format!("gpu-allocator: {e}")))?;
|
||||
|
||||
let swapchain_loader = ash::khr::swapchain::Device::new(instance, &device);
|
||||
let pipeline_cache = create_pipeline_cache(&device);
|
||||
|
||||
log::info!(target: "vk", "контекст Vulkan готов: {device_name}");
|
||||
|
||||
Ok(Self {
|
||||
device_name,
|
||||
graphics_queue,
|
||||
compute_queue,
|
||||
transfer_queue,
|
||||
graphics_family,
|
||||
compute_family,
|
||||
transfer_family,
|
||||
pipeline_cache,
|
||||
allocator: Some(Arc::new(Mutex::new(allocator))),
|
||||
swapchain_loader,
|
||||
device,
|
||||
physical_device,
|
||||
surface,
|
||||
surface_loader,
|
||||
debug,
|
||||
instance: instance.clone(),
|
||||
// Заполняет вызывающий: сюда `Entry` не передаётся, чтобы на пути ошибки
|
||||
// инстанс уничтожался ровно один раз, а библиотека выгружалась после него.
|
||||
entry: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn device(&self) -> &ash::Device {
|
||||
&self.device
|
||||
}
|
||||
pub fn instance(&self) -> &ash::Instance {
|
||||
&self.instance
|
||||
}
|
||||
pub fn physical_device(&self) -> vk::PhysicalDevice {
|
||||
self.physical_device
|
||||
}
|
||||
pub fn surface(&self) -> vk::SurfaceKHR {
|
||||
self.surface
|
||||
}
|
||||
pub fn surface_loader(&self) -> &ash::khr::surface::Instance {
|
||||
&self.surface_loader
|
||||
}
|
||||
pub fn swapchain_loader(&self) -> &ash::khr::swapchain::Device {
|
||||
&self.swapchain_loader
|
||||
}
|
||||
pub fn allocator(&self) -> &Arc<Mutex<Allocator>> {
|
||||
self.allocator.as_ref().expect("аллокатор уже уничтожен")
|
||||
}
|
||||
pub fn graphics_queue(&self) -> vk::Queue {
|
||||
self.graphics_queue
|
||||
}
|
||||
pub fn compute_queue(&self) -> vk::Queue {
|
||||
self.compute_queue
|
||||
}
|
||||
pub fn transfer_queue(&self) -> vk::Queue {
|
||||
self.transfer_queue
|
||||
}
|
||||
pub fn graphics_family(&self) -> u32 {
|
||||
self.graphics_family
|
||||
}
|
||||
pub fn compute_family(&self) -> u32 {
|
||||
self.compute_family
|
||||
}
|
||||
pub fn transfer_family(&self) -> u32 {
|
||||
self.transfer_family
|
||||
}
|
||||
pub fn pipeline_cache(&self) -> vk::PipelineCache {
|
||||
self.pipeline_cache
|
||||
}
|
||||
pub fn device_name(&self) -> &str {
|
||||
&self.device_name
|
||||
}
|
||||
|
||||
pub fn wait_idle(&self) {
|
||||
let _ = unsafe { self.device.device_wait_idle() };
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Context {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
let _ = self.device.device_wait_idle();
|
||||
|
||||
save_pipeline_cache(&self.device, self.pipeline_cache);
|
||||
if self.pipeline_cache != vk::PipelineCache::null() {
|
||||
self.device.destroy_pipeline_cache(self.pipeline_cache, None);
|
||||
}
|
||||
// Аллокатор обязан уйти до устройства, иначе он освободит память уже
|
||||
// уничтоженного `VkDevice`.
|
||||
drop(self.allocator.take());
|
||||
|
||||
self.device.destroy_device(None);
|
||||
self.surface_loader.destroy_surface(self.surface, None);
|
||||
if let Some((loader, messenger)) = self.debug.take() {
|
||||
loader.destroy_debug_utils_messenger(messenger, None);
|
||||
}
|
||||
self.instance.destroy_instance(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Выбор физического устройства — то, что в C++ делает vkb::PhysicalDeviceSelector
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
struct ChosenDevice {
|
||||
handle: vk::PhysicalDevice,
|
||||
name: String,
|
||||
graphics_family: u32,
|
||||
/// Выделенное семейство: COMPUTE без GRAPHICS. `None` — такого нет.
|
||||
compute_family: Option<u32>,
|
||||
/// Выделенное семейство: TRANSFER без GRAPHICS и COMPUTE.
|
||||
transfer_family: Option<u32>,
|
||||
score: u32,
|
||||
}
|
||||
|
||||
fn select_physical_device(
|
||||
instance: &ash::Instance,
|
||||
surface_loader: &ash::khr::surface::Instance,
|
||||
surface: vk::SurfaceKHR,
|
||||
) -> Result<ChosenDevice> {
|
||||
let devices =
|
||||
unsafe { instance.enumerate_physical_devices() }.ctx("vkEnumeratePhysicalDevices")?;
|
||||
|
||||
let mut best: Option<ChosenDevice> = None;
|
||||
for handle in devices {
|
||||
let Some(candidate) = inspect_device(instance, surface_loader, surface, handle) else {
|
||||
continue;
|
||||
};
|
||||
if best.as_ref().is_none_or(|b| candidate.score > b.score) {
|
||||
best = Some(candidate);
|
||||
}
|
||||
}
|
||||
best.ok_or(VkError::NoSuitableDevice)
|
||||
}
|
||||
|
||||
fn inspect_device(
|
||||
instance: &ash::Instance,
|
||||
surface_loader: &ash::khr::surface::Instance,
|
||||
surface: vk::SurfaceKHR,
|
||||
handle: vk::PhysicalDevice,
|
||||
) -> Option<ChosenDevice> {
|
||||
let props = unsafe { instance.get_physical_device_properties(handle) };
|
||||
let name = props
|
||||
.device_name_as_c_str()
|
||||
.map(|s| s.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|_| String::from("устройство без имени"));
|
||||
|
||||
if props.api_version < vk::API_VERSION_1_3 {
|
||||
log::debug!(target: "vk", "{name}: пропускаю, API ниже 1.3");
|
||||
return None;
|
||||
}
|
||||
|
||||
// Возможности: ровно тот набор, который требует C++-версия.
|
||||
//
|
||||
// Запрос обёрнут в блок не для красоты. `push_next` в ash берёт `&mut` на
|
||||
// вложенную структуру и держит заимствование, пока жива голова цепочки, поэтому
|
||||
// читать `f13`/`f12` при живом `f2` компилятор не даёт. В C++ pNext — сырой
|
||||
// указатель, и там этот вопрос просто не возникает. Блок гасит заимствование:
|
||||
// наружу выходят уже заполненные значения.
|
||||
let (f13, f12, f10) = {
|
||||
let mut f13 = vk::PhysicalDeviceVulkan13Features::default();
|
||||
let mut f12 = vk::PhysicalDeviceVulkan12Features::default();
|
||||
let mut f2 = vk::PhysicalDeviceFeatures2::default()
|
||||
.push_next(&mut f13)
|
||||
.push_next(&mut f12);
|
||||
unsafe { instance.get_physical_device_features2(handle, &mut f2) };
|
||||
let f10 = f2.features;
|
||||
(f13, f12, f10)
|
||||
};
|
||||
|
||||
let missing = [
|
||||
("synchronization2", f13.synchronization2 == vk::TRUE),
|
||||
("dynamicRendering", f13.dynamic_rendering == vk::TRUE),
|
||||
("timelineSemaphore", f12.timeline_semaphore == vk::TRUE),
|
||||
("descriptorIndexing", f12.descriptor_indexing == vk::TRUE),
|
||||
("bufferDeviceAddress", f12.buffer_device_address == vk::TRUE),
|
||||
(
|
||||
"shaderSubgroupExtendedTypes",
|
||||
f12.shader_subgroup_extended_types == vk::TRUE,
|
||||
),
|
||||
("scalarBlockLayout", f12.scalar_block_layout == vk::TRUE),
|
||||
(
|
||||
"runtimeDescriptorArray",
|
||||
f12.runtime_descriptor_array == vk::TRUE,
|
||||
),
|
||||
("fillModeNonSolid", f10.fill_mode_non_solid == vk::TRUE),
|
||||
]
|
||||
.into_iter()
|
||||
.filter_map(|(feature, ok)| (!ok).then_some(feature))
|
||||
.collect::<Vec<_>>();
|
||||
if !missing.is_empty() {
|
||||
log::debug!(target: "vk", "{name}: пропускаю, нет возможностей: {}", missing.join(", "));
|
||||
return None;
|
||||
}
|
||||
|
||||
let extensions = unsafe { instance.enumerate_device_extension_properties(handle) }.ok()?;
|
||||
let has_swapchain = extensions
|
||||
.iter()
|
||||
.any(|e| e.extension_name_as_c_str() == Ok(ash::khr::swapchain::NAME));
|
||||
if !has_swapchain {
|
||||
log::debug!(target: "vk", "{name}: пропускаю, нет VK_KHR_swapchain");
|
||||
return None;
|
||||
}
|
||||
|
||||
// Семейства очередей. Графическое обязано ещё и уметь показывать на поверхность:
|
||||
// C++-версия полагалась на то, что vkb проверит это за неё.
|
||||
let families = unsafe { instance.get_physical_device_queue_family_properties(handle) };
|
||||
let mut graphics_family = None;
|
||||
let mut compute_family = None;
|
||||
let mut transfer_family = None;
|
||||
for (index, family) in families.iter().enumerate() {
|
||||
let i = index as u32;
|
||||
let flags = family.queue_flags;
|
||||
let graphics = flags.contains(vk::QueueFlags::GRAPHICS);
|
||||
let compute = flags.contains(vk::QueueFlags::COMPUTE);
|
||||
let transfer = flags.contains(vk::QueueFlags::TRANSFER);
|
||||
|
||||
if graphics_family.is_none() && graphics {
|
||||
let presents =
|
||||
unsafe { surface_loader.get_physical_device_surface_support(handle, i, surface) }
|
||||
.unwrap_or(false);
|
||||
if presents {
|
||||
graphics_family = Some(i);
|
||||
}
|
||||
}
|
||||
if compute_family.is_none() && compute && !graphics {
|
||||
compute_family = Some(i);
|
||||
}
|
||||
if transfer_family.is_none() && transfer && !graphics && !compute {
|
||||
transfer_family = Some(i);
|
||||
}
|
||||
}
|
||||
let graphics_family = graphics_family?;
|
||||
|
||||
let score = match props.device_type {
|
||||
vk::PhysicalDeviceType::DISCRETE_GPU => 1000,
|
||||
vk::PhysicalDeviceType::INTEGRATED_GPU => 100,
|
||||
vk::PhysicalDeviceType::VIRTUAL_GPU => 10,
|
||||
_ => 1,
|
||||
};
|
||||
|
||||
Some(ChosenDevice {
|
||||
handle,
|
||||
name,
|
||||
graphics_family,
|
||||
compute_family,
|
||||
transfer_family,
|
||||
score,
|
||||
})
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Кэш конвейеров
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Файл кэша лежит рядом с исполняемым файлом, а не в текущем каталоге, как в
|
||||
/// C++-версии: запускать редактор из произвольного места теперь можно, и кэш при этом
|
||||
/// не рассыпается по диску.
|
||||
fn pipeline_cache_path() -> std::path::PathBuf {
|
||||
std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|p| p.parent().map(|d| d.join("pipeline_cache.bin")))
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("pipeline_cache.bin"))
|
||||
}
|
||||
|
||||
fn create_pipeline_cache(device: &ash::Device) -> vk::PipelineCache {
|
||||
let blob = std::fs::read(pipeline_cache_path()).unwrap_or_default();
|
||||
let ci = vk::PipelineCacheCreateInfo::default().initial_data(&blob);
|
||||
match unsafe { device.create_pipeline_cache(&ci, None) } {
|
||||
Ok(cache) => {
|
||||
log::info!(target: "vk", "кэш конвейеров загружен: {} байт", blob.len());
|
||||
cache
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
target: "vk",
|
||||
"кэш конвейеров не создан ({e:?}) — конвейеры будут пересобираться каждый запуск"
|
||||
);
|
||||
vk::PipelineCache::null()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn save_pipeline_cache(device: &ash::Device, cache: vk::PipelineCache) {
|
||||
if cache == vk::PipelineCache::null() {
|
||||
return;
|
||||
}
|
||||
if let Ok(blob) = unsafe { device.get_pipeline_cache_data(cache) } {
|
||||
if !blob.is_empty() {
|
||||
let _ = std::fs::write(pipeline_cache_path(), &blob);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Сообщения слоя валидации
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
unsafe extern "system" fn debug_callback(
|
||||
severity: vk::DebugUtilsMessageSeverityFlagsEXT,
|
||||
_types: vk::DebugUtilsMessageTypeFlagsEXT,
|
||||
data: *const vk::DebugUtilsMessengerCallbackDataEXT<'_>,
|
||||
_user: *mut c_void,
|
||||
) -> vk::Bool32 {
|
||||
let message = unsafe {
|
||||
data.as_ref()
|
||||
.filter(|d| !d.p_message.is_null())
|
||||
.map(|d| CStr::from_ptr(d.p_message).to_string_lossy())
|
||||
.unwrap_or(std::borrow::Cow::Borrowed("сообщение без текста"))
|
||||
};
|
||||
if severity.contains(vk::DebugUtilsMessageSeverityFlagsEXT::ERROR) {
|
||||
log::error!(target: "vk", "{message}");
|
||||
} else if severity.contains(vk::DebugUtilsMessageSeverityFlagsEXT::WARNING) {
|
||||
log::warn!(target: "vk", "{message}");
|
||||
} else {
|
||||
log::info!(target: "vk", "{message}");
|
||||
}
|
||||
vk::FALSE
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
//! Ошибки Vulkan-слоя.
|
||||
//!
|
||||
//! C++-версия бросала `std::runtime_error` со строкой, собранной на месте. Здесь
|
||||
//! ошибка — значение: её нельзя не заметить, а `?` избавляет от той же ручной
|
||||
//! проверки `if (r != VK_SUCCESS)`, которой в `Renderer.cpp` посвящена отдельная
|
||||
//! функция `CheckVk`.
|
||||
|
||||
use ash::vk;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum VkError {
|
||||
#[error("не удалось загрузить библиотеку Vulkan (нет драйвера или загрузчика): {0}")]
|
||||
Loading(#[from] ash::LoadingError),
|
||||
|
||||
#[error("{context} вернул ошибку: {source:?}")]
|
||||
Api {
|
||||
context: &'static str,
|
||||
#[source]
|
||||
source: vk::Result,
|
||||
},
|
||||
|
||||
#[error("не найдено устройство Vulkan 1.3 с нужным набором возможностей")]
|
||||
NoSuitableDevice,
|
||||
|
||||
#[error("не удалось получить дескриптор окна: {0}")]
|
||||
WindowHandle(#[from] raw_window_handle::HandleError),
|
||||
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, VkError>;
|
||||
|
||||
/// Навесить на результат ash имя вызова — то же, что делал `CheckVk(r, where)`.
|
||||
pub(crate) trait Ctx<T> {
|
||||
fn ctx(self, context: &'static str) -> Result<T>;
|
||||
}
|
||||
|
||||
impl<T> Ctx<T> for std::result::Result<T, vk::Result> {
|
||||
fn ctx(self, context: &'static str) -> Result<T> {
|
||||
self.map_err(|source| VkError::Api { context, source })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
//! Меш на видеокарте — перенос `simv::vk::GpuMesh`.
|
||||
//!
|
||||
//! Загрузка через промежуточный буфер на очереди передачи, с ожиданием забора.
|
||||
//! Загрузки редки (открытие модели), так что блокирующая копия здесь уместна — но
|
||||
//! копировать этот приём в покадровые пути нельзя.
|
||||
//!
|
||||
//! В C++ у `GpuMesh` есть состояние «пустой» и метод `IsValid()`, а у `Renderer` —
|
||||
//! отдельный флаг `hasMesh`. Здесь оба исчезли: незагруженный меш выражается через
|
||||
//! `Option<GpuMesh>`, и забыть проверить его невозможно — компилятор не даст.
|
||||
|
||||
use ash::vk;
|
||||
use simv_mesh::Mesh;
|
||||
|
||||
use crate::buffer::{Buffer, MemoryLocation};
|
||||
use crate::context::Context;
|
||||
use crate::error::{Ctx, Result, VkError};
|
||||
|
||||
pub struct GpuMesh {
|
||||
vertex: Buffer,
|
||||
index: Buffer,
|
||||
vertex_count: u32,
|
||||
index_count: u32,
|
||||
}
|
||||
|
||||
impl GpuMesh {
|
||||
pub fn upload(ctx: &Context, mesh: &Mesh) -> Result<Self> {
|
||||
if mesh.is_empty() {
|
||||
return Err(VkError::Other(String::from(
|
||||
"выгрузка пустого меша на видеокарту",
|
||||
)));
|
||||
}
|
||||
let vertex_count = mesh.positions.len() as u32;
|
||||
let index_count = mesh.indices.len() as u32;
|
||||
|
||||
let vertex_bytes: &[u8] = bytemuck::cast_slice(&mesh.positions);
|
||||
let index_bytes: &[u8] = bytemuck::cast_slice(&mesh.indices);
|
||||
|
||||
let vertex = Buffer::new(
|
||||
ctx,
|
||||
"mesh.vertices",
|
||||
vertex_bytes.len() as vk::DeviceSize,
|
||||
vk::BufferUsageFlags::TRANSFER_DST | vk::BufferUsageFlags::VERTEX_BUFFER,
|
||||
MemoryLocation::DeviceLocal,
|
||||
)?;
|
||||
let index = Buffer::new(
|
||||
ctx,
|
||||
"mesh.indices",
|
||||
index_bytes.len() as vk::DeviceSize,
|
||||
vk::BufferUsageFlags::TRANSFER_DST | vk::BufferUsageFlags::INDEX_BUFFER,
|
||||
MemoryLocation::DeviceLocal,
|
||||
)?;
|
||||
|
||||
let mut vertex_stage = Buffer::new(
|
||||
ctx,
|
||||
"mesh.vertices.staging",
|
||||
vertex_bytes.len() as vk::DeviceSize,
|
||||
vk::BufferUsageFlags::TRANSFER_SRC,
|
||||
MemoryLocation::HostVisible,
|
||||
)?;
|
||||
let mut index_stage = Buffer::new(
|
||||
ctx,
|
||||
"mesh.indices.staging",
|
||||
index_bytes.len() as vk::DeviceSize,
|
||||
vk::BufferUsageFlags::TRANSFER_SRC,
|
||||
MemoryLocation::HostVisible,
|
||||
)?;
|
||||
vertex_stage.write(vertex_bytes, 0)?;
|
||||
index_stage.write(index_bytes, 0)?;
|
||||
|
||||
one_shot_copy(
|
||||
ctx,
|
||||
&[
|
||||
(
|
||||
vertex_stage.handle(),
|
||||
vertex.handle(),
|
||||
vertex_bytes.len() as vk::DeviceSize,
|
||||
),
|
||||
(
|
||||
index_stage.handle(),
|
||||
index.handle(),
|
||||
index_bytes.len() as vk::DeviceSize,
|
||||
),
|
||||
],
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
vertex,
|
||||
index,
|
||||
vertex_count,
|
||||
index_count,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn vertex_buffer(&self) -> vk::Buffer {
|
||||
self.vertex.handle()
|
||||
}
|
||||
pub fn index_buffer(&self) -> vk::Buffer {
|
||||
self.index.handle()
|
||||
}
|
||||
pub fn vertex_count(&self) -> u32 {
|
||||
self.vertex_count
|
||||
}
|
||||
pub fn index_count(&self) -> u32 {
|
||||
self.index_count
|
||||
}
|
||||
}
|
||||
|
||||
/// Одноразовый командный буфер на очереди передачи: записать копии, отправить,
|
||||
/// дождаться забора, прибрать за собой.
|
||||
///
|
||||
/// C++-версия вызывала эту функцию по разу на буфер, то есть дважды отправляла работу
|
||||
/// и дважды ждала. Здесь копии собраны в одну отправку — при загрузке `plane.obj`
|
||||
/// это экономит одно полное ожидание забора.
|
||||
fn one_shot_copy(ctx: &Context, copies: &[(vk::Buffer, vk::Buffer, vk::DeviceSize)]) -> Result<()> {
|
||||
let device = ctx.device();
|
||||
|
||||
let pool_ci = vk::CommandPoolCreateInfo::default()
|
||||
.flags(vk::CommandPoolCreateFlags::TRANSIENT)
|
||||
.queue_family_index(ctx.transfer_family());
|
||||
let pool = unsafe { device.create_command_pool(&pool_ci, None) }
|
||||
.ctx("vkCreateCommandPool(one-shot)")?;
|
||||
|
||||
// Дальше любой выход обязан прибрать пул, поэтому тело вынесено в замыкание.
|
||||
let result = (|| -> Result<()> {
|
||||
let alloc_ci = vk::CommandBufferAllocateInfo::default()
|
||||
.command_pool(pool)
|
||||
.level(vk::CommandBufferLevel::PRIMARY)
|
||||
.command_buffer_count(1);
|
||||
let cmd = unsafe { device.allocate_command_buffers(&alloc_ci) }
|
||||
.ctx("vkAllocateCommandBuffers(one-shot)")?[0];
|
||||
|
||||
let begin = vk::CommandBufferBeginInfo::default()
|
||||
.flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT);
|
||||
unsafe { device.begin_command_buffer(cmd, &begin) }.ctx("vkBeginCommandBuffer")?;
|
||||
for &(src, dst, size) in copies {
|
||||
let region = [vk::BufferCopy::default().size(size)];
|
||||
unsafe { device.cmd_copy_buffer(cmd, src, dst, ®ion) };
|
||||
}
|
||||
unsafe { device.end_command_buffer(cmd) }.ctx("vkEndCommandBuffer")?;
|
||||
|
||||
let fence = unsafe { device.create_fence(&vk::FenceCreateInfo::default(), None) }
|
||||
.ctx("vkCreateFence(one-shot)")?;
|
||||
let cmds = [cmd];
|
||||
let submit = [vk::SubmitInfo::default().command_buffers(&cmds)];
|
||||
let submitted = unsafe { device.queue_submit(ctx.transfer_queue(), &submit, fence) };
|
||||
let waited = submitted
|
||||
.ctx("vkQueueSubmit(one-shot)")
|
||||
.and_then(|()| {
|
||||
unsafe { device.wait_for_fences(&[fence], true, u64::MAX) }
|
||||
.ctx("vkWaitForFences(one-shot)")
|
||||
});
|
||||
unsafe { device.destroy_fence(fence, None) };
|
||||
waited
|
||||
})();
|
||||
|
||||
unsafe { device.destroy_command_pool(pool, None) };
|
||||
result
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
//! Опорная сетка и оси — перенос `simv::vk::GridRenderer`.
|
||||
//!
|
||||
//! Геометрия строится один раз в буфер, видимый процессору: она статична и крошечная,
|
||||
//! так что промежуточная копия ради неё не нужна. Дескрипторных наборов нет — всё
|
||||
//! состояние умещается в одну push-константу.
|
||||
|
||||
use ash::vk;
|
||||
use glam::{Mat4, Vec3};
|
||||
|
||||
use crate::buffer::{Buffer, MemoryLocation};
|
||||
use crate::context::Context;
|
||||
use crate::error::{Ctx, Result};
|
||||
use crate::shader::{spirv, ShaderModule};
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
struct GridVertex {
|
||||
pos: Vec3,
|
||||
color: Vec3,
|
||||
}
|
||||
|
||||
/// Половина протяжённости сетки в клетках и размер клетки — как в C++-версии.
|
||||
const HALF_EXTENT_CELLS: i32 = 10;
|
||||
const CELL_SIZE: f32 = 1.0;
|
||||
|
||||
pub struct GridRenderer {
|
||||
device: ash::Device,
|
||||
layout: vk::PipelineLayout,
|
||||
pipeline: vk::Pipeline,
|
||||
vertex_buffer: Buffer,
|
||||
vertex_count: u32,
|
||||
color_format: vk::Format,
|
||||
depth_format: vk::Format,
|
||||
}
|
||||
|
||||
impl GridRenderer {
|
||||
pub fn new(ctx: &Context) -> Result<Self> {
|
||||
let device = ctx.device().clone();
|
||||
|
||||
let ranges = [vk::PushConstantRange::default()
|
||||
.stage_flags(vk::ShaderStageFlags::VERTEX)
|
||||
.offset(0)
|
||||
.size(std::mem::size_of::<Mat4>() as u32)];
|
||||
let layout_ci = vk::PipelineLayoutCreateInfo::default().push_constant_ranges(&ranges);
|
||||
let layout = unsafe { device.create_pipeline_layout(&layout_ci, None) }
|
||||
.ctx("vkCreatePipelineLayout(grid)")?;
|
||||
|
||||
let vertices = build_geometry();
|
||||
let bytes: &[u8] = bytemuck::cast_slice(&vertices);
|
||||
let mut vertex_buffer = Buffer::new(
|
||||
ctx,
|
||||
"grid.vertices",
|
||||
bytes.len() as vk::DeviceSize,
|
||||
vk::BufferUsageFlags::VERTEX_BUFFER,
|
||||
MemoryLocation::HostVisible,
|
||||
)?;
|
||||
vertex_buffer.write(bytes, 0)?;
|
||||
|
||||
Ok(Self {
|
||||
device,
|
||||
layout,
|
||||
pipeline: vk::Pipeline::null(),
|
||||
vertex_buffer,
|
||||
vertex_count: vertices.len() as u32,
|
||||
color_format: vk::Format::UNDEFINED,
|
||||
depth_format: vk::Format::UNDEFINED,
|
||||
})
|
||||
}
|
||||
|
||||
/// Пересобрать конвейер, если форматы вложений сменились (пересоздание swapchain).
|
||||
pub fn ensure_pipeline(
|
||||
&mut self,
|
||||
ctx: &Context,
|
||||
color_format: vk::Format,
|
||||
depth_format: vk::Format,
|
||||
) -> Result<()> {
|
||||
if self.pipeline != vk::Pipeline::null()
|
||||
&& self.color_format == color_format
|
||||
&& self.depth_format == depth_format
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
self.destroy_pipeline();
|
||||
self.color_format = color_format;
|
||||
self.depth_format = depth_format;
|
||||
self.pipeline = self.build_pipeline(ctx)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_pipeline(&self, ctx: &Context) -> Result<vk::Pipeline> {
|
||||
// Модули живут до конца функции и уничтожаются на выходе, включая пути ошибок.
|
||||
let vert = ShaderModule::new(&self.device, spirv::GRID_VERT)?;
|
||||
let frag = ShaderModule::new(&self.device, spirv::GRID_FRAG)?;
|
||||
let stages = [
|
||||
vk::PipelineShaderStageCreateInfo::default()
|
||||
.stage(vk::ShaderStageFlags::VERTEX)
|
||||
.module(vert.handle())
|
||||
.name(c"main"),
|
||||
vk::PipelineShaderStageCreateInfo::default()
|
||||
.stage(vk::ShaderStageFlags::FRAGMENT)
|
||||
.module(frag.handle())
|
||||
.name(c"main"),
|
||||
];
|
||||
|
||||
let bindings = [vk::VertexInputBindingDescription::default()
|
||||
.binding(0)
|
||||
.stride(std::mem::size_of::<GridVertex>() as u32)
|
||||
.input_rate(vk::VertexInputRate::VERTEX)];
|
||||
let attributes = [
|
||||
vk::VertexInputAttributeDescription::default()
|
||||
.location(0)
|
||||
.binding(0)
|
||||
.format(vk::Format::R32G32B32_SFLOAT)
|
||||
.offset(std::mem::offset_of!(GridVertex, pos) as u32),
|
||||
vk::VertexInputAttributeDescription::default()
|
||||
.location(1)
|
||||
.binding(0)
|
||||
.format(vk::Format::R32G32B32_SFLOAT)
|
||||
.offset(std::mem::offset_of!(GridVertex, color) as u32),
|
||||
];
|
||||
let vertex_input = vk::PipelineVertexInputStateCreateInfo::default()
|
||||
.vertex_binding_descriptions(&bindings)
|
||||
.vertex_attribute_descriptions(&attributes);
|
||||
|
||||
let input_assembly = vk::PipelineInputAssemblyStateCreateInfo::default()
|
||||
.topology(vk::PrimitiveTopology::LINE_LIST);
|
||||
let viewport = vk::PipelineViewportStateCreateInfo::default()
|
||||
.viewport_count(1)
|
||||
.scissor_count(1);
|
||||
let rasterization = vk::PipelineRasterizationStateCreateInfo::default()
|
||||
// Для линейной топологии режим заливки не используется.
|
||||
.polygon_mode(vk::PolygonMode::FILL)
|
||||
.cull_mode(vk::CullModeFlags::NONE)
|
||||
.line_width(1.0);
|
||||
let multisample = vk::PipelineMultisampleStateCreateInfo::default()
|
||||
.rasterization_samples(vk::SampleCountFlags::TYPE_1);
|
||||
let depth_stencil = vk::PipelineDepthStencilStateCreateInfo::default()
|
||||
.depth_test_enable(true)
|
||||
.depth_write_enable(true)
|
||||
.depth_compare_op(vk::CompareOp::LESS_OR_EQUAL)
|
||||
.min_depth_bounds(0.0)
|
||||
.max_depth_bounds(1.0);
|
||||
let blend_attachments = [vk::PipelineColorBlendAttachmentState::default()
|
||||
.color_write_mask(vk::ColorComponentFlags::RGBA)];
|
||||
let color_blend =
|
||||
vk::PipelineColorBlendStateCreateInfo::default().attachments(&blend_attachments);
|
||||
let dynamic_states = [vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR];
|
||||
let dynamic = vk::PipelineDynamicStateCreateInfo::default().dynamic_states(&dynamic_states);
|
||||
|
||||
let color_formats = [self.color_format];
|
||||
let mut rendering = vk::PipelineRenderingCreateInfo::default()
|
||||
.color_attachment_formats(&color_formats)
|
||||
.depth_attachment_format(self.depth_format);
|
||||
|
||||
let create_info = [vk::GraphicsPipelineCreateInfo::default()
|
||||
.stages(&stages)
|
||||
.vertex_input_state(&vertex_input)
|
||||
.input_assembly_state(&input_assembly)
|
||||
.viewport_state(&viewport)
|
||||
.rasterization_state(&rasterization)
|
||||
.multisample_state(&multisample)
|
||||
.depth_stencil_state(&depth_stencil)
|
||||
.color_blend_state(&color_blend)
|
||||
.dynamic_state(&dynamic)
|
||||
.layout(self.layout)
|
||||
.push_next(&mut rendering)];
|
||||
|
||||
let pipelines = unsafe {
|
||||
self.device
|
||||
.create_graphics_pipelines(ctx.pipeline_cache(), &create_info, None)
|
||||
}
|
||||
.map_err(|(_, e)| crate::error::VkError::Api {
|
||||
context: "vkCreateGraphicsPipelines(grid)",
|
||||
source: e,
|
||||
})?;
|
||||
Ok(pipelines[0])
|
||||
}
|
||||
|
||||
pub fn draw(&self, cmd: vk::CommandBuffer, view_proj: &Mat4) {
|
||||
if self.pipeline == vk::Pipeline::null() || self.vertex_count == 0 {
|
||||
return;
|
||||
}
|
||||
unsafe {
|
||||
self.device
|
||||
.cmd_bind_pipeline(cmd, vk::PipelineBindPoint::GRAPHICS, self.pipeline);
|
||||
self.device.cmd_push_constants(
|
||||
cmd,
|
||||
self.layout,
|
||||
vk::ShaderStageFlags::VERTEX,
|
||||
0,
|
||||
bytemuck::bytes_of(view_proj),
|
||||
);
|
||||
self.device
|
||||
.cmd_bind_vertex_buffers(cmd, 0, &[self.vertex_buffer.handle()], &[0]);
|
||||
self.device.cmd_draw(cmd, self.vertex_count, 1, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
fn destroy_pipeline(&mut self) {
|
||||
if self.pipeline != vk::Pipeline::null() {
|
||||
unsafe { self.device.destroy_pipeline(self.pipeline, None) };
|
||||
self.pipeline = vk::Pipeline::null();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for GridRenderer {
|
||||
fn drop(&mut self) {
|
||||
self.destroy_pipeline();
|
||||
unsafe { self.device.destroy_pipeline_layout(self.layout, None) };
|
||||
}
|
||||
}
|
||||
|
||||
/// Три плоскости сетки через начало координат плюс цветные оси.
|
||||
fn build_geometry() -> Vec<GridVertex> {
|
||||
const GRID: Vec3 = Vec3::new(0.30, 0.31, 0.35);
|
||||
let n = HALF_EXTENT_CELLS;
|
||||
let ext = n as f32 * CELL_SIZE;
|
||||
|
||||
let mut verts = Vec::with_capacity(((2 * n + 1) * 6 * 2 + 6) as usize);
|
||||
let mut line = |a: Vec3, b: Vec3, color: Vec3| {
|
||||
verts.push(GridVertex { pos: a, color });
|
||||
verts.push(GridVertex { pos: b, color });
|
||||
};
|
||||
|
||||
for i in -n..=n {
|
||||
let t = i as f32 * CELL_SIZE;
|
||||
// Плоскость XZ (y = 0)
|
||||
line(Vec3::new(-ext, 0.0, t), Vec3::new(ext, 0.0, t), GRID);
|
||||
line(Vec3::new(t, 0.0, -ext), Vec3::new(t, 0.0, ext), GRID);
|
||||
// Плоскость XY (z = 0)
|
||||
line(Vec3::new(-ext, t, 0.0), Vec3::new(ext, t, 0.0), GRID);
|
||||
line(Vec3::new(t, -ext, 0.0), Vec3::new(t, ext, 0.0), GRID);
|
||||
// Плоскость YZ (x = 0)
|
||||
line(Vec3::new(0.0, -ext, t), Vec3::new(0.0, ext, t), GRID);
|
||||
line(Vec3::new(0.0, t, -ext), Vec3::new(0.0, t, ext), GRID);
|
||||
}
|
||||
|
||||
// Оси идут последними, чтобы перекрыть серые центральные линии.
|
||||
line(
|
||||
Vec3::new(-ext, 0.0, 0.0),
|
||||
Vec3::new(ext, 0.0, 0.0),
|
||||
Vec3::new(0.85, 0.25, 0.25), // X красная
|
||||
);
|
||||
line(
|
||||
Vec3::new(0.0, -ext, 0.0),
|
||||
Vec3::new(0.0, ext, 0.0),
|
||||
Vec3::new(0.25, 0.80, 0.30), // Y зелёная
|
||||
);
|
||||
line(
|
||||
Vec3::new(0.0, 0.0, -ext),
|
||||
Vec3::new(0.0, 0.0, ext),
|
||||
Vec3::new(0.30, 0.45, 0.90), // Z синяя
|
||||
);
|
||||
|
||||
verts
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
//! Изображение с представлением — перенос `simv::vk::Image`.
|
||||
//!
|
||||
//! Как и `buffer.rs`, в C++-версии лежит без применения: глубинное вложение
|
||||
//! `Renderer` создаёт там свои `VkImage`/`VkImageView` напрямую. Здесь на нём как раз
|
||||
//! и построено глубинное вложение — см. заметку в `buffer.rs`.
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use ash::vk;
|
||||
use gpu_allocator::vulkan::{Allocation, AllocationCreateDesc, AllocationScheme, Allocator};
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::error::{Ctx, Result, VkError};
|
||||
|
||||
pub struct ImageDesc {
|
||||
pub extent: vk::Extent3D,
|
||||
pub format: vk::Format,
|
||||
pub usage: vk::ImageUsageFlags,
|
||||
pub image_type: vk::ImageType,
|
||||
pub view_type: vk::ImageViewType,
|
||||
pub aspect: vk::ImageAspectFlags,
|
||||
}
|
||||
|
||||
impl Default for ImageDesc {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
extent: vk::Extent3D {
|
||||
width: 1,
|
||||
height: 1,
|
||||
depth: 1,
|
||||
},
|
||||
format: vk::Format::R8G8B8A8_UNORM,
|
||||
usage: vk::ImageUsageFlags::SAMPLED,
|
||||
image_type: vk::ImageType::TYPE_2D,
|
||||
view_type: vk::ImageViewType::TYPE_2D,
|
||||
aspect: vk::ImageAspectFlags::COLOR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Image {
|
||||
device: ash::Device,
|
||||
allocator: Arc<Mutex<Allocator>>,
|
||||
image: vk::Image,
|
||||
view: vk::ImageView,
|
||||
allocation: Option<Allocation>,
|
||||
extent: vk::Extent3D,
|
||||
format: vk::Format,
|
||||
aspect: vk::ImageAspectFlags,
|
||||
/// Текущая раскладка. Отслеживается здесь, чтобы `transition_to` не выставлял
|
||||
/// барьер на переход в ту же раскладку — так же, как в C++-версии.
|
||||
layout: vk::ImageLayout,
|
||||
}
|
||||
|
||||
impl Image {
|
||||
pub fn new(ctx: &Context, name: &str, desc: &ImageDesc) -> Result<Self> {
|
||||
let device = ctx.device().clone();
|
||||
let ci = vk::ImageCreateInfo::default()
|
||||
.image_type(desc.image_type)
|
||||
.format(desc.format)
|
||||
.extent(desc.extent)
|
||||
.mip_levels(1)
|
||||
.array_layers(1)
|
||||
.samples(vk::SampleCountFlags::TYPE_1)
|
||||
.tiling(vk::ImageTiling::OPTIMAL)
|
||||
.usage(desc.usage)
|
||||
.sharing_mode(vk::SharingMode::EXCLUSIVE)
|
||||
.initial_layout(vk::ImageLayout::UNDEFINED);
|
||||
let image = unsafe { device.create_image(&ci, None) }.ctx("vkCreateImage")?;
|
||||
let requirements = unsafe { device.get_image_memory_requirements(image) };
|
||||
|
||||
let allocator = ctx.allocator().clone();
|
||||
let allocation = {
|
||||
let mut guard = allocator.lock().expect("аллокатор отравлен паникой");
|
||||
guard.allocate(&AllocationCreateDesc {
|
||||
name,
|
||||
requirements,
|
||||
location: gpu_allocator::MemoryLocation::GpuOnly,
|
||||
linear: false,
|
||||
allocation_scheme: AllocationScheme::GpuAllocatorManaged,
|
||||
})
|
||||
};
|
||||
let allocation = match allocation {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
unsafe { device.destroy_image(image, None) };
|
||||
return Err(VkError::Other(format!("gpu-allocator: {e}")));
|
||||
}
|
||||
};
|
||||
unsafe { device.bind_image_memory(image, allocation.memory(), allocation.offset()) }
|
||||
.ctx("vkBindImageMemory")?;
|
||||
|
||||
let view_ci = vk::ImageViewCreateInfo::default()
|
||||
.image(image)
|
||||
.view_type(desc.view_type)
|
||||
.format(desc.format)
|
||||
.subresource_range(vk::ImageSubresourceRange {
|
||||
aspect_mask: desc.aspect,
|
||||
base_mip_level: 0,
|
||||
level_count: 1,
|
||||
base_array_layer: 0,
|
||||
layer_count: 1,
|
||||
});
|
||||
let view = unsafe { device.create_image_view(&view_ci, None) }.ctx("vkCreateImageView")?;
|
||||
|
||||
Ok(Self {
|
||||
device,
|
||||
allocator,
|
||||
image,
|
||||
view,
|
||||
allocation: Some(allocation),
|
||||
extent: desc.extent,
|
||||
format: desc.format,
|
||||
aspect: desc.aspect,
|
||||
layout: vk::ImageLayout::UNDEFINED,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn image(&self) -> vk::Image {
|
||||
self.image
|
||||
}
|
||||
pub fn view(&self) -> vk::ImageView {
|
||||
self.view
|
||||
}
|
||||
pub fn extent(&self) -> vk::Extent3D {
|
||||
self.extent
|
||||
}
|
||||
pub fn format(&self) -> vk::Format {
|
||||
self.format
|
||||
}
|
||||
pub fn layout(&self) -> vk::ImageLayout {
|
||||
self.layout
|
||||
}
|
||||
|
||||
/// Барьер перехода раскладки на sync2. Повторный переход в ту же раскладку —
|
||||
/// не операция.
|
||||
pub fn transition_to(
|
||||
&mut self,
|
||||
cmd: vk::CommandBuffer,
|
||||
new_layout: vk::ImageLayout,
|
||||
src_stage: vk::PipelineStageFlags2,
|
||||
src_access: vk::AccessFlags2,
|
||||
dst_stage: vk::PipelineStageFlags2,
|
||||
dst_access: vk::AccessFlags2,
|
||||
) {
|
||||
if new_layout == self.layout {
|
||||
return;
|
||||
}
|
||||
let barrier = vk::ImageMemoryBarrier2::default()
|
||||
.src_stage_mask(src_stage)
|
||||
.src_access_mask(src_access)
|
||||
.dst_stage_mask(dst_stage)
|
||||
.dst_access_mask(dst_access)
|
||||
.old_layout(self.layout)
|
||||
.new_layout(new_layout)
|
||||
.image(self.image)
|
||||
.subresource_range(vk::ImageSubresourceRange {
|
||||
aspect_mask: self.aspect,
|
||||
base_mip_level: 0,
|
||||
level_count: 1,
|
||||
base_array_layer: 0,
|
||||
layer_count: 1,
|
||||
});
|
||||
let barriers = [barrier];
|
||||
let dependency = vk::DependencyInfo::default().image_memory_barriers(&barriers);
|
||||
unsafe { self.device.cmd_pipeline_barrier2(cmd, &dependency) };
|
||||
self.layout = new_layout;
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Image {
|
||||
fn drop(&mut self) {
|
||||
unsafe { self.device.destroy_image_view(self.view, None) };
|
||||
if let Some(allocation) = self.allocation.take() {
|
||||
let _ = self
|
||||
.allocator
|
||||
.lock()
|
||||
.map(|mut guard| guard.free(allocation));
|
||||
}
|
||||
unsafe { self.device.destroy_image(self.image, None) };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//! Весь Vulkan редактора — соответствует цели `simv_vk`.
|
||||
//!
|
||||
//! Единственный крейт с `ash` в зависимостях. В C++ это правило («весь Vulkan живёт в
|
||||
//! `src/vk/`») держится на дисциплине и комментарии в CLAUDE.md; здесь его проверяет
|
||||
//! компоновщик зависимостей: у `simv-mesh` и `simv-editor` `ash` просто нет.
|
||||
//!
|
||||
//! Публичный интерфейс `Renderer` не содержит типов Vulkan — как и в C++-версии, где
|
||||
//! ради этого заведён pImpl. В Rust того же добиваются приватные поля модуля, так что
|
||||
//! `struct Renderer::Impl` и вся возня с `unique_ptr` не понадобились.
|
||||
|
||||
pub mod buffer;
|
||||
pub mod context;
|
||||
pub mod error;
|
||||
pub mod gpu_mesh;
|
||||
pub mod grid_renderer;
|
||||
pub mod image;
|
||||
pub mod mesh_renderer;
|
||||
pub mod render_mode;
|
||||
pub mod renderer;
|
||||
pub mod shader;
|
||||
pub mod swapchain;
|
||||
|
||||
pub use context::{Context, ContextOptions};
|
||||
pub use error::{Result, VkError};
|
||||
pub use render_mode::RenderMode;
|
||||
pub use renderer::{FrameState, Renderer};
|
||||
@@ -0,0 +1,238 @@
|
||||
//! Конвейеры модели — перенос `simv::vk::MeshRenderer`.
|
||||
//!
|
||||
//! Два конвейера на один макет: заливка и `POLYGON_MODE_LINE` для каркаса. У линейного
|
||||
//! включено небольшое смещение глубины, чтобы каркас лёг поверх заливки, а не спорил
|
||||
//! с ней за z. Отсечение выключено — у загруженных моделей обход граней вперемешку.
|
||||
//!
|
||||
//! ВАЖНО: `MeshPushConstants` обязан байт в байт совпадать с блоком `push_constant`
|
||||
//! в `shaders/editor/mesh.vert` и `mesh.frag`. Поле `color.a` — это ФЛАГ (1 — плоская
|
||||
//! заливка, 0 — постоянный цвет каркаса), а не прозрачность. Меняете здесь — меняйте
|
||||
//! и в шейдере.
|
||||
|
||||
use ash::vk;
|
||||
use glam::{Mat4, Vec4};
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::error::{Ctx, Result, VkError};
|
||||
use crate::gpu_mesh::GpuMesh;
|
||||
use crate::render_mode::RenderMode;
|
||||
use crate::shader::{spirv, ShaderModule};
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
struct MeshPushConstants {
|
||||
mvp: Mat4,
|
||||
color: Vec4,
|
||||
}
|
||||
|
||||
/// `color.a`: 1 — плоская заливка, 0 — постоянный цвет.
|
||||
const SOLID: Vec4 = Vec4::new(0.72, 0.74, 0.78, 1.0);
|
||||
const WIRE: Vec4 = Vec4::new(0.78, 0.82, 0.88, 0.0);
|
||||
const OVERLAY: Vec4 = Vec4::new(0.06, 0.07, 0.09, 0.0);
|
||||
|
||||
pub struct MeshRenderer {
|
||||
device: ash::Device,
|
||||
layout: vk::PipelineLayout,
|
||||
fill: vk::Pipeline,
|
||||
line: vk::Pipeline,
|
||||
color_format: vk::Format,
|
||||
depth_format: vk::Format,
|
||||
}
|
||||
|
||||
impl MeshRenderer {
|
||||
pub fn new(ctx: &Context) -> Result<Self> {
|
||||
let device = ctx.device().clone();
|
||||
let ranges = [vk::PushConstantRange::default()
|
||||
.stage_flags(vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT)
|
||||
.offset(0)
|
||||
.size(std::mem::size_of::<MeshPushConstants>() as u32)];
|
||||
let layout_ci = vk::PipelineLayoutCreateInfo::default().push_constant_ranges(&ranges);
|
||||
let layout = unsafe { device.create_pipeline_layout(&layout_ci, None) }
|
||||
.ctx("vkCreatePipelineLayout(mesh)")?;
|
||||
|
||||
Ok(Self {
|
||||
device,
|
||||
layout,
|
||||
fill: vk::Pipeline::null(),
|
||||
line: vk::Pipeline::null(),
|
||||
color_format: vk::Format::UNDEFINED,
|
||||
depth_format: vk::Format::UNDEFINED,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn ensure_pipelines(
|
||||
&mut self,
|
||||
ctx: &Context,
|
||||
color_format: vk::Format,
|
||||
depth_format: vk::Format,
|
||||
) -> Result<()> {
|
||||
if self.fill != vk::Pipeline::null()
|
||||
&& self.color_format == color_format
|
||||
&& self.depth_format == depth_format
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
self.destroy_pipelines();
|
||||
self.color_format = color_format;
|
||||
self.depth_format = depth_format;
|
||||
self.fill = self.build_pipeline(ctx, vk::PolygonMode::FILL, false)?;
|
||||
self.line = self.build_pipeline(ctx, vk::PolygonMode::LINE, true)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_pipeline(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
polygon_mode: vk::PolygonMode,
|
||||
depth_bias: bool,
|
||||
) -> Result<vk::Pipeline> {
|
||||
let vert = ShaderModule::new(&self.device, spirv::MESH_VERT)?;
|
||||
let frag = ShaderModule::new(&self.device, spirv::MESH_FRAG)?;
|
||||
let stages = [
|
||||
vk::PipelineShaderStageCreateInfo::default()
|
||||
.stage(vk::ShaderStageFlags::VERTEX)
|
||||
.module(vert.handle())
|
||||
.name(c"main"),
|
||||
vk::PipelineShaderStageCreateInfo::default()
|
||||
.stage(vk::ShaderStageFlags::FRAGMENT)
|
||||
.module(frag.handle())
|
||||
.name(c"main"),
|
||||
];
|
||||
|
||||
// Поток вершин — плотный float3: `mesh.frag` восстанавливает нормаль грани из
|
||||
// экранных производных, так что нормали передавать не нужно.
|
||||
let bindings = [vk::VertexInputBindingDescription::default()
|
||||
.binding(0)
|
||||
.stride(std::mem::size_of::<f32>() as u32 * 3)
|
||||
.input_rate(vk::VertexInputRate::VERTEX)];
|
||||
let attributes = [vk::VertexInputAttributeDescription::default()
|
||||
.location(0)
|
||||
.binding(0)
|
||||
.format(vk::Format::R32G32B32_SFLOAT)
|
||||
.offset(0)];
|
||||
let vertex_input = vk::PipelineVertexInputStateCreateInfo::default()
|
||||
.vertex_binding_descriptions(&bindings)
|
||||
.vertex_attribute_descriptions(&attributes);
|
||||
|
||||
let input_assembly = vk::PipelineInputAssemblyStateCreateInfo::default()
|
||||
.topology(vk::PrimitiveTopology::TRIANGLE_LIST);
|
||||
let viewport = vk::PipelineViewportStateCreateInfo::default()
|
||||
.viewport_count(1)
|
||||
.scissor_count(1);
|
||||
|
||||
let mut rasterization = vk::PipelineRasterizationStateCreateInfo::default()
|
||||
.polygon_mode(polygon_mode)
|
||||
// У загруженных моделей обход граней бывает вперемешку.
|
||||
.cull_mode(vk::CullModeFlags::NONE)
|
||||
.front_face(vk::FrontFace::COUNTER_CLOCKWISE)
|
||||
.line_width(1.0);
|
||||
if depth_bias {
|
||||
// Подвинуть каркас к камере, чтобы он лёг поверх заливки.
|
||||
rasterization = rasterization
|
||||
.depth_bias_enable(true)
|
||||
.depth_bias_constant_factor(-1.0)
|
||||
.depth_bias_slope_factor(-1.0);
|
||||
}
|
||||
|
||||
let multisample = vk::PipelineMultisampleStateCreateInfo::default()
|
||||
.rasterization_samples(vk::SampleCountFlags::TYPE_1);
|
||||
let depth_stencil = vk::PipelineDepthStencilStateCreateInfo::default()
|
||||
.depth_test_enable(true)
|
||||
.depth_write_enable(true)
|
||||
.depth_compare_op(vk::CompareOp::LESS_OR_EQUAL)
|
||||
.min_depth_bounds(0.0)
|
||||
.max_depth_bounds(1.0);
|
||||
let blend_attachments = [vk::PipelineColorBlendAttachmentState::default()
|
||||
.color_write_mask(vk::ColorComponentFlags::RGBA)];
|
||||
let color_blend =
|
||||
vk::PipelineColorBlendStateCreateInfo::default().attachments(&blend_attachments);
|
||||
let dynamic_states = [vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR];
|
||||
let dynamic = vk::PipelineDynamicStateCreateInfo::default().dynamic_states(&dynamic_states);
|
||||
|
||||
let color_formats = [self.color_format];
|
||||
let mut rendering = vk::PipelineRenderingCreateInfo::default()
|
||||
.color_attachment_formats(&color_formats)
|
||||
.depth_attachment_format(self.depth_format);
|
||||
|
||||
let create_info = [vk::GraphicsPipelineCreateInfo::default()
|
||||
.stages(&stages)
|
||||
.vertex_input_state(&vertex_input)
|
||||
.input_assembly_state(&input_assembly)
|
||||
.viewport_state(&viewport)
|
||||
.rasterization_state(&rasterization)
|
||||
.multisample_state(&multisample)
|
||||
.depth_stencil_state(&depth_stencil)
|
||||
.color_blend_state(&color_blend)
|
||||
.dynamic_state(&dynamic)
|
||||
.layout(self.layout)
|
||||
.push_next(&mut rendering)];
|
||||
|
||||
let pipelines = unsafe {
|
||||
self.device
|
||||
.create_graphics_pipelines(ctx.pipeline_cache(), &create_info, None)
|
||||
}
|
||||
.map_err(|(_, e)| VkError::Api {
|
||||
context: "vkCreateGraphicsPipelines(mesh)",
|
||||
source: e,
|
||||
})?;
|
||||
Ok(pipelines[0])
|
||||
}
|
||||
|
||||
pub fn draw(&self, cmd: vk::CommandBuffer, mesh: &GpuMesh, mvp: &Mat4, mode: RenderMode) {
|
||||
if self.fill == vk::Pipeline::null() || self.line == vk::Pipeline::null() {
|
||||
return;
|
||||
}
|
||||
unsafe {
|
||||
self.device
|
||||
.cmd_bind_vertex_buffers(cmd, 0, &[mesh.vertex_buffer()], &[0]);
|
||||
self.device
|
||||
.cmd_bind_index_buffer(cmd, mesh.index_buffer(), 0, vk::IndexType::UINT32);
|
||||
}
|
||||
|
||||
let draw_with = |pipeline: vk::Pipeline, color: Vec4| {
|
||||
let push = MeshPushConstants { mvp: *mvp, color };
|
||||
unsafe {
|
||||
self.device
|
||||
.cmd_bind_pipeline(cmd, vk::PipelineBindPoint::GRAPHICS, pipeline);
|
||||
self.device.cmd_push_constants(
|
||||
cmd,
|
||||
self.layout,
|
||||
vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT,
|
||||
0,
|
||||
bytemuck::bytes_of(&push),
|
||||
);
|
||||
self.device
|
||||
.cmd_draw_indexed(cmd, mesh.index_count(), 1, 0, 0, 0);
|
||||
}
|
||||
};
|
||||
|
||||
match mode {
|
||||
RenderMode::Solid => draw_with(self.fill, SOLID),
|
||||
RenderMode::Wireframe => draw_with(self.line, WIRE),
|
||||
RenderMode::SolidWire => {
|
||||
draw_with(self.fill, SOLID);
|
||||
draw_with(self.line, OVERLAY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn destroy_pipelines(&mut self) {
|
||||
unsafe {
|
||||
if self.fill != vk::Pipeline::null() {
|
||||
self.device.destroy_pipeline(self.fill, None);
|
||||
self.fill = vk::Pipeline::null();
|
||||
}
|
||||
if self.line != vk::Pipeline::null() {
|
||||
self.device.destroy_pipeline(self.line, None);
|
||||
self.line = vk::Pipeline::null();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MeshRenderer {
|
||||
fn drop(&mut self) {
|
||||
self.destroy_pipelines();
|
||||
unsafe { self.device.destroy_pipeline_layout(self.layout, None) };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
//! Режим отображения модели — перенос `simv::vk::RenderMode`.
|
||||
|
||||
/// Как рисуется загруженная модель во вьюпорте.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
|
||||
pub enum RenderMode {
|
||||
/// Залитые треугольники с плоской заливкой по нормали грани.
|
||||
#[default]
|
||||
Solid,
|
||||
/// Только рёбра треугольников.
|
||||
Wireframe,
|
||||
/// Заливка с наложенным поверх каркасом.
|
||||
SolidWire,
|
||||
}
|
||||
@@ -0,0 +1,626 @@
|
||||
//! Кадр целиком — перенос `simv::vk::Renderer`.
|
||||
//!
|
||||
//! Владеет всем стеком показа: контекстом, цепочкой показа, глубинным вложением,
|
||||
//! покадровыми командными буферами и синхронизацией, бэкендом egui и обоими
|
||||
//! рендерерами сцены. В публичном интерфейсе нет ни одного типа Vulkan — ровно та же
|
||||
//! цель, ради которой в C++ заведён pImpl, но здесь её решают приватные поля модуля,
|
||||
//! так что `struct Renderer::Impl` и `unique_ptr` не понадобились.
|
||||
//!
|
||||
//! Расхождение с C++-версией: там замыкание интерфейса хранится полем
|
||||
//! (`SetUiCallback`), здесь оно передаётся параметром в [`Renderer::draw_frame`].
|
||||
//! Причина не стилистическая. Замыкание в C++ захватывает по ссылке разом камеру,
|
||||
//! панели и сам рендерер; в Rust хранить такой `FnMut` внутри объекта, который это же
|
||||
//! замыкание и вызывает, значит одолжить рендерер дважды — компилятор не пропустит.
|
||||
//! Параметр развязывает время жизни и ничего не стоит.
|
||||
|
||||
use ash::vk;
|
||||
use egui_ash_renderer::{DynamicRendering, Options};
|
||||
use glam::Mat4;
|
||||
use simv_mesh::Mesh;
|
||||
|
||||
use crate::context::{Context, ContextOptions};
|
||||
use crate::error::{Ctx, Result, VkError};
|
||||
use crate::gpu_mesh::GpuMesh;
|
||||
use crate::grid_renderer::GridRenderer;
|
||||
use crate::image::{Image, ImageDesc};
|
||||
use crate::mesh_renderer::MeshRenderer;
|
||||
use crate::render_mode::RenderMode;
|
||||
use crate::swapchain::Swapchain;
|
||||
|
||||
/// Бэкенд egui поверх нашего же аллокатора: обёртка `GpuAllocator` — это
|
||||
/// `Arc<Mutex<gpu_allocator::vulkan::Allocator>>`, тот самый, что раздаёт память
|
||||
/// буферам и изображениям сцены. Одна куча на всё приложение.
|
||||
type EguiRenderer = egui_ash_renderer::Renderer<egui_ash_renderer::allocator::GpuAllocator>;
|
||||
|
||||
const FRAMES_IN_FLIGHT: usize = 2;
|
||||
const DEPTH_FORMAT: vk::Format = vk::Format::D32_SFLOAT;
|
||||
const CLEAR_COLOR: [f32; 4] = [0.05, 0.06, 0.08, 1.0];
|
||||
|
||||
struct FrameSlot {
|
||||
cmd_pool: vk::CommandPool,
|
||||
cmd: vk::CommandBuffer,
|
||||
image_available: vk::Semaphore,
|
||||
in_flight: vk::Fence,
|
||||
/// Текстуры egui, которые перестали быть нужны на кадре, использовавшем этот слот.
|
||||
/// Освобождаются, когда забор слота снова сработает, — то есть когда видеокарта
|
||||
/// точно закончила с тем кадром.
|
||||
textures_to_free: Vec<egui::TextureId>,
|
||||
}
|
||||
|
||||
pub struct Renderer {
|
||||
// Поля объявлены в порядке уничтожения: всё, что держит клон `ash::Device` или
|
||||
// ссылку на аллокатор, обязано уйти раньше контекста, а контекст — последним.
|
||||
egui_renderer: EguiRenderer,
|
||||
egui_state: egui_winit::State,
|
||||
egui_ctx: egui::Context,
|
||||
egui_pool: vk::CommandPool,
|
||||
|
||||
gpu_mesh: Option<GpuMesh>,
|
||||
mesh_renderer: MeshRenderer,
|
||||
grid_renderer: GridRenderer,
|
||||
|
||||
depth: Image,
|
||||
frames: Vec<FrameSlot>,
|
||||
render_finished: Vec<vk::Semaphore>,
|
||||
cursor: usize,
|
||||
|
||||
swapchain: Swapchain,
|
||||
context: Context,
|
||||
|
||||
/// Матрица модели: загруженная сетка переносится в начало координат.
|
||||
model: Mat4,
|
||||
}
|
||||
|
||||
/// Что интерфейс сообщает сцене на этом кадре.
|
||||
///
|
||||
/// В C++ то же состояние попадало в рендерер сеттерами (`SetViewProj`,
|
||||
/// `SetRenderMode`, `SetGridVisible`), которые UI-колбэк вызывал прямо из
|
||||
/// `DrawFrame`. Здесь колбэк вызывается из метода рендерера, поэтому трогать
|
||||
/// рендерер оттуда нельзя — он уже одолжен. Состояние вместо этого возвращается
|
||||
/// из замыкания и применяется к тому же кадру, без отставания на кадр.
|
||||
pub struct FrameState {
|
||||
pub view_proj: Mat4,
|
||||
pub render_mode: RenderMode,
|
||||
pub grid_visible: bool,
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
pub fn new(window: &simv_core::Window) -> Result<Self> {
|
||||
let context = Context::new(
|
||||
window,
|
||||
&ContextOptions {
|
||||
// В редакторе нет вычислительной работы.
|
||||
require_async_compute: false,
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
let swapchain = Swapchain::new(&context, window, true)?;
|
||||
|
||||
let (frames, render_finished) = create_frame_state(&context, swapchain.frames().len())?;
|
||||
let depth = create_depth(&context, swapchain.extent())?;
|
||||
|
||||
let egui_ctx = egui::Context::default();
|
||||
let egui_state = egui_winit::State::new(
|
||||
egui_ctx.clone(),
|
||||
egui::ViewportId::ROOT,
|
||||
window.raw(),
|
||||
Some(window.raw().scale_factor() as f32),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let egui_pool = {
|
||||
let ci = vk::CommandPoolCreateInfo::default()
|
||||
.flags(vk::CommandPoolCreateFlags::TRANSIENT)
|
||||
.queue_family_index(context.graphics_family());
|
||||
unsafe { context.device().create_command_pool(&ci, None) }
|
||||
.ctx("vkCreateCommandPool(egui)")?
|
||||
};
|
||||
let egui_renderer = EguiRenderer::with_gpu_allocator(
|
||||
context.allocator().clone(),
|
||||
context.device().clone(),
|
||||
egui_ash_renderer::RenderMode::DynamicRendering(dynamic_rendering(&swapchain)),
|
||||
egui_options(),
|
||||
)
|
||||
.map_err(|e| VkError::Other(format!("egui_ash_renderer: {e}")))?;
|
||||
|
||||
let mut mesh_renderer = MeshRenderer::new(&context)?;
|
||||
let mut grid_renderer = GridRenderer::new(&context)?;
|
||||
mesh_renderer.ensure_pipelines(&context, swapchain.color_format(), DEPTH_FORMAT)?;
|
||||
grid_renderer.ensure_pipeline(&context, swapchain.color_format(), DEPTH_FORMAT)?;
|
||||
|
||||
Ok(Self {
|
||||
egui_renderer,
|
||||
egui_state,
|
||||
egui_ctx,
|
||||
egui_pool,
|
||||
gpu_mesh: None,
|
||||
mesh_renderer,
|
||||
grid_renderer,
|
||||
depth,
|
||||
frames,
|
||||
render_finished,
|
||||
cursor: 0,
|
||||
swapchain,
|
||||
context,
|
||||
model: Mat4::IDENTITY,
|
||||
})
|
||||
}
|
||||
|
||||
/// Отношение сторон кадрового буфера — для матрицы проекции.
|
||||
pub fn aspect_ratio(&self) -> f32 {
|
||||
let extent = self.swapchain.extent();
|
||||
if extent.height == 0 {
|
||||
return 1.0;
|
||||
}
|
||||
extent.width as f32 / extent.height as f32
|
||||
}
|
||||
|
||||
/// Скормить событие окна интерфейсу. Возвращает `true`, если egui его забрал себе
|
||||
/// и сцене оно не предназначено.
|
||||
pub fn on_window_event(
|
||||
&mut self,
|
||||
window: &simv_core::Window,
|
||||
event: &winit::event::WindowEvent,
|
||||
) -> bool {
|
||||
self.egui_state
|
||||
.on_window_event(window.raw(), event)
|
||||
.consumed
|
||||
}
|
||||
|
||||
/// Выгрузить меш на видеокарту и заново отцентрировать модель на начале координат.
|
||||
///
|
||||
/// Ожидание простоя устройства здесь допустимо: загрузки редки. В покадровые пути
|
||||
/// этот приём переносить нельзя.
|
||||
pub fn set_mesh_cpu(&mut self, mesh: &Mesh) -> Result<()> {
|
||||
if mesh.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
self.context.wait_idle();
|
||||
self.gpu_mesh = Some(GpuMesh::upload(&self.context, mesh)?);
|
||||
let center = 0.5 * (mesh.bbox_min + mesh.bbox_max);
|
||||
self.model = Mat4::from_translation(-center);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn clear_mesh(&mut self) {
|
||||
self.context.wait_idle();
|
||||
self.gpu_mesh = None;
|
||||
self.model = Mat4::IDENTITY;
|
||||
}
|
||||
|
||||
pub fn wait_idle(&self) {
|
||||
self.context.wait_idle();
|
||||
}
|
||||
|
||||
/// Нарисовать кадр: интерфейс, сетка, модель — и показать.
|
||||
///
|
||||
/// Замыкание вызывается между сбором ввода и тесселяцией, то есть ровно там же,
|
||||
/// где C++-версия зовёт `uiCallback` между `ImGui::NewFrame` и `ImGui::Render`.
|
||||
pub fn draw_frame(
|
||||
&mut self,
|
||||
window: &simv_core::Window,
|
||||
mut ui: impl FnMut(&egui::Context) -> FrameState,
|
||||
) -> Result<()> {
|
||||
let (fbw, fbh) = window.framebuffer_size();
|
||||
if fbw == 0 || fbh == 0 {
|
||||
return Ok(()); // свёрнуто — рисовать некуда
|
||||
}
|
||||
|
||||
let slot = self.cursor % FRAMES_IN_FLIGHT;
|
||||
let device = self.context.device().clone();
|
||||
|
||||
let in_flight = self.frames[slot].in_flight;
|
||||
unsafe { device.wait_for_fences(&[in_flight], true, u64::MAX) }.ctx("vkWaitForFences")?;
|
||||
|
||||
// Забор сработал — значит кадр, занимавший этот слот, отсчитан, и его текстуры
|
||||
// можно отдавать.
|
||||
for id in std::mem::take(&mut self.frames[slot].textures_to_free) {
|
||||
self.egui_renderer
|
||||
.free_texture(id)
|
||||
.map_err(|e| VkError::Other(format!("egui free_texture: {e}")))?;
|
||||
}
|
||||
|
||||
let acquired = unsafe {
|
||||
self.context.swapchain_loader().acquire_next_image(
|
||||
self.swapchain.handle(),
|
||||
u64::MAX,
|
||||
self.frames[slot].image_available,
|
||||
vk::Fence::null(),
|
||||
)
|
||||
};
|
||||
let image_index = match acquired {
|
||||
Ok((index, _suboptimal)) => index as usize,
|
||||
Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => {
|
||||
self.recreate_swap_dependent(window)?;
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => return Err(VkError::Api { context: "vkAcquireNextImageKHR", source: e }),
|
||||
};
|
||||
|
||||
// ── Кадр egui: панели заполняются переданным замыканием ──────────────
|
||||
let raw_input = self.egui_state.take_egui_input(window.raw());
|
||||
// egui может прогнать интерфейс не один раз за кадр (например, если виджет
|
||||
// попросил пересчёт), поэтому берётся последнее состояние.
|
||||
let mut frame_state = None;
|
||||
let mut output = self.egui_ctx.run_ui(raw_input, |u| {
|
||||
let ctx = u.ctx().clone();
|
||||
frame_state = Some(ui(&ctx));
|
||||
});
|
||||
let frame_state = frame_state.expect("замыкание интерфейса не вернуло состояние кадра");
|
||||
self.egui_state
|
||||
.handle_platform_output(window.raw(), output.platform_output);
|
||||
let primitives = self
|
||||
.egui_ctx
|
||||
.tessellate(output.shapes, output.pixels_per_point);
|
||||
|
||||
// egui 0.36 отдаёт на текстуру список правок, а не одну: атлас шрифта может
|
||||
// дорасти несколькими кусками за кадр.
|
||||
for (&id, deltas) in &output.textures_delta.set {
|
||||
for delta in deltas {
|
||||
self.egui_renderer
|
||||
.set_texture(
|
||||
self.context.graphics_queue(),
|
||||
self.egui_pool,
|
||||
id,
|
||||
delta,
|
||||
)
|
||||
.map_err(|e| VkError::Other(format!("egui set_texture: {e}")))?;
|
||||
}
|
||||
}
|
||||
// Освобождение откладывается на два кадра: сейчас эти текстуры ещё попадут в
|
||||
// командный буфер, который только предстоит отправить.
|
||||
self.frames[slot]
|
||||
.textures_to_free
|
||||
.extend(output.textures_delta.free.iter().copied());
|
||||
// У `TexturesDelta` есть `Drop`, который в отладочной сборке падает, если
|
||||
// правки уронили не применив. Разобрали — говорим об этом явно.
|
||||
output.textures_delta.clear();
|
||||
|
||||
// ── Запись команд ────────────────────────────────────────────────────
|
||||
let cmd = self.frames[slot].cmd;
|
||||
unsafe {
|
||||
device.reset_fences(&[in_flight]).ctx("vkResetFences")?;
|
||||
device
|
||||
.reset_command_buffer(cmd, vk::CommandBufferResetFlags::empty())
|
||||
.ctx("vkResetCommandBuffer")?;
|
||||
let begin = vk::CommandBufferBeginInfo::default()
|
||||
.flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT);
|
||||
device
|
||||
.begin_command_buffer(cmd, &begin)
|
||||
.ctx("vkBeginCommandBuffer")?;
|
||||
}
|
||||
|
||||
let swap_image = self.swapchain.frames()[image_index].image;
|
||||
let swap_view = self.swapchain.frames()[image_index].view;
|
||||
let extent = self.swapchain.extent();
|
||||
|
||||
// Цвет: UNDEFINED → COLOR_ATTACHMENT_OPTIMAL, глубина: UNDEFINED → DEPTH_ATTACHMENT.
|
||||
{
|
||||
let color_barrier = vk::ImageMemoryBarrier2::default()
|
||||
.src_stage_mask(vk::PipelineStageFlags2::TOP_OF_PIPE)
|
||||
.dst_stage_mask(vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT)
|
||||
.dst_access_mask(vk::AccessFlags2::COLOR_ATTACHMENT_WRITE)
|
||||
.old_layout(vk::ImageLayout::UNDEFINED)
|
||||
.new_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
|
||||
.image(swap_image)
|
||||
.subresource_range(color_range());
|
||||
let depth_barrier = vk::ImageMemoryBarrier2::default()
|
||||
.src_stage_mask(vk::PipelineStageFlags2::TOP_OF_PIPE)
|
||||
.dst_stage_mask(
|
||||
vk::PipelineStageFlags2::EARLY_FRAGMENT_TESTS
|
||||
| vk::PipelineStageFlags2::LATE_FRAGMENT_TESTS,
|
||||
)
|
||||
.dst_access_mask(vk::AccessFlags2::DEPTH_STENCIL_ATTACHMENT_WRITE)
|
||||
.old_layout(vk::ImageLayout::UNDEFINED)
|
||||
.new_layout(vk::ImageLayout::DEPTH_ATTACHMENT_OPTIMAL)
|
||||
.image(self.depth.image())
|
||||
.subresource_range(depth_range());
|
||||
let barriers = [color_barrier, depth_barrier];
|
||||
let dependency = vk::DependencyInfo::default().image_memory_barriers(&barriers);
|
||||
unsafe { device.cmd_pipeline_barrier2(cmd, &dependency) };
|
||||
}
|
||||
|
||||
let color_attachment = vk::RenderingAttachmentInfo::default()
|
||||
.image_view(swap_view)
|
||||
.image_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
|
||||
.load_op(vk::AttachmentLoadOp::CLEAR)
|
||||
.store_op(vk::AttachmentStoreOp::STORE)
|
||||
.clear_value(vk::ClearValue {
|
||||
color: vk::ClearColorValue {
|
||||
float32: CLEAR_COLOR,
|
||||
},
|
||||
});
|
||||
let depth_attachment = vk::RenderingAttachmentInfo::default()
|
||||
.image_view(self.depth.view())
|
||||
.image_layout(vk::ImageLayout::DEPTH_ATTACHMENT_OPTIMAL)
|
||||
.load_op(vk::AttachmentLoadOp::CLEAR)
|
||||
.store_op(vk::AttachmentStoreOp::DONT_CARE)
|
||||
.clear_value(vk::ClearValue {
|
||||
depth_stencil: vk::ClearDepthStencilValue {
|
||||
depth: 1.0,
|
||||
stencil: 0,
|
||||
},
|
||||
});
|
||||
let color_attachments = [color_attachment];
|
||||
let rendering = vk::RenderingInfo::default()
|
||||
.render_area(vk::Rect2D {
|
||||
offset: vk::Offset2D { x: 0, y: 0 },
|
||||
extent,
|
||||
})
|
||||
.layer_count(1)
|
||||
.color_attachments(&color_attachments)
|
||||
.depth_attachment(&depth_attachment);
|
||||
|
||||
unsafe {
|
||||
device.cmd_begin_rendering(cmd, &rendering);
|
||||
|
||||
// Область вывода и отсечения общие для обоих рендереров сцены.
|
||||
let viewport = [vk::Viewport {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
width: extent.width as f32,
|
||||
height: extent.height as f32,
|
||||
min_depth: 0.0,
|
||||
max_depth: 1.0,
|
||||
}];
|
||||
let scissor = [vk::Rect2D {
|
||||
offset: vk::Offset2D { x: 0, y: 0 },
|
||||
extent,
|
||||
}];
|
||||
device.cmd_set_viewport(cmd, 0, &viewport);
|
||||
device.cmd_set_scissor(cmd, 0, &scissor);
|
||||
}
|
||||
|
||||
if frame_state.grid_visible {
|
||||
self.grid_renderer.draw(cmd, &frame_state.view_proj);
|
||||
}
|
||||
if let Some(mesh) = &self.gpu_mesh {
|
||||
self.mesh_renderer.draw(
|
||||
cmd,
|
||||
mesh,
|
||||
&(frame_state.view_proj * self.model),
|
||||
frame_state.render_mode,
|
||||
);
|
||||
}
|
||||
|
||||
self.egui_renderer
|
||||
.cmd_draw(cmd, extent, output.pixels_per_point, &primitives)
|
||||
.map_err(|e| VkError::Other(format!("egui cmd_draw: {e}")))?;
|
||||
|
||||
unsafe {
|
||||
device.cmd_end_rendering(cmd);
|
||||
|
||||
// Цвет: COLOR_ATTACHMENT_OPTIMAL → PRESENT_SRC.
|
||||
let present_barrier = [vk::ImageMemoryBarrier2::default()
|
||||
.src_stage_mask(vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT)
|
||||
.src_access_mask(vk::AccessFlags2::COLOR_ATTACHMENT_WRITE)
|
||||
.dst_stage_mask(vk::PipelineStageFlags2::BOTTOM_OF_PIPE)
|
||||
.old_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
|
||||
.new_layout(vk::ImageLayout::PRESENT_SRC_KHR)
|
||||
.image(swap_image)
|
||||
.subresource_range(color_range())];
|
||||
let dependency = vk::DependencyInfo::default().image_memory_barriers(&present_barrier);
|
||||
device.cmd_pipeline_barrier2(cmd, &dependency);
|
||||
device
|
||||
.end_command_buffer(cmd)
|
||||
.ctx("vkEndCommandBuffer")?;
|
||||
}
|
||||
|
||||
// ── Отправка и показ ─────────────────────────────────────────────────
|
||||
let render_finished = self.render_finished[image_index];
|
||||
let wait = [vk::SemaphoreSubmitInfo::default()
|
||||
.semaphore(self.frames[slot].image_available)
|
||||
.stage_mask(vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT)];
|
||||
let signal = [vk::SemaphoreSubmitInfo::default()
|
||||
.semaphore(render_finished)
|
||||
.stage_mask(vk::PipelineStageFlags2::ALL_GRAPHICS)];
|
||||
let cmd_infos = [vk::CommandBufferSubmitInfo::default().command_buffer(cmd)];
|
||||
let submit = [vk::SubmitInfo2::default()
|
||||
.wait_semaphore_infos(&wait)
|
||||
.command_buffer_infos(&cmd_infos)
|
||||
.signal_semaphore_infos(&signal)];
|
||||
unsafe { device.queue_submit2(self.context.graphics_queue(), &submit, in_flight) }
|
||||
.ctx("vkQueueSubmit2")?;
|
||||
|
||||
let swapchains = [self.swapchain.handle()];
|
||||
let wait_semaphores = [render_finished];
|
||||
let indices = [image_index as u32];
|
||||
let present = vk::PresentInfoKHR::default()
|
||||
.wait_semaphores(&wait_semaphores)
|
||||
.swapchains(&swapchains)
|
||||
.image_indices(&indices);
|
||||
let presented = unsafe {
|
||||
self.context
|
||||
.swapchain_loader()
|
||||
.queue_present(self.context.graphics_queue(), &present)
|
||||
};
|
||||
match presented {
|
||||
Ok(false) => {}
|
||||
Ok(true) | Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => {
|
||||
self.recreate_swap_dependent(window)?;
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(VkError::Api {
|
||||
context: "vkQueuePresentKHR",
|
||||
source: e,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
self.cursor = self.cursor.wrapping_add(1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Пересобрать всё, что зависит от цепочки показа.
|
||||
///
|
||||
/// Порядок важен и перенесён из C++-версии дословно: неудачный `vkAcquireNextImage`
|
||||
/// может оставить семафор `image_available` взведённым, поэтому покадровые семафоры
|
||||
/// пересоздаются целиком; число образов цепочки могло измениться, поэтому семафоры
|
||||
/// «кадр готов» тоже; и только потом переcобираются конвейеры.
|
||||
fn recreate_swap_dependent(&mut self, window: &simv_core::Window) -> Result<()> {
|
||||
self.context.wait_idle();
|
||||
self.swapchain.recreate(&self.context, window, true)?;
|
||||
self.depth = create_depth(&self.context, self.swapchain.extent())?;
|
||||
|
||||
let device = self.context.device();
|
||||
for frame in &mut self.frames {
|
||||
unsafe { device.destroy_semaphore(frame.image_available, None) };
|
||||
frame.image_available =
|
||||
unsafe { device.create_semaphore(&vk::SemaphoreCreateInfo::default(), None) }
|
||||
.ctx("vkCreateSemaphore")?;
|
||||
}
|
||||
for semaphore in self.render_finished.drain(..) {
|
||||
unsafe { device.destroy_semaphore(semaphore, None) };
|
||||
}
|
||||
for _ in 0..self.swapchain.frames().len() {
|
||||
self.render_finished.push(
|
||||
unsafe { device.create_semaphore(&vk::SemaphoreCreateInfo::default(), None) }
|
||||
.ctx("vkCreateSemaphore")?,
|
||||
);
|
||||
}
|
||||
|
||||
let color_format = self.swapchain.color_format();
|
||||
self.mesh_renderer
|
||||
.ensure_pipelines(&self.context, color_format, DEPTH_FORMAT)?;
|
||||
self.grid_renderer
|
||||
.ensure_pipeline(&self.context, color_format, DEPTH_FORMAT)?;
|
||||
self.egui_renderer
|
||||
.set_render_mode(egui_ash_renderer::RenderMode::DynamicRendering(
|
||||
dynamic_rendering(&self.swapchain),
|
||||
))
|
||||
.map_err(|e| VkError::Other(format!("egui set_render_mode: {e}")))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Renderer {
|
||||
fn drop(&mut self) {
|
||||
self.context.wait_idle();
|
||||
let device = self.context.device();
|
||||
unsafe {
|
||||
for frame in &self.frames {
|
||||
device.destroy_fence(frame.in_flight, None);
|
||||
device.destroy_semaphore(frame.image_available, None);
|
||||
device.destroy_command_pool(frame.cmd_pool, None);
|
||||
}
|
||||
for &semaphore in &self.render_finished {
|
||||
device.destroy_semaphore(semaphore, None);
|
||||
}
|
||||
device.destroy_command_pool(self.egui_pool, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Вспомогательное
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn color_range() -> vk::ImageSubresourceRange {
|
||||
vk::ImageSubresourceRange {
|
||||
aspect_mask: vk::ImageAspectFlags::COLOR,
|
||||
base_mip_level: 0,
|
||||
level_count: 1,
|
||||
base_array_layer: 0,
|
||||
layer_count: 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn depth_range() -> vk::ImageSubresourceRange {
|
||||
vk::ImageSubresourceRange {
|
||||
aspect_mask: vk::ImageAspectFlags::DEPTH,
|
||||
base_mip_level: 0,
|
||||
level_count: 1,
|
||||
base_array_layer: 0,
|
||||
layer_count: 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Форматы вложений для конвейера egui. Глубина здесь не для того, чтобы egui её
|
||||
/// использовал, а чтобы его конвейер совпал с проходом: интерфейс рисуется в тот же
|
||||
/// `vkCmdBeginRendering`, что и сцена, а у прохода есть глубинное вложение.
|
||||
fn dynamic_rendering(swapchain: &Swapchain) -> DynamicRendering {
|
||||
DynamicRendering {
|
||||
color_attachment_format: swapchain.color_format(),
|
||||
depth_attachment_format: Some(DEPTH_FORMAT),
|
||||
stencil_attachment_format: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn egui_options() -> Options {
|
||||
Options {
|
||||
in_flight_frames: FRAMES_IN_FLIGHT,
|
||||
// Интерфейс рисуется поверх сцены и в глубину не пишет.
|
||||
enable_depth_test: false,
|
||||
enable_depth_write: false,
|
||||
// Цель показа в формате UNORM, значит перевод в sRGB делает фрагментный шейдер.
|
||||
srgb_framebuffer: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn create_depth(ctx: &Context, extent: vk::Extent2D) -> Result<Image> {
|
||||
Image::new(
|
||||
ctx,
|
||||
"depth",
|
||||
&ImageDesc {
|
||||
extent: vk::Extent3D {
|
||||
width: extent.width.max(1),
|
||||
height: extent.height.max(1),
|
||||
depth: 1,
|
||||
},
|
||||
format: DEPTH_FORMAT,
|
||||
usage: vk::ImageUsageFlags::DEPTH_STENCIL_ATTACHMENT,
|
||||
image_type: vk::ImageType::TYPE_2D,
|
||||
view_type: vk::ImageViewType::TYPE_2D,
|
||||
aspect: vk::ImageAspectFlags::DEPTH,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn create_frame_state(
|
||||
ctx: &Context,
|
||||
swapchain_image_count: usize,
|
||||
) -> Result<(Vec<FrameSlot>, Vec<vk::Semaphore>)> {
|
||||
let device = ctx.device();
|
||||
let mut frames = Vec::with_capacity(FRAMES_IN_FLIGHT);
|
||||
for _ in 0..FRAMES_IN_FLIGHT {
|
||||
let pool_ci = vk::CommandPoolCreateInfo::default()
|
||||
.flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER)
|
||||
.queue_family_index(ctx.graphics_family());
|
||||
let cmd_pool =
|
||||
unsafe { device.create_command_pool(&pool_ci, None) }.ctx("vkCreateCommandPool")?;
|
||||
|
||||
let alloc_ci = vk::CommandBufferAllocateInfo::default()
|
||||
.command_pool(cmd_pool)
|
||||
.level(vk::CommandBufferLevel::PRIMARY)
|
||||
.command_buffer_count(1);
|
||||
let cmd = unsafe { device.allocate_command_buffers(&alloc_ci) }
|
||||
.ctx("vkAllocateCommandBuffers")?[0];
|
||||
|
||||
let image_available =
|
||||
unsafe { device.create_semaphore(&vk::SemaphoreCreateInfo::default(), None) }
|
||||
.ctx("vkCreateSemaphore")?;
|
||||
// Взведён при создании: первый кадр не должен ждать несуществующей отправки.
|
||||
let fence_ci = vk::FenceCreateInfo::default().flags(vk::FenceCreateFlags::SIGNALED);
|
||||
let in_flight =
|
||||
unsafe { device.create_fence(&fence_ci, None) }.ctx("vkCreateFence")?;
|
||||
|
||||
frames.push(FrameSlot {
|
||||
cmd_pool,
|
||||
cmd,
|
||||
image_available,
|
||||
in_flight,
|
||||
textures_to_free: Vec::new(),
|
||||
});
|
||||
}
|
||||
|
||||
// По одному семафору «кадр готов» на образ цепочки: он взводится при показе.
|
||||
let mut render_finished = Vec::with_capacity(swapchain_image_count);
|
||||
for _ in 0..swapchain_image_count {
|
||||
render_finished.push(
|
||||
unsafe { device.create_semaphore(&vk::SemaphoreCreateInfo::default(), None) }
|
||||
.ctx("vkCreateSemaphore")?,
|
||||
);
|
||||
}
|
||||
|
||||
Ok((frames, render_finished))
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
//! Шейдерные модули — перенос `simv::vk::Shader`.
|
||||
//!
|
||||
//! Главное отличие от C++-версии: SPIR-V не ищется на диске, а зашит в исполняемый
|
||||
//! файл на этапе сборки (см. `build.rs`). Вместе с `FindSpvPath` исчезли и
|
||||
//! POST_BUILD-копирование каталога `spirv/`, и требование запускать редактор из
|
||||
//! каталога бинаря, и целый класс ошибок «шейдер не найден» во время выполнения:
|
||||
//! отсутствующий шейдер теперь ломает сборку, а не запуск.
|
||||
|
||||
use std::io::Cursor;
|
||||
|
||||
use ash::vk;
|
||||
|
||||
use crate::error::{Ctx, Result};
|
||||
|
||||
/// Скомпилированный SPIR-V, вшитый в бинарь. Пути соответствуют раскладке
|
||||
/// `shaders/editor/` в корне репозитория.
|
||||
pub mod spirv {
|
||||
pub const MESH_VERT: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/editor/mesh.vert.spv"));
|
||||
pub const MESH_FRAG: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/editor/mesh.frag.spv"));
|
||||
pub const GRID_VERT: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/editor/grid.vert.spv"));
|
||||
pub const GRID_FRAG: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/editor/grid.frag.spv"));
|
||||
}
|
||||
|
||||
/// Владеющая обёртка над `VkShaderModule`: уничтожается на выходе из области
|
||||
/// видимости, в том числе если сборка конвейера завершилась ошибкой.
|
||||
pub struct ShaderModule {
|
||||
device: ash::Device,
|
||||
handle: vk::ShaderModule,
|
||||
}
|
||||
|
||||
impl ShaderModule {
|
||||
pub fn new(device: &ash::Device, spirv_bytes: &[u8]) -> Result<Self> {
|
||||
// `read_spv` заодно снимает вопрос выравнивания: `include_bytes!` даёт срез
|
||||
// байтов, а Vulkan хочет 32-битные слова.
|
||||
let words = ash::util::read_spv(&mut Cursor::new(spirv_bytes))
|
||||
.map_err(|e| crate::error::VkError::Other(format!("битый SPIR-V: {e}")))?;
|
||||
let ci = vk::ShaderModuleCreateInfo::default().code(&words);
|
||||
let handle =
|
||||
unsafe { device.create_shader_module(&ci, None) }.ctx("vkCreateShaderModule")?;
|
||||
Ok(Self {
|
||||
device: device.clone(),
|
||||
handle,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn handle(&self) -> vk::ShaderModule {
|
||||
self.handle
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ShaderModule {
|
||||
fn drop(&mut self) {
|
||||
unsafe { self.device.destroy_shader_module(self.handle, None) };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
//! Цепочка показа — перенос `simv::vk::Swapchain`.
|
||||
//!
|
||||
//! В C++ вся эта функция умещается в вызов `vkb::SwapchainBuilder`: формат, режим
|
||||
//! показа, число образов и представления создаются за него. Здесь то же самое сделано
|
||||
//! руками — выбор формата с откатом, зажим числа образов и размера по возможностям
|
||||
//! поверхности, создание представлений.
|
||||
|
||||
use ash::vk;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::error::{Ctx, Result};
|
||||
|
||||
pub struct SwapchainFrame {
|
||||
pub image: vk::Image,
|
||||
pub view: vk::ImageView,
|
||||
}
|
||||
|
||||
pub struct Swapchain {
|
||||
device: ash::Device,
|
||||
loader: ash::khr::swapchain::Device,
|
||||
handle: vk::SwapchainKHR,
|
||||
frames: Vec<SwapchainFrame>,
|
||||
color_format: vk::Format,
|
||||
extent: vk::Extent2D,
|
||||
}
|
||||
|
||||
impl Swapchain {
|
||||
pub fn new(ctx: &Context, window: &simv_core::Window, vsync: bool) -> Result<Self> {
|
||||
let mut swapchain = Self {
|
||||
device: ctx.device().clone(),
|
||||
loader: ctx.swapchain_loader().clone(),
|
||||
handle: vk::SwapchainKHR::null(),
|
||||
frames: Vec::new(),
|
||||
color_format: vk::Format::UNDEFINED,
|
||||
extent: vk::Extent2D::default(),
|
||||
};
|
||||
swapchain.recreate(ctx, window, vsync)?;
|
||||
Ok(swapchain)
|
||||
}
|
||||
|
||||
pub fn handle(&self) -> vk::SwapchainKHR {
|
||||
self.handle
|
||||
}
|
||||
pub fn frames(&self) -> &[SwapchainFrame] {
|
||||
&self.frames
|
||||
}
|
||||
pub fn color_format(&self) -> vk::Format {
|
||||
self.color_format
|
||||
}
|
||||
pub fn extent(&self) -> vk::Extent2D {
|
||||
self.extent
|
||||
}
|
||||
|
||||
/// Пересобрать цепочку под текущий размер окна. Старая уничтожается полностью:
|
||||
/// `oldSwapchain` не используется — так же, как в C++-версии.
|
||||
pub fn recreate(
|
||||
&mut self,
|
||||
ctx: &Context,
|
||||
window: &simv_core::Window,
|
||||
vsync: bool,
|
||||
) -> Result<()> {
|
||||
ctx.wait_idle();
|
||||
self.destroy();
|
||||
|
||||
let surface_loader = ctx.surface_loader();
|
||||
let physical_device = ctx.physical_device();
|
||||
let surface = ctx.surface();
|
||||
|
||||
let caps = unsafe {
|
||||
surface_loader.get_physical_device_surface_capabilities(physical_device, surface)
|
||||
}
|
||||
.ctx("vkGetPhysicalDeviceSurfaceCapabilitiesKHR")?;
|
||||
let formats =
|
||||
unsafe { surface_loader.get_physical_device_surface_formats(physical_device, surface) }
|
||||
.ctx("vkGetPhysicalDeviceSurfaceFormatsKHR")?;
|
||||
let present_modes = unsafe {
|
||||
surface_loader.get_physical_device_surface_present_modes(physical_device, surface)
|
||||
}
|
||||
.ctx("vkGetPhysicalDeviceSurfacePresentModesKHR")?;
|
||||
|
||||
// Желаемый формат тот же, что просит C++-версия; если его нет — первый доступный.
|
||||
let wanted = vk::SurfaceFormatKHR {
|
||||
format: vk::Format::B8G8R8A8_UNORM,
|
||||
color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR,
|
||||
};
|
||||
let surface_format = formats
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|f| f.format == wanted.format && f.color_space == wanted.color_space)
|
||||
.unwrap_or_else(|| formats[0]);
|
||||
|
||||
// FIFO поддерживается всегда; MAILBOX может отсутствовать.
|
||||
let desired_mode = if vsync {
|
||||
vk::PresentModeKHR::FIFO
|
||||
} else {
|
||||
vk::PresentModeKHR::MAILBOX
|
||||
};
|
||||
let present_mode = present_modes
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|&m| m == desired_mode)
|
||||
.unwrap_or(vk::PresentModeKHR::FIFO);
|
||||
|
||||
// `current_extent` со всеми битами в единицах означает «размер задаёт
|
||||
// приложение» — тогда берём размер окна и зажимаем по пределам поверхности.
|
||||
let (width, height) = window.framebuffer_size();
|
||||
let extent = if caps.current_extent.width == u32::MAX {
|
||||
vk::Extent2D {
|
||||
width: width.clamp(caps.min_image_extent.width, caps.max_image_extent.width),
|
||||
height: height.clamp(caps.min_image_extent.height, caps.max_image_extent.height),
|
||||
}
|
||||
} else {
|
||||
caps.current_extent
|
||||
};
|
||||
|
||||
let mut image_count = caps.min_image_count + 1;
|
||||
if caps.max_image_count > 0 && image_count > caps.max_image_count {
|
||||
image_count = caps.max_image_count;
|
||||
}
|
||||
|
||||
let ci = vk::SwapchainCreateInfoKHR::default()
|
||||
.surface(surface)
|
||||
.min_image_count(image_count)
|
||||
.image_format(surface_format.format)
|
||||
.image_color_space(surface_format.color_space)
|
||||
.image_extent(extent)
|
||||
.image_array_layers(1)
|
||||
.image_usage(
|
||||
vk::ImageUsageFlags::COLOR_ATTACHMENT | vk::ImageUsageFlags::TRANSFER_DST,
|
||||
)
|
||||
.image_sharing_mode(vk::SharingMode::EXCLUSIVE)
|
||||
.pre_transform(caps.current_transform)
|
||||
.composite_alpha(vk::CompositeAlphaFlagsKHR::OPAQUE)
|
||||
.present_mode(present_mode)
|
||||
.clipped(true);
|
||||
|
||||
self.handle =
|
||||
unsafe { self.loader.create_swapchain(&ci, None) }.ctx("vkCreateSwapchainKHR")?;
|
||||
self.color_format = surface_format.format;
|
||||
self.extent = extent;
|
||||
|
||||
let images = unsafe { self.loader.get_swapchain_images(self.handle) }
|
||||
.ctx("vkGetSwapchainImagesKHR")?;
|
||||
self.frames.reserve(images.len());
|
||||
for image in images {
|
||||
let view_ci = vk::ImageViewCreateInfo::default()
|
||||
.image(image)
|
||||
.view_type(vk::ImageViewType::TYPE_2D)
|
||||
.format(self.color_format)
|
||||
.subresource_range(vk::ImageSubresourceRange {
|
||||
aspect_mask: vk::ImageAspectFlags::COLOR,
|
||||
base_mip_level: 0,
|
||||
level_count: 1,
|
||||
base_array_layer: 0,
|
||||
layer_count: 1,
|
||||
});
|
||||
let view = unsafe { self.device.create_image_view(&view_ci, None) }
|
||||
.ctx("vkCreateImageView(swapchain)")?;
|
||||
self.frames.push(SwapchainFrame { image, view });
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn destroy(&mut self) {
|
||||
unsafe {
|
||||
for frame in self.frames.drain(..) {
|
||||
self.device.destroy_image_view(frame.view, None);
|
||||
}
|
||||
if self.handle != vk::SwapchainKHR::null() {
|
||||
self.loader.destroy_swapchain(self.handle, None);
|
||||
self.handle = vk::SwapchainKHR::null();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Swapchain {
|
||||
fn drop(&mut self) {
|
||||
self.destroy();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user