
Данный цикл статей будет посвящен выводу цветного треугольника в Vulkan c использованием библиотеки ash. Я надеюсь, что читатель имел дело с какими либо графическими api, потому что Vulkan — это сложно и очень много кода, но так же и возможность использования крутых фич от GPU
В данной статье будет раскрыто создание vulkan device
-
Проверка версий Vulkan
-
Создание AppInfo<‘_>
-
Проверка поддерживаемых слоев
-
Проверка поддерживаемых расширений
-
Создание Instance
-
Просмотр доступных устройств и выбор устройства
-
Получение свойств выбранного устройства
-
Получение свойств семейств очередей
-
Получение поддерживаемых слоев и расширений
-
Создание логического устройства
Для начала создадим пустой проект на rust
cargo new --bin CryEngine
Добавим зависимость
cargo add ash
use ash::Entry; use ash::vk::*; fn main() { /* Загружаем функции Vulkan, мы можем подгрузить их динамически с помощью load или статически с помощью linked функции, но для этого потребуется добавить features для ash */ let entry = unsafe { Entry::load().unwrap() }; // Получаем последнюю доступную версию VK_API_VERSION let version = unsafe { entry.try_enumerate_instance_version() .expect("Error enumerate instance version") }; let api_version = match version { // Тут уже будет поновее версия 1.1+ Some(version) => { version }, // Если доступна только первая версия None => { API_VERSION_1_0 } }; }
Дальше нам нужно заполнить информацию о нашем приложении, заполнив структуру ApplicationInfo<‘_>, она понадобится для создания Vulkan Instance
use ash::Entry; use ash::vk::*; fn main() { let entry = unsafe { Entry::load().unwrap() }; // Получаем максимально доступную версию let version = unsafe { entry.try_enumerate_instance_version() .expect("Error enumerate instance version") }; // Выбор версии let api_version = match version { Some(version) => { version }, None => { API_VERSION_1_0 } }; /* Самая важная строчка здесь - это версия vulkan, которую мы будем использовать, остальное можете на свое усмотрение заполнить. Буква "с" перед строкой автоматически конвертирует её в CStr строку */ let app_info = ApplicationInfo::default() .application_name(c"Far Cry 9") .engine_name(c"Cry Engine") .engine_version(12) .application_version(0) .api_version(api_version); }
Layers and Extensions
В Vulkan для отлова ошибок используются слои(Layers). Layers — это опциональные компоненты Vulkan API, которые перехватывают вызовы от приложения до драйверов, они могут проверять правильно ли используется Vulkan. Подробнее можно почитать в спецификации: https://docs.vulkan.org/guide/latest/layers.html
use ash::Entry; use ash::vk::*; fn main() { let entry = unsafe { Entry::load().unwrap() }; let version = unsafe { entry.try_enumerate_instance_version().expect("Error enumerate instance version")}; let api_version = match version { Some(version) => { version }, None => { API_VERSION_1_0 } }; let app_info = ApplicationInfo::default() .application_name(c"Far Cry 9") .engine_name(c"Cry Engine") .engine_version(12) .application_version(0) .api_version(api_version); let layers = unsafe { entry.enumerate_instance_layer_properties().expect("Error enumerate layers") }; let extensions = unsafe { entry.enumerate_instance_extension_properties(None).expect("Error enumerate instance properties") }; println!("-----------Layers--------------"); for i in &layers { println!("{:?}", i.layer_name_as_c_str().unwrap_or(&c"None")); } }
У меня после вызова данного кода будут доступны такие слои:
"VK_LAYER_AMD_switchable_graphics" "VK_LAYER_VALVE_steam_overlay" "VK_LAYER_VALVE_steam_fossilize" "VK_LAYER_OBS_HOOK" "VK_LAYER_RENDERDOC_Capture" "VK_LAYER_LUNARG_api_dump" "VK_LAYER_LUNARG_gfxreconstruct" "VK_LAYER_KHRONOS_synchronization2" "VK_LAYER_KHRONOS_validation" "VK_LAYER_LUNARG_monitor" "VK_LAYER_LUNARG_screenshot" "VK_LAYER_KHRONOS_profiles" "VK_LAYER_KHRONOS_shader_object" "VK_LAYER_LUNARG_crash_diagnostic"
Нам потребуется поддержка лишь одного слоя — VK_LAYER_KHRONOS_validation, именно она будет отслеживать неправильное использование Vulkan
Так же нам потребуются extensions(Расширения), они добавляют новые возможности в Vulkan
use ash::Entry; use ash::vk::*; fn main() { let entry = unsafe { Entry::load().unwrap() }; let version = unsafe { entry.try_enumerate_instance_version().expect("Error enumerate instance version")}; let api_version = match version { Some(version) => { version }, None => { API_VERSION_1_0 } }; let app_info = ApplicationInfo::default() .application_name(c"Far Cry 9") .engine_name(c"Cry Engine") .engine_version(12) .application_version(0) .api_version(api_version); let layers = unsafe { entry.enumerate_instance_layer_properties().expect("Error enumerate layers") }; let extensions = unsafe { /// Мы передаем None, чтобы узнать о глобальных расширениях, так же можно узнать /// об расширений конкретного слоя, но нам это не нужно entry.enumerate_instance_extension_properties(None).expect("Error enumerate instance properties") }; println!("-----------Layers--------------"); for i in &layers { println!("{:?}", i.layer_name_as_c_str().unwrap_or(&c"None")); } println!("------------Extensions------------"); for i in &extensions { println!("{:?}", i.extension_name_as_c_str().unwrap_or(&c"None")); } println!("---------------------------------"); }
Я использую Window OS, поэтому буду видеть расширения связанные с этой операционной системой
"VK_KHR_device_group_creation" "VK_KHR_external_fence_capabilities" "VK_KHR_external_memory_capabilities" "VK_KHR_external_semaphore_capabilities" "VK_KHR_get_physical_device_properties2" "VK_KHR_get_surface_capabilities2" "VK_KHR_surface" "VK_KHR_win32_surface" "VK_EXT_debug_report" "VK_EXT_debug_utils" "VK_EXT_swapchain_colorspace" "VK_KHR_portability_enumeration" "VK_LUNARG_direct_driver_loading"
Из всех этих расширений я выделю только 4 нужных расширения:
-
«VK_KHR_surface» — Позволит нам выводить изображение на экран. Vulkan может рендерить в свой FrameBuffre не задействую само окно ос
-
«VK_KHR_win32_surface» — Расширение для поддержки поверхности Windows
-
«VK_EXT_debug_report» — Расширение для отлова ошибок
-
«VK_EXT_debug_utils» — Еще одно расширение для отлова ошибок
Теперь заполним InstanceCreateInfo<‘_> нужными нам расширениями и слоями
use ash::Entry; use ash::vk::*; fn main() { let entry = unsafe { Entry::load().unwrap() }; let version = unsafe { entry.try_enumerate_instance_version().expect("Error enumerate instance version") }; let api_version = match version { Some(version) => { version }, None => { API_VERSION_1_0 } }; let app_info = ApplicationInfo::default() .application_name(c"Far Cry 9") .engine_name(c"Cry Engine") .engine_version(12) .application_version(0) .api_version(api_version); let layers = unsafe { entry.enumerate_instance_layer_properties().expect("Error enumerate layers") }; let extensions = unsafe { entry.enumerate_instance_extension_properties(None).expect("Error enumerate instance properties") }; println!("------------Layers---------------"); for i in &layers { println!("{:?}", i.layer_name_as_c_str().unwrap_or(&c"None")); } println!("------------Extensions------------"); for i in &extensions { println!("{:?}", i.extension_name_as_c_str().unwrap_or(&c"None")); } println!("---------------------------------"); /// Берем только нужные расширения! let required_extensions = [ CStr::from_bytes_with_nul(b"VK_KHR_surface\0").unwrap().as_ptr(), CStr::from_bytes_with_nul(b"VK_KHR_win32_surface\0").unwrap().as_ptr(), CStr::from_bytes_with_nul(b"VK_EXT_debug_report\0").unwrap().as_ptr(), CStr::from_bytes_with_nul(b"VK_EXT_debug_utils\0").unwrap().as_ptr(), ]; /// Только необходимые слои! let required_layers = [ CStr::from_bytes_with_nul(b"VK_LAYER_KHRONOS_validation\0").unwrap().as_ptr(), ]; let instance_info = InstanceCreateInfo::default() .enabled_extension_names(&required_extensions) .enabled_layer_names(&required_layers) .application_info(&app_info); let instance = unsafe { // Вместо None можем передать свою callback функцию entry.create_instance(&instance_info, None) .map_err(|e| format!("Error create insatnce with errror: {}", e)) .unwrap() }; }
Device(Логическое устройство)
Device — это абстракция над физическим устройством. С помощью device уже можно будет создавать буферы, изображения, текстуры, а так же отправлять команды на GPU и ждать синхронизации. Логическое устройство создается похожим образом на Instance, так же нужно запросить слои и расширения для него, но будет чуть больше того, что мы должны передать для его создания.
Сначала просмотрим, какие вообще устройства у нас есть и выберем дискретную(встроенную) GPU
use ash::Entry; use ash::vk::*; fn main() { let entry = unsafe { Entry::load().unwrap() }; let version = unsafe { entry.try_enumerate_instance_version().expect("Error enumerate instance version") }; let api_version = match version { Some(version) => { version }, None => { API_VERSION_1_0 } }; let app_info = ApplicationInfo::default() .application_name(c"Far Cry 9") .engine_name(c"Cry Engine") .engine_version(12) .application_version(0) .api_version(api_version); let layers = unsafe { entry.enumerate_instance_layer_properties().expect("Error enumerate layers") }; let extensions = unsafe { entry.enumerate_instance_extension_properties(None).expect("Error enumerate instance properties") }; println!("-----------Layers--------------"); for i in &layers { println!("{:?}", i.layer_name_as_c_str().unwrap_or(&c"None")); } println!("------------Extensions------------"); for i in &extensions { println!("{:?}", i.extension_name_as_c_str().unwrap_or(&c"None")); } println!("---------------------------------"); let extension_names = extensions .iter() .map(|x| x.extension_name.as_ptr()) .collect::<Vec<*const i8>>(); let layer_names = layers .iter() .map(|x| x.layer_name.as_ptr()) .collect::<Vec<*const i8>>(); let instance_info = InstanceCreateInfo::default() .enabled_extension_names(&extension_names) .enabled_layer_names(&layer_names) .application_info(&app_info); let instance = unsafe { entry.create_instance(&instance_info, None) .map_err(|e| format!("Error create insatnce with errror: {}", e)) .unwrap() }; // Получаем все доступные устройства let phys_devs = unsafe { instance.enumerate_physical_devices().expect("Error enumerate Physical Devices")}; let mut phsy_dev_index = 0; println!("-------------Available GPU-------------------"); for (index, i) in phys_devs.iter().enumerate() { // Запрашиваем свойства физического устройства let prop = unsafe { instance.get_physical_device_properties(*i) }; // Выводим общую информацию println!("DEVICE_NAME: {:?}", prop.device_name_as_c_str().unwrap_or(&c"Undefined")); println!("VULKAN_API_VERSION: {:?}", prop.api_version); println!("DEVICE_TYPE: {:?}", prop.device_type); println!("DRIVER VERSION: {:?}", prop.driver_version); if prop.device_type == PhysicalDeviceType::INTEGRATED_GPU { phsy_dev_index = index; break; } } }
QueueFamily
Перед тем, как собрать полную информацию о нашем GPU, я бы хотел рассказать о такой вещи, как Queue Familes(Семейства очередей). У GPU есть так называемые Queue(Очереди) у которых есть свойства. Одни могут вычислять, другие передавать данные с CPU на GPU, другие могут непосредственно работать с графикой. Семейства очередей — это очереди с одними и теми же свойствами. Перед созданием логического устройства нужно будет указать какие семейства очередей будет использовать логическое устройство в будущем.
use std::ffi::CStr; use ash::Entry; use ash::vk::*; fn main() { let entry = unsafe { Entry::load().unwrap() }; let version = unsafe { entry.try_enumerate_instance_version().expect("Error enumerate instance version") }; let api_version = match version { Some(version) => { version }, None => { API_VERSION_1_0 } }; let app_info = ApplicationInfo::default() .application_name(c"Far Cry 9") .engine_name(c"Cry Engine") .engine_version(12) .application_version(0) .api_version(api_version); let layers = unsafe { entry.enumerate_instance_layer_properties().expect("Error enumerate layers") }; let extensions = unsafe { entry.enumerate_instance_extension_properties(None).expect("Error enumerate instance properties") }; println!("-----------Layers--------------"); for i in &layers { println!("{:?}", i.layer_name_as_c_str().unwrap_or(&c"None")); } println!("------------Extensions------------"); for i in &extensions { println!("{:?}", i.extension_name_as_c_str().unwrap_or(&c"None")); } println!("---------------------------------"); let required_extensions = [ CStr::from_bytes_with_nul(b"VK_KHR_surface\0").unwrap().as_ptr(), CStr::from_bytes_with_nul(b"VK_KHR_win32_surface\0").unwrap().as_ptr(), CStr::from_bytes_with_nul(b"VK_EXT_debug_report\0").unwrap().as_ptr(), CStr::from_bytes_with_nul(b"VK_EXT_debug_utils\0").unwrap().as_ptr(), ]; let required_layers = [ CStr::from_bytes_with_nul(b"VK_LAYER_KHRONOS_validation\0").unwrap().as_ptr(), ]; let instance_info = InstanceCreateInfo::default() .enabled_extension_names(&required_extensions) .enabled_layer_names(&required_layers) .application_info(&app_info); let instance = unsafe { entry.create_instance(&instance_info, None) .map_err(|e| format!("Error create insatnce with errror: {}", e)) .unwrap() }; let phys_devs = unsafe { instance.enumerate_physical_devices().expect("Error enumerate Physical Devices") }; let mut phys_dev_index = 0; println!("-------------Avaliable GPU-------------------"); for (index, i) in phys_devs.iter().enumerate() { let prop = unsafe { instance.get_physical_device_properties(*i) }; println!("DEVICE_NAME: {:?}", prop.device_name_as_c_str().unwrap_or(&c"Undefined")); println!("VULKAN_API_VERSION: {:?}", prop.api_version); println!("DEVICE_TYPE: {:?}", prop.device_type); println!("DRIVER VERSION: {:?}", prop.driver_version); if prop.device_type == PhysicalDeviceType::INTEGRATED_GPU { phys_dev_index = index; break; } } let phys_dev = phys_devs[phys_dev_index]; /// Информацию о памяти let memory_prop = unsafe { instance.get_physical_device_memory_properties(phys_dev) }; /// Информация о семействах очередей let queue_family_prop = unsafe { instance.get_physical_device_queue_family_properties(phys_dev) }; /// Общая информация о GPU let phys_prop = unsafe { instance.get_physical_device_properties(phys_dev) }; // Сохраняем информацию в свою структуру struct QueueFamilyInfo { queue_family_index: usize, queue_prop: QueueFamilyProperties } let mut queue_infos = vec![]; for (index, i) in queue_family_prop.iter().enumerate() { println!("Queue Family: {}: Queue Count: {:?}, Flags: {:?}", index, i.queue_count, i.queue_flags); queue_infos.push(QueueFamilyInfo { queue_family_index: index, queue_prop: *i }); } }
У меня вывод будет такой:
Queue Family: 0: Queue Count: 1, Flags: GRAPHICS | COMPUTE | TRANSFER | SPARSE_BINDING Queue Family: 1: Queue Count: 2, Flags: COMPUTE | TRANSFER | SPARSE_BINDING Queue Family: 2: Queue Count: 1, Flags: TRANSFER | SPARSE_BINDING
Может быть как одно семейство поддерживающее вообще все операции, так и семейства заточенные под 1-2 операции, они считаются более быстрыми, чем общие.
Priority
Так же помимо информации о семействах нужно передать приоритет каждой очереди в семействе, но можно просто упростить и сделать все очереди равными. Приоритеты нужны, чтобы драйвер мог понять, какие операции предпочтительнее выполнить первыми, а какие можно позже.
use std::ffi::CStr; use ash::Entry; use ash::vk::*; fn main() { let entry = unsafe { Entry::load().unwrap() }; let version = unsafe { entry.try_enumerate_instance_version().expect("Error enumerate instance version") }; let api_version = match version { Some(version) => { version }, None => { API_VERSION_1_0 } }; let app_info = ApplicationInfo::default() .application_name(c"Far Cry 9") .engine_name(c"Cry Engine") .engine_version(12) .application_version(0) .api_version(api_version); let layers = unsafe { entry.enumerate_instance_layer_properties().expect("Error enumerate layers") }; let extensions = unsafe { entry.enumerate_instance_extension_properties(None).expect("Error enumerate instance properties") }; println!("-----------Layers--------------"); for i in &layers { println!("{:?}", i.layer_name_as_c_str().unwrap_or(&c"None")); } println!("------------Extensions------------"); for i in &extensions { println!("{:?}", i.extension_name_as_c_str().unwrap_or(&c"None")); } println!("---------------------------------"); let required_extensions = [ CStr::from_bytes_with_nul(b"VK_KHR_surface\0").unwrap().as_ptr(), CStr::from_bytes_with_nul(b"VK_KHR_win32_surface\0").unwrap().as_ptr(), CStr::from_bytes_with_nul(b"VK_EXT_debug_report\0").unwrap().as_ptr(), CStr::from_bytes_with_nul(b"VK_EXT_debug_utils\0").unwrap().as_ptr(), ]; let required_layers = [ CStr::from_bytes_with_nul(b"VK_LAYER_KHRONOS_validation\0").unwrap().as_ptr(), ]; let instance_info = InstanceCreateInfo::default() .enabled_extension_names(&required_extensions) .enabled_layer_names(&required_layers) .application_info(&app_info); let instance = unsafe { entry.create_instance(&instance_info, None) .map_err(|e| format!("Error create insatnce with errror: {}", e)) .unwrap() }; let phys_devs = unsafe { instance.enumerate_physical_devices().expect("Error enumerate Physical Devices") }; let mut phys_dev_index = 0; println!("-------------Avaliable GPU-------------------"); for (index, i) in phys_devs.iter().enumerate() { let prop = unsafe { instance.get_physical_device_properties(*i) }; println!("DEVICE_NAME: {:?}", prop.device_name_as_c_str().unwrap_or(&c"Undefined")); println!("VULKAN_API_VERSION: {:?}", prop.api_version); println!("DEVICE_TYPE: {:?}", prop.device_type); println!("DRIVER VERSION: {:?}", prop.driver_version); if prop.device_type == PhysicalDeviceType::INTEGRATED_GPU { phys_dev_index = index; break; } } let phys_dev = phys_devs[phys_dev_index]; let memory_prop = unsafe { instance.get_physical_device_memory_properties(phys_dev) }; let queue_family_prop = unsafe { instance.get_physical_device_queue_family_properties(phys_dev) }; let phys_prop = unsafe { instance.get_physical_device_properties(phys_dev) }; struct QueueFamilyInfo { queue_family_index: usize, queue_prop: QueueFamilyProperties } let mut queue_infos = vec![]; for (index, i) in queue_family_prop.iter().enumerate() { println!("Queue Family: {}: Queue Count: {:?}, Flags: {:?}", index, i.queue_count, i.queue_flags); queue_infos.push(QueueFamilyInfo { queue_family_index: index, queue_prop: *i }); } // Устанавливаем приоритет одинаковым для всех очередей в семействе let priority = [1.0f32]; let mut queue_family_infos = vec![]; for i in queue_infos { let device_queue_info = DeviceQueueCreateInfo::default() .queue_family_index(i.queue_family_index as u32) .queue_priorities(&priority); queue_family_infos.push(device_queue_info) } }
Device Extensions and Layers
У устройства нам понадобится лишь одно расширение — это поддержка Swapchain(Цепочка обмена)
use std::ffi::CStr; use ash::Entry; use ash::vk::*; fn main() { let entry = unsafe { Entry::load().unwrap() }; let version = unsafe { entry.try_enumerate_instance_version().expect("Error enumerate instance version") }; let api_version = match version { Some(version) => { version }, None => { API_VERSION_1_0 } }; let app_info = ApplicationInfo::default() .application_name(c"Far Cry 9") .engine_name(c"Cry Engine") .engine_version(12) .application_version(0) .api_version(api_version); let layers = unsafe { entry.enumerate_instance_layer_properties().expect("Error enumerate layers") }; let extensions = unsafe { entry.enumerate_instance_extension_properties(None).expect("Error enumerate instance properties") }; println!("-----------Layers--------------"); for i in &layers { println!("{:?}", i.layer_name_as_c_str().unwrap_or(&c"None")); } println!("------------Extensions------------"); for i in &extensions { println!("{:?}", i.extension_name_as_c_str().unwrap_or(&c"None")); } println!("---------------------------------"); let required_extensions = [ CStr::from_bytes_with_nul(b"VK_KHR_surface\0").unwrap().as_ptr(), CStr::from_bytes_with_nul(b"VK_KHR_win32_surface\0").unwrap().as_ptr(), CStr::from_bytes_with_nul(b"VK_EXT_debug_report\0").unwrap().as_ptr(), CStr::from_bytes_with_nul(b"VK_EXT_debug_utils\0").unwrap().as_ptr(), ]; let required_layers = [ CStr::from_bytes_with_nul(b"VK_LAYER_KHRONOS_validation\0").unwrap().as_ptr(), ]; let instance_info = InstanceCreateInfo::default() .enabled_extension_names(&required_extensions) .enabled_layer_names(&required_layers) .application_info(&app_info); let instance = unsafe { entry.create_instance(&instance_info, None) .map_err(|e| format!("Error create insatnce with errror: {}", e)) .unwrap() }; let phys_devs = unsafe { instance.enumerate_physical_devices().expect("Error enumerate Physical Devices") }; let mut phys_dev_index = 0; println!("-------------Avaliable GPU-------------------"); for (index, i) in phys_devs.iter().enumerate() { let prop = unsafe { instance.get_physical_device_properties(*i) }; println!("DEVICE_NAME: {:?}", prop.device_name_as_c_str().unwrap_or(&c"Undefined")); println!("VULKAN_API_VERSION: {:?}", prop.api_version); println!("DEVICE_TYPE: {:?}", prop.device_type); println!("DRIVER VERSION: {:?}", prop.driver_version); if prop.device_type == PhysicalDeviceType::INTEGRATED_GPU { phys_dev_index = index; break; } } let phys_dev = phys_devs[phys_dev_index]; let memory_prop = unsafe { instance.get_physical_device_memory_properties(phys_dev) }; let queue_family_prop = unsafe { instance.get_physical_device_queue_family_properties(phys_dev) }; let phys_prop = unsafe { instance.get_physical_device_properties(phys_dev) }; struct QueueFamilyInfo { queue_family_index: usize, queue_prop: QueueFamilyProperties } let mut queue_infos = vec![]; for (index, i) in queue_family_prop.iter().enumerate() { println!("Queue Family: {}: Queue Count: {:?}, Flags: {:?}", index, i.queue_count, i.queue_flags); queue_infos.push(QueueFamilyInfo { queue_family_index: index, queue_prop: *i }); } let priority = [1.0f32]; let mut queue_family_infos = vec![]; for i in queue_infos { let device_queue_info = DeviceQueueCreateInfo::default() .queue_family_index(i.queue_family_index as u32) .queue_priorities(&priority); queue_family_infos.push(device_queue_info) } let extensions = unsafe { instance.enumerate_device_extension_properties(phys_dev).expect("Error enumerate device extensions") }; println!("------------Device Extensions---------------------"); for i in &extensions { println!("{:?}", i.extension_name_as_c_str().unwrap_or(&c"None")); } // Единственное расширение которое нам необходимо! let required_extesions = [ CStr::from_bytes_with_nul(b"VK_KHR_swapchain\0").unwrap().as_ptr() ]; let layers = unsafe { instance.enumerate_device_layer_properties(phys_dev) .expect("Error enumerate device layers") }; println!("------------Device Layers---------------------"); for i in &layers { println!("{:?}", i.layer_name_as_c_str().unwrap_or(&c"None")); } println!("-------------------------------------------------"); // Пустой features let features = PhysicalDeviceFeatures::default(); // Заполняем старыми значениями let device_info = DeviceCreateInfo::default() .enabled_features(&features) .queue_create_infos(&queue_family_infos) .enabled_extension_names(&required_extesions); // Создаем устройство let device = unsafe { instance.create_device(phys_dev, &device_info, None).expect("Error create device") }; }
На этом всё! Постепенно будут выходить и новые статьи. Надеюсь, тех, кто интересуется графикой, не отпугнул этот объём — да, здесь куда больше действий, чем в старых добрых glBegin/glEnd, но зато можно прочувствовать полный контроль над GPU. Взять её за жабры и сделать ровно то, что ты хочешь.
Полезные ссылки:
-
https://github.com/adrien-ben/vulkan-tutorial-rs — Хороший туториал на Rust
-
https://vulkan-tutorial.com/ — Самый проверенный туториал, но используется язык C++
-
https://github.com/ash-rs/ash/tree/master/ash-examples — официальные примеры от создателей
ash -
https://github.com/EmbarkStudios/kajiya — Графический движок, написан на Rust бывшим программистов Frostbite
-
https://github.com/olejaaaaaaaa/VulkanExamples — Ссылка на полный код туториал
ссылка на оригинал статьи https://habr.com/ru/articles/942880/
Добавить комментарий