new architecture

This commit is contained in:
zack 2025-03-26 20:02:58 -04:00
parent 9f7e72b784
commit 9cfd9d8b17
No known key found for this signature in database
GPG key ID: EE8A2B709E2401D1
28 changed files with 2625 additions and 5351 deletions

2345
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,22 +1,24 @@
[workspace]
resolver = "2"
members = ["crates/shaders-shared", "crates/vk-rs"]
members = ["crates/engine", "crates/gfx_hal", "crates/resource_manager"]
[workspace.dependencies]
ash = { version = "0.37.3" }
ash = { version = "0.38" }
ash-window = "0.13.0"
color-eyre = "0.6.3"
winit = { version = "0.30.7", features = ["rwh_06"] }
raw-window-handle = "0.6"
gpu-allocator = { version = "0.25.0", features = ["vulkan"] }
glam = { version = "0.22", default-features = false, features = ["libm"] }
bytemuck = "1.21.0"
gpu-allocator = { version = "0.27.0", features = ["vulkan"] }
glam = { version = "0.22", default-features = false, features = [
"libm",
"bytemuck",
] }
bytemuck = { version = "1.21.0", features = ["derive"] }
tracing = "0.1"
tracing-subscriber = "0.3"
parking_lot = "0.12.3"
spirv-builder = { git = "https://github.com/Rust-GPU/rust-gpu.git" }
spirv-std = { git = "https://github.com/Rust-GPU/rust-gpu.git" }
thiserror = "2.0.12"
# # Enable incremental by default in release mode.

18
crates/engine/Cargo.toml Normal file
View file

@ -0,0 +1,18 @@
[package]
name = "engine"
version = "0.1.0"
edition = "2021"
[dependencies]
ash.workspace = true
winit.workspace = true
raw-window-handle.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
glam.workspace = true
bytemuck.workspace = true
thiserror.workspace = true
gpu-allocator.workspace = true
gfx_hal = { path = "../gfx_hal" }
resource_manager = { path = "../resource_manager" }

View file

@ -0,0 +1,5 @@
mod scene_data;
fn main() {
println!("Hello, world!");
}

View file

@ -0,0 +1,67 @@
use core::f32;
use bytemuck::{Pod, Zeroable};
use glam::{Mat4, Vec3};
#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
pub struct Vertex {
pub pos: [f32; 3],
pub normal: [f32; 3],
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
pub struct UniformBufferObject {
pub model: Mat4,
pub view: Mat4,
pub proj: Mat4,
pub light_dir: Vec3,
pub _padding: f32,
pub light_color: Vec3,
pub _padding2: f32,
}
pub fn create_sphere(radius: f32, sectors: u32, stacks: u32) -> (Vec<Vertex>, Vec<u32>) {
let mut vertices = Vec::new();
let mut indices = Vec::new();
let sector_step = 2.0 * f32::consts::PI / sectors as f32;
let stack_step = f32::consts::PI / stacks as f32;
for i in 0..stacks {
let stack_angle = std::f32::consts::PI / 2.0 - (i as f32 * stack_step);
let xy = radius * stack_angle.cos();
let z = radius * stack_angle.sin();
for j in 0..=sectors {
let sector_angle = j as f32 * sector_step;
let x = xy * sector_angle.cos();
let y = xy * sector_angle.sin();
let pos = [x, y, z];
let normal = Vec3::from(pos).normalize().to_array();
vertices.push(Vertex { pos, normal });
}
}
for i in 0..stacks {
let k1 = i * (sectors + 1);
let k2 = k1 + sectors + 1;
for j in 0..sectors {
if i != 0 {
indices.push(k1 + j);
indices.push(k2 + j);
indices.push(k1 + j + 1);
}
if i != (stacks - 1) {
indices.push(k1 + j + 1);
indices.push(k2 + j);
indices.push(k2 + j + 1);
}
}
}
(vertices, indices)
}

12
crates/gfx_hal/Cargo.toml Normal file
View file

@ -0,0 +1,12 @@
[package]
name = "gfx_hal"
version = "0.1.0"
edition = "2021"
[dependencies]
ash.workspace = true
ash-window.workspace = true
thiserror.workspace = true
tracing.workspace = true
winit.workspace = true
parking_lot.workspace = true

View file

@ -0,0 +1,223 @@
use ash::vk;
use parking_lot::Mutex;
use std::ffi::CStr;
use std::{collections::HashMap, sync::Arc};
use crate::error::{GfxHalError, Result};
use crate::instance::Instance;
use crate::physical_device::{PhysicalDevice, QueueFamilyIndices};
use crate::queue::Queue;
/// Represents the logical Vulkan device, created from a `PhysicalDevice`.
///
/// Owns the `ash::Device` and provides access to device functions and queues.
pub struct Device {
instance: Arc<Instance>,
physical_device: vk::PhysicalDevice,
device: ash::Device,
queues: Mutex<HashMap<(u32, u32), Arc<Queue>>>,
graphics_queue_family_index: u32,
compute_queue_family_index: Option<u32>,
transfer_queue_family_index: Option<u32>,
}
impl Device {
/// Creates a new logical device. Typically called via `PhysicalDevice::create_logical_device`.
///
/// # Saftey
/// - `instance` and `physical_device_handle` must be valid.
/// - `queue_family_indicies` must be valid indicies obtained from the `physical_device_handle`.
/// - `required_extensions` must be supported by the `physical_device_handle`.
/// - `enabled_features` and `mesh_features` must be supported by the `physical_device_handle`.
pub(crate) unsafe fn new(
instance: Arc<Instance>,
physical_device_handle: vk::PhysicalDevice,
queue_family_indicies: &QueueFamilyIndices,
required_extensions: &[&CStr],
enabled_features: &vk::PhysicalDeviceFeatures,
mesh_features: Option<&vk::PhysicalDeviceMeshShaderFeaturesEXT>,
) -> Result<Arc<Self>> {
let mut queue_create_infos = Vec::new();
let mut unique_queue_families = std::collections::HashSet::new();
let graphics_family = queue_family_indicies.graphics_family.ok_or_else(|| {
GfxHalError::MissingQueueFamily("Graphics Queue Family Missing".to_string())
})?;
unique_queue_families.insert(graphics_family);
if let Some(compute_family) = queue_family_indicies.compute_family {
unique_queue_families.insert(compute_family);
}
if let Some(transfer_family) = queue_family_indicies.transfer_family {
unique_queue_families.insert(transfer_family);
}
let queue_priorities = [1.0f32];
for &family_index in &unique_queue_families {
let queue_create_info = vk::DeviceQueueCreateInfo::default()
.queue_family_index(family_index)
.queue_priorities(&queue_priorities);
queue_create_infos.push(queue_create_info);
}
let extension_names_raw: Vec<*const i8> =
required_extensions.iter().map(|s| s.as_ptr()).collect();
let mut features2 = vk::PhysicalDeviceFeatures2::default().features(*enabled_features);
let mut mesh_features_copy;
if let Some(mesh_feats) = mesh_features {
mesh_features_copy = *mesh_feats;
features2 = features2.push_next(&mut mesh_features_copy);
}
let device_create_info = vk::DeviceCreateInfo::default()
.queue_create_infos(&queue_create_infos)
.enabled_extension_names(&extension_names_raw)
.push_next(&mut features2);
tracing::info!(
"Creating logical device with extensions: {:?}",
required_extensions
);
let device = instance.ash_instance().create_device(
physical_device_handle,
&device_create_info,
None,
)?;
tracing::info!("logical device created successfully.");
let mut queues_map = HashMap::new();
let arc_device_placeholder = Arc::new(Self {
instance,
physical_device: physical_device_handle,
device,
queues: Mutex::new(HashMap::new()),
graphics_queue_family_index: graphics_family,
compute_queue_family_index: queue_family_indicies.compute_family,
transfer_queue_family_index: queue_family_indicies.transfer_family,
});
for &family_index in &unique_queue_families {
let queue_handler = arc_device_placeholder
.device
.get_device_queue(family_index, 0);
let queue_wrapper = Arc::new(Queue::new(
Arc::clone(&arc_device_placeholder),
queue_handler,
family_index,
));
queues_map.insert((family_index, 0), queue_wrapper);
}
let device_handle = unsafe {
arc_device_placeholder
.instance
.ash_instance()
.create_device(physical_device_handle, &device_create_info, None)?
};
let final_device = Arc::new(Self {
instance: Arc::clone(&arc_device_placeholder.instance), // Clone from placeholder
physical_device: physical_device_handle,
device: device_handle, // Use the newly created handle
queues: Mutex::new(queues_map), // Use the populated map
graphics_queue_family_index: graphics_family,
compute_queue_family_index: queue_family_indicies.compute_family,
transfer_queue_family_index: queue_family_indicies.transfer_family,
});
Ok(final_device)
}
/// Provides raw access to the underlying `ash::Device`.
/// Use with caution, prefer safe wrappers where possible.
pub fn raw(&self) -> &ash::Device {
&self.device
}
/// Gets the handle of the physical device this logical device was created from.
pub fn physical_device_handle(&self) -> vk::PhysicalDevice {
self.physical_device
}
/// Gets the primary graphics queue family index used by this device.
pub fn graphics_queue_family_index(&self) -> u32 {
self.graphics_queue_family_index
}
/// Gets the compute queue family index, if a distinct one was found/used.
pub fn compute_queue_family_index(&self) -> Option<u32> {
self.compute_queue_family_index
}
/// Gets the transfer queue family index, if a distinct one was found/used.
pub fn transfer_queue_family_index(&self) -> Option<u32> {
self.transfer_queue_family_index
}
/// Gets a wrapped queue handle.
/// Currently only supports queue index 0 for each family.
pub fn get_queue(&self, family_index: u32, queue_index: u32) -> Option<Arc<Queue>> {
if queue_index != 0 {
tracing::warn!("get_queue currently only supports queue_index 0");
return None;
}
self.queues
.lock()
.get(&(family_index, queue_index))
.cloned()
}
/// Gets the primary graphics queue (family index from `graphics_queue_family_index`, queue index 0).
/// Panics if the graphics queue wasn't successfully created.
pub fn get_graphics_queue(&self) -> Arc<Queue> {
self.get_queue(self.graphics_queue_family_index, 0)
.expect("Graphics queue should always exist")
}
/// Waits until the logical device becomes idle.
/// This is a heavy operation and should be used sparingly (e.g., before destruction).
pub fn wait_idle(&self) -> Result<()> {
tracing::debug!("Waiting for device idle...");
unsafe { self.device.device_wait_idle()? };
tracing::debug!("Device idle.");
Ok(())
}
}
impl Drop for Device {
fn drop(&mut self) {
tracing::debug!("Destroying logical device...");
if let Err(e) = self.wait_idle() {
tracing::error!("Error waiting for device idle during drop: {}", e);
}
unsafe {
self.device.destroy_device(None);
}
tracing::debug!("Logical device destroyed.");
}
}
impl PhysicalDevice {
/// Creates the logical device (`Device`) from this physical device.
///
/// # Safety
/// See `Device::new` safety comments.
pub unsafe fn create_logical_device(
&self,
required_extensions: &[&CStr],
queue_family_indices: &QueueFamilyIndices,
enabled_features: &vk::PhysicalDeviceFeatures,
mesh_features: Option<&vk::PhysicalDeviceMeshShaderFeaturesEXT>,
) -> Result<Arc<Device>> {
Device::new(
Arc::clone(self.instance()),
self.handle(),
queue_family_indices,
required_extensions,
enabled_features,
mesh_features,
)
}
}

View file

@ -0,0 +1,64 @@
use ash::vk;
use thiserror::Error;
/// Top-level error type for the gfx_hal crate.
#[derive(Error, Debug)]
pub enum GfxHalError {
/// Error originating directly from a Vulkan API call.
#[error("Vulkan API Error: {0}")]
VulkanError(#[from] vk::Result),
/// Error loading the Vulkan library or resolving function pointers.
#[error("Failed to load Vulkan library: {0}")]
LoadError(String),
/// No suitable physical device (GPU) could be found that meets requirements.
#[error("No suitable physical device found: {0}")]
NoSuitableGpu(String),
/// A required Vulkan instance or device extension is not supported.
#[error("Missing required Vulkan extension: {0:?}")]
MissingExtension(String),
/// A required Vulkan feature is not supported by the physical device.
#[error("Missing required Vulkan feature.")]
MissingFeature,
/// Failed to find a suitable queue family (e.g., graphics, present).
#[error("Could not find required queue family: {0}")]
MissingQueueFamily(String),
/// Error related to window system integration surface creation.
#[error("Failed to create Vulkan surface: {0}")]
SurfaceCreationError(vk::Result),
/// The Vulkan surface became invalid (e.g., window resized, closed).
#[error("Vulkan surface is no longer valid (maybe lost or out of date)")]
SurfaceLost,
/// Error converting a C-style string.
#[error("Invalid C string: {0}")]
InvalidCString(#[from] std::ffi::NulError),
/// Error converting C string slice to Rust string slice.
#[error("Invalid UTF-8 sequence in C string: {0}")]
InvalidCStringUtf8(#[from] std::ffi::FromBytesWithNulError),
/// Generic I/O error.
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
/// Error with winit windowing library.
#[error("Winit Error: {0}")]
WinitHandleError(#[from] winit::raw_window_handle::HandleError),
/// Ash loader error.
#[error("Error loading the ash entry.")]
AshEntryError(#[from] ash::LoadingError),
/// Placeholder for other specific errors.
#[error("An unexpected error occurred: {0}")]
Other(String),
}
pub type Result<T, E = GfxHalError> = std::result::Result<T, E>;

View file

@ -0,0 +1,353 @@
use std::{
collections::HashSet,
ffi::{c_char, c_void, CStr, CString},
sync::Arc,
};
use ash::{ext::debug_utils, vk};
use winit::raw_window_handle::{DisplayHandle, HasDisplayHandle};
use crate::error::{GfxHalError, Result};
unsafe extern "system" fn vulkan_debug_callback(
message_severity: vk::DebugUtilsMessageSeverityFlagsEXT,
message_type: vk::DebugUtilsMessageTypeFlagsEXT,
p_callback_data: *const vk::DebugUtilsMessengerCallbackDataEXT,
_p_user_data: *mut c_void,
) -> vk::Bool32 {
let callback_data = *p_callback_data;
let message_id_number: i32 = callback_data.message_id_number;
let message_id_name = if callback_data.p_message_id_name.is_null() {
std::borrow::Cow::from("")
} else {
CStr::from_ptr(callback_data.p_message_id_name).to_string_lossy()
};
let message = if callback_data.p_message.is_null() {
std::borrow::Cow::from("")
} else {
CStr::from_ptr(callback_data.p_message).to_string_lossy()
};
let severity = match message_severity {
vk::DebugUtilsMessageSeverityFlagsEXT::VERBOSE => "[VERBOSE]",
vk::DebugUtilsMessageSeverityFlagsEXT::INFO => "[INFO]",
vk::DebugUtilsMessageSeverityFlagsEXT::WARNING => "[WARNING]",
vk::DebugUtilsMessageSeverityFlagsEXT::ERROR => "[ERROR]",
_ => "[UNKNOWN SEVERITY]",
};
let ty = match message_type {
vk::DebugUtilsMessageTypeFlagsEXT::GENERAL => "[GENERAL]",
vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION => "[VALIDATION]",
vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE => "[PERFORMANCE]",
_ => "[UNKNOWN TYPE]",
};
// Use the tracing crate for output
match message_severity {
vk::DebugUtilsMessageSeverityFlagsEXT::VERBOSE => {
tracing::debug!(
"{} {} ({}:{}) {}",
severity,
ty,
message_id_name,
message_id_number,
message
);
}
vk::DebugUtilsMessageSeverityFlagsEXT::INFO => {
tracing::info!(
"{} {} ({}:{}) {}",
severity,
ty,
message_id_name,
message_id_number,
message
);
}
vk::DebugUtilsMessageSeverityFlagsEXT::WARNING => {
tracing::warn!(
"{} {} ({}:{}) {}",
severity,
ty,
message_id_name,
message_id_number,
message
);
}
// Treat ERROR and higher as errors
_ => {
tracing::error!(
"{} {} ({}:{}) {}",
severity,
ty,
message_id_name,
message_id_number,
message
);
}
}
vk::FALSE // Standard return value
}
#[derive(Clone, Debug)]
pub struct InstanceConfig {
pub application_name: String,
pub engine_name: String,
pub application_version: u32,
pub engine_version: u32,
/// Enable Vulkan validation layers
pub enable_validation: bool,
/// Additional required instance extensions beyond surface/debug.
pub required_extensions: Vec<&'static CStr>,
}
impl Default for InstanceConfig {
fn default() -> Self {
InstanceConfig {
application_name: "Defualt App".to_string(),
engine_name: "Default Engine".to_string(),
application_version: vk::make_api_version(0, 1, 0, 0),
engine_version: vk::make_api_version(0, 1, 0, 0),
enable_validation: cfg!(debug_assertions),
required_extensions: Vec::new(),
}
}
}
/// Represents the Vulkan API Instance
///
/// Owns the `ash::Entry`, `ash::Instance` and potentially the debug messenger.
/// This is the starting point for interacting with Vulkan
pub struct Instance {
entry: ash::Entry,
instance: ash::Instance,
debug_utils: Option<ash::ext::debug_utils::Instance>,
debug_messenger: Option<vk::DebugUtilsMessengerEXT>,
}
impl Instance {
/// Creates a new Vulkan `Instance`
///
/// # Arguments
/// * `config` - Configuration settings for the instance
/// * `display_handle` - Raw display handle for the windowing system
/// * `external_extensions` - A slice of `CString` representing additional required instance extensions,
/// typically provided by integration libraries like `egui-ash`
pub fn new(
config: &InstanceConfig,
display_handle: &dyn HasDisplayHandle,
external_extentions: &[CString],
) -> Result<Arc<Self>> {
let entry = unsafe { ash::Entry::load()? };
let app_name = CString::new(config.application_name.clone())?;
let engine_name = CString::new(config.engine_name.clone())?;
let app_info = vk::ApplicationInfo::default()
.application_name(&app_name)
.application_version(config.application_version)
.engine_name(&engine_name)
.engine_version(config.engine_version)
.api_version(vk::API_VERSION_1_3);
let validation_layers = [c"VK_LAYER_KHRONOS_validation"];
let enabled_layer_names_raw: Vec<*const c_char> = if config.enable_validation
&& Self::check_validation_layer_support(&entry, &validation_layers)?
{
tracing::info!("Validation layers enabled.");
validation_layers.iter().map(|name| name.as_ptr()).collect()
} else {
if config.enable_validation {
tracing::warn!("Validation layers requested but not supported. Disabling.");
}
Vec::new()
};
let display_handle: DisplayHandle = display_handle.display_handle()?;
let surface_extensions_ptrs =
ash_window::enumerate_required_extensions(display_handle.into())?;
let mut required_cstrs_for_check: Vec<&CStr> = Vec::new();
let surface_extensions_cstrs: Vec<&CStr> = surface_extensions_ptrs
.iter()
.map(|&ptr| unsafe { CStr::from_ptr(ptr) })
.collect();
required_cstrs_for_check.extend(&surface_extensions_cstrs);
required_cstrs_for_check.extend(external_extentions.iter().map(|cs| cs.as_c_str()));
if config.enable_validation {
required_cstrs_for_check.push(ash::ext::debug_utils::NAME);
}
required_cstrs_for_check.sort_unstable();
required_cstrs_for_check.dedup();
Self::check_instance_extension_support(&entry, &required_cstrs_for_check)?;
tracing::info!(
"Required instance extensions supported: {:?}",
required_cstrs_for_check
);
let mut enabled_extension_names_raw: Vec<*const c_char> = Vec::new();
enabled_extension_names_raw.extend(surface_extensions_ptrs);
enabled_extension_names_raw.extend(external_extentions.iter().map(|cs| cs.as_ptr()));
if config.enable_validation {
enabled_extension_names_raw.push(ash::ext::debug_utils::NAME.as_ptr());
}
enabled_extension_names_raw.sort_unstable();
enabled_extension_names_raw.dedup();
let mut debug_create_info = vk::DebugUtilsMessengerCreateInfoEXT::default()
.message_severity(
vk::DebugUtilsMessageSeverityFlagsEXT::WARNING
| vk::DebugUtilsMessageSeverityFlagsEXT::ERROR,
)
.message_type(
vk::DebugUtilsMessageTypeFlagsEXT::GENERAL
| vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION
| vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE,
)
.pfn_user_callback(Some(vulkan_debug_callback));
let mut instance_create_info = vk::InstanceCreateInfo::default()
.application_info(&app_info)
.enabled_layer_names(&enabled_layer_names_raw)
.enabled_extension_names(&enabled_extension_names_raw);
if config.enable_validation {
instance_create_info = instance_create_info.push_next(&mut debug_create_info);
}
let instance = unsafe { entry.create_instance(&instance_create_info, None)? };
tracing::info!("Vulkan instance created succesfully.");
let (debug_utils, debug_messenger) = if config.enable_validation {
let utils = ash::ext::debug_utils::Instance::new(&entry, &instance);
let messenger =
unsafe { utils.create_debug_utils_messenger(&debug_create_info, None)? };
tracing::debug!("Debug messenger created.");
(Some(utils), Some(messenger))
} else {
(None, None)
};
Ok(Arc::new(Self {
entry,
instance,
debug_utils,
debug_messenger,
}))
}
/// Provides access to the loaded Vulkan entry points.
pub fn entry(&self) -> &ash::Entry {
&self.entry
}
/// Provides access to the raw `ash::Instance`.
pub fn ash_instance(&self) -> &ash::Instance {
&self.instance
}
/// Provides access to the DebugUtils extension loader, if enabled.
pub fn debug_utils(&self) -> Option<&debug_utils::Instance> {
self.debug_utils.as_ref()
}
/// Checks if the requested validation layers are available.
fn check_validation_layer_support(
entry: &ash::Entry,
required_layers: &[&CStr],
) -> Result<bool> {
let available_layers = unsafe { entry.enumerate_instance_layer_properties()? };
let available_names: HashSet<&CStr> = available_layers
.iter()
.map(|layer| unsafe { CStr::from_ptr(layer.layer_name.as_ptr()) })
.collect();
for layer in required_layers {
if !available_names.contains(layer) {
tracing::warn!("Required validation layer {:?} not found.", layer);
return Ok(false);
}
}
Ok(true)
}
/// Checks if the requested instance extensions are available.
/// Takes a deduplicated list of required extension names (&CStr).
fn check_instance_extension_support(
entry: &ash::Entry,
required_extensions: &[&CStr],
) -> Result<()> {
let available_extensions = unsafe { entry.enumerate_instance_extension_properties(None)? };
let available_names: HashSet<&CStr> = available_extensions
.iter()
.map(|ext| unsafe { CStr::from_ptr(ext.extension_name.as_ptr()) })
.collect();
tracing::debug!("Available instance extensions: {:?}", available_names);
for ext in required_extensions {
if !available_names.contains(ext) {
tracing::error!("Missing required instance extension: {:?}", ext);
return Err(GfxHalError::MissingExtension(
ext.to_string_lossy().into_owned(),
));
}
}
Ok(())
}
// /// Enumerates all physical devices available to this instance.
// ///
// /// # Safety
// /// The `Instance` must be kept alive while the returned `PhysicalDevices`s are in use.
// /// This is ensured by returning `PhysicalDevice`s holding an `Arc<Instance>`.
// pub unsafe fn enumerate_phyiscal_devices(self: &Arc<Self>) -> Result<Vec<PhysicalDevice>> {
// let physical_device_handles = self.instance.enumerate_physical_devices()?;
//
// if physical_device_handles.is_empty() {
// return Err(GfxHalError::NoSuitableGpu(
// "No Vulkan-compatibile GPUs found.".to_string(),
// ));
// }
//
// let devices = physical_device_handles
// .into_iter()
// .map(|handle| PhysicalDevice::new(Arc::clone(self), handle))
// .collect()?;
//
// Ok(devices)
// }
// /// Creates a vulkan surface for the given window
// ///
// /// # Safety
// /// The `window_handle_trait_obj` must point to a valid window/display managed by caller.
// /// The `Instance` must be kept alive longer than the returned `Surface`
// pub unsafe fn create_surface(
// self: &Arc<Self>,
// window_handle_trait_obj: &(impl HasWindowHandle + HasDisplayHandle),
// ) -> Result<Arc<Surface>> {
// Surface::new(Arc::clone(self), window_handle_trait_obj)
// }
}
impl Drop for Instance {
fn drop(&mut self) {
unsafe {
if let (Some(utils), Some(messenger)) = (&self.debug_utils, self.debug_messenger) {
tracing::debug!("Destroying debug messenger...");
utils.destroy_debug_utils_messenger(messenger, None);
}
tracing::debug!("Destroying Vulkan instance...");
self.instance.destroy_instance(None);
tracing::debug!("Vulkan instance destroyed");
}
}
}

View file

@ -0,0 +1,8 @@
pub mod device;
pub mod error;
pub mod instance;
pub mod physical_device;
pub mod queue;
pub mod surface;
pub mod swapchain;
pub mod sync;

View file

@ -0,0 +1,164 @@
use ash::vk;
use crate::{error::GfxHalError, instance::Instance};
use std::{ffi::CStr, sync::Arc};
/// Represents a physical Vulkan device (GPU).
///
/// This struct holds a handle to the Vulkan physical device and a
/// reference back to the `Instance` it originated from. It does *not* own
/// the `vk::PhysicalDevice` in the sense that it doesn't destroy it; physical
/// devices are implicitly managed by the `vk::Instance`
///
/// It's cheap to clone as it only clones the `Arc<Instance>` and copies the handle
#[derive(Clone)]
pub struct PhysicalDevice {
/// Shared reference to the Vulkan instance
instance: Arc<Instance>,
/// The raw Vulkan physical device handle.
handle: vk::PhysicalDevice,
}
/// Holds information about queue families found on a `PhysicalDevice`.
#[derive(Debug, Clone)]
pub struct QueueFamilyIndices {
/// Queue family index supporting graphics operations.
pub graphics_family: Option<u32>,
/// Queue family index supporting compute operations.
pub compute_family: Option<u32>,
/// Queue family index supporting transfer operations.
pub transfer_family: Option<u32>,
/// Queue family index supporting presentaiton to a given surface.
/// This might be the same as the graphics family.
pub present_family: Option<u32>,
}
impl QueueFamilyIndices {
/// Checks if all essential queue families (graphics, present if surface exists) were found.
pub fn is_complete(&self, requires_present: bool) -> bool {
self.graphics_family.is_some() && (!requires_present || self.present_family.is_some())
}
}
/// Represents the suitability of a physical device.
#[derive(Debug)]
pub enum Suitability<'a> {
/// The device is suitable and meets requirements.
Suitable {
/// A score indicating preference (higher is better).
score: u32,
/// The indicies of the required queue families.
indicies: QueueFamilyIndices,
/// The properties of the device.
properties: Box<vk::PhysicalDeviceProperties>,
/// The supported base features of the device.
features: Box<vk::PhysicalDeviceFeatures>,
/// THe supported mesh shader features.
mesh_shader_features: vk::PhysicalDeviceMeshShaderFeaturesEXT<'a>,
},
/// The device is not suitable.
NotSuitable {
/// The reason why the device is not suitable.
reason: String,
},
}
impl PhysicalDevice {
/// Creates a new `PhysicalDevice` wrapper
/// Typically called internally by `Instance::enumerate_physical_devices`
pub(crate) fn new(instance: Arc<Instance>, handle: vk::PhysicalDevice) -> Self {
Self { instance, handle }
}
/// Gets the raw `vk::PhysicalDevice` handle.
pub fn handle(&self) -> vk::PhysicalDevice {
self.handle
}
/// Gets a reference to the `Instance` this device belongs to.
pub fn instance(&self) -> &Arc<Instance> {
&self.instance
}
/// Queries the basic properties of the physical device.
///
/// # Safety
/// Assumes the `PhysicalDevice` handle is valid.
pub unsafe fn get_properties(&self) -> vk::PhysicalDeviceProperties {
self.instance
.ash_instance()
.get_physical_device_properties(self.handle)
}
/// Queries the supported features, including mesh shaders.
///
/// # Safety
/// Assumes the `PhysicalDevice` handle is valid.
pub unsafe fn get_features(
&self,
) -> (
vk::PhysicalDeviceFeatures,
vk::PhysicalDeviceMeshShaderFeaturesEXT,
) {
let mut mesh_shader_features = vk::PhysicalDeviceMeshShaderFeaturesEXT::default();
let mut features2 =
vk::PhysicalDeviceFeatures2::default().push_next(&mut mesh_shader_features);
self.instance
.ash_instance()
.get_physical_device_features2(self.handle, &mut features2);
(features2.features, mesh_shader_features)
}
/// Queries the properties of all queue families available on the device.
///
/// # Safety
/// Assumes the `PhysicalDevice` handle is valid.
pub unsafe fn get_queue_family_properties(&self) -> Vec<vk::QueueFamilyProperties> {
self.instance
.ash_instance()
.get_physical_device_queue_family_properties(self.handle)
}
/// Queries the device specific extensions supported by this physical device.
///
/// # Safety
/// Assumes the `PhysicalDevice` handle is valid.
pub unsafe fn get_supported_extensions(
&self,
) -> Result<Vec<vk::ExtensionProperties>, GfxHalError> {
self.instance
.ash_instance()
.enumerate_device_extension_properties(self.handle)
.map_err(GfxHalError::VulkanError)
}
// /// Finds suitable queue family indicies based on required flags and optional surface.
// ///
// /// # Safety
// /// Assumes the `PhysicalDevice` handle and `Surface` (if provided) are valid.
// pub unsafe fn find_queue_families(
// &self,
// surface: Option<&Surface>,
// ) -> Result<QueueFamilyIndices, GfxHalError> {
// }
// /// Checks if the physical device meets the specified requirements and scores it.
// ///
// /// # Arguments
// /// * `required_extensions` - A slice of C-style strings representing required device extensions (e.g., `ash::extensions::khr::Swapchain::name()`).
// /// * `required_mesh_features` - The minimum mesh shader features required. Check `task_shader` and `mesh_shader` fields.
// /// * `surface` - An optional surface to check for presentation support.
// ///
// /// # Safety
// /// Assumes the `PhysicalDevice` handle and `Surface` (if provided) are valid.
// pub unsafe fn check_suitability(
// &self,
// required_extensions: &[&CStr],
// required_mesh_features: &vk::PhysicalDeviceMeshShaderFeaturesEXT,
// surface: Option<&Surface>,
// ) -> Result<Suitability, GfxHalError> {
// }
}

102
crates/gfx_hal/src/queue.rs Normal file
View file

@ -0,0 +1,102 @@
use std::sync::Arc;
use ash::vk;
use parking_lot::Mutex;
use crate::device::Device;
use crate::error::Result;
use crate::sync::Fence;
/// Represents a Vulkan device queue.
///
/// Holds a reference to the `Device` and the raw `vk::Queue` handle.
/// Provides methods for submitting command buffers.
pub struct Queue {
device: Arc<Device>,
queue: vk::Queue,
family_index: u32,
// Each queue submission must be externally synchronized or locked internally.
// Using a Mutex here provides a simple internal locking per queue.
submit_lock: Mutex<()>,
}
impl Queue {
/// Creates a new Queue wrapper. Called internally by `Device`.
pub(crate) fn new(device: Arc<Device>, queue: vk::Queue, family_index: u32) -> Self {
Self {
device,
queue,
family_index,
submit_lock: Mutex::new(()),
}
}
/// Gets the raw `vk::Queue` handle.
pub fn handle(&self) -> vk::Queue {
self.queue
}
/// Gets the queue family index this queue belongs to.
pub fn family_index(&self) -> u32 {
self.family_index
}
/// Gets a reference to the logical device this queue belongs to.
pub fn device(&self) -> &Arc<Device> {
&self.device
}
/// Submits command buffers to the queue.
///
/// This method acquires an internal lock for the duration of the submission call
/// to prevent concurrent `vkQueueSubmit` calls on the same queue from this wrapper.
///
/// # Arguments
/// * `submits` - A slice of `vk::SubmitInfo` describing the work to submit.
/// * `signal_fence` - An optional `Fence` to signal when the submission completes.
///
/// # Safety
/// - The command buffers and synchronization primitieves within `submits` must be valid.
/// - The `signal_fence`, if provided, must be valid and unsignaled.
pub unsafe fn submit(
&self,
submits: &[vk::SubmitInfo],
signal_fence: Option<&Fence>,
) -> Result<()> {
let fence_handle = signal_fence.map_or(vk::Fence::null(), |f| f.handle());
let _lock = self.submit_lock.lock();
tracing::trace!(
"Submitting {} batch(es) to queue family {}",
submits.len(),
self.family_index
);
self.device
.raw()
.queue_submit(self.queue, submits, fence_handle)?;
tracing::trace!("Submission successful.");
Ok(())
}
/// Waits until this queue becomes idle.
///
/// This is a heavy operation and blocks the current thread.
pub fn wait_idle(&self) -> Result<()> {
tracing::debug!("Waiting for queue idle (family {})...", self.family_index);
// Lock the mutex while waiting to prevent submissions during the wait?
// Or allow submissions and let Vulkan handle it? Let Vulkan handle it.
unsafe { self.device.raw().queue_wait_idle(self.queue)? };
tracing::debug!("Queue idle (family {}).", self.family_index);
Ok(())
}
// Note: vkQueuePresentKHR is intentionally omitted here.
// Presentation is tightly coupled with Swapchain. It's safer to
// have a method like `Swapchain::present(&self, queue: &Queue, ...)`
// which internally calls `queue.device().raw().queue_present_khr(...)`
// using the swapchain's loader.
// If direct access is needed, it can be done with `queue.device().raw()`.
}
// Queues don't own the vk::Queue handle (the Device does), so no Drop impl.

View file

@ -0,0 +1,126 @@
use crate::{
error::{GfxHalError, Result},
instance::Instance,
};
use ash::{khr::surface::Instance as SurfaceLoader, vk};
use std::sync::Arc;
use winit::raw_window_handle::{HasDisplayHandle, HasWindowHandle};
/// Represents a Vulkan presentation surface, tied to a window.
///
/// Owns the `vk::SurfaceKHR` handle and the `ash` Surface loader extension.
pub struct Surface {
instance: Arc<Instance>,
surface_loader: SurfaceLoader,
surface: vk::SurfaceKHR,
}
impl Surface {
/// Creates a new Vulkan `Surface`
///
/// # Safety
/// - The `window_handle_trait_obj` must provide valid window and display handles
/// for the lifetime of the `Surface`.
/// - The `Instance` must outlive the `Surface`
pub unsafe fn new(
instance: Arc<Instance>,
window_handle_trait_obj: &(impl HasWindowHandle + HasDisplayHandle),
) -> Result<Arc<Self>> {
let surface_loader = SurfaceLoader::new(instance.entry(), instance.ash_instance());
let surface = ash_window::create_surface(
instance.entry(),
instance.ash_instance(),
window_handle_trait_obj.display_handle()?.into(),
window_handle_trait_obj.window_handle()?.into(),
None,
)
.map_err(GfxHalError::SurfaceCreationError)?;
tracing::info!("Vulkan surface created successfully.");
Ok(Arc::new(Self {
instance,
surface_loader,
surface,
}))
}
/// Gets the raw `vk::SurfaceKHR` handle.
pub fn handle(&self) -> vk::SurfaceKHR {
self.surface
}
/// Gets a reference to the `ash` Surface loader extension.
pub fn surface_loader(&self) -> &SurfaceLoader {
&self.surface_loader
}
/// Gets a reference to the `Instance` this surface belongs to
pub fn instance(&self) -> &Arc<Instance> {
&self.instance
}
/// Queries surface capabilites for a given physical device.
///
/// # Safety
/// The `physical_device` handle must be valid and compatible with this surface.
pub unsafe fn get_physical_device_surface_capabilities(
&self,
physical_device: vk::PhysicalDevice,
) -> Result<vk::SurfaceCapabilitiesKHR> {
self.surface_loader
.get_physical_device_surface_capabilities(physical_device, self.surface)
.map_err(GfxHalError::VulkanError)
}
/// Queries supported surface formats for a given physical device.
///
/// # Safety
/// The `physical_device` handle must be valid and compatible with this surface.
pub unsafe fn get_physical_device_surface_formats(
&self,
physical_device: vk::PhysicalDevice,
) -> Result<Vec<vk::SurfaceFormatKHR>> {
self.surface_loader
.get_physical_device_surface_formats(physical_device, self.surface)
.map_err(GfxHalError::VulkanError)
}
/// Queries supported present modes for a given physical device.
///
/// # Safety
/// The `physical_device` handle must be valid and compatible with this surface.
pub unsafe fn get_physical_device_surface_present_modes(
&self,
physical_device: vk::PhysicalDevice,
) -> Result<Vec<vk::PresentModeKHR>> {
self.surface_loader
.get_physical_device_surface_present_modes(physical_device, self.surface)
.map_err(GfxHalError::VulkanError)
}
/// Queries surface support for a given queue family index on a physical device.
///
/// # Safety
/// The `physical_device` handle must be valid and compatible with this surface.
pub unsafe fn get_physical_device_surface_support(
&self,
physical_device: vk::PhysicalDevice,
queue_family_index: u32,
) -> Result<bool> {
self.surface_loader
.get_physical_device_surface_support(physical_device, queue_family_index, self.surface)
.map_err(GfxHalError::VulkanError)
}
}
impl Drop for Surface {
fn drop(&mut self) {
tracing::debug!("Destroying Vulkan surface...");
unsafe {
self.surface_loader.destroy_surface(self.surface, None);
}
tracing::debug!("Vulkan surface destroyed.");
}
}

View file

@ -0,0 +1,321 @@
use std::sync::Arc;
use ash::khr::swapchain::Device as SwapchainLoader;
use ash::vk;
use crate::device::{self, Device};
use crate::error::{GfxHalError, Result};
use crate::surface::{self, Surface};
use crate::sync::{Fence, Semaphore};
/// Configuration for creating or recreating a `Swapchain`.
#[derive(Clone, Debug)]
pub struct SwapchainConfig {
/// Desired number of images in the swapchain (min/max clamped by capabilities).
pub desired_image_count: u32,
/// Preferred surface format (e.g., `vk::Format::B8G8R8A8_SRGB`).
pub desired_format: vk::SurfaceFormatKHR,
/// Preferred presentation mode (e.g., `vk::PresentModeKHR::MAILBOX`).
pub desired_present_mode: vk::PresentModeKHR,
/// Desired usage flags for swapchain images (e.g., `vk::ImageUsageFlags::COLOR_ATTACHMENT`).
pub image_usage: vk::ImageUsageFlags,
/// The dimensions of the surface.
pub extent: vk::Extent2D,
/// Transformation to apply (usually `vk::SurfaceTransformFlagsKHR::IDENTITY`).
pub pre_transform: vk::SurfaceTransformFlagsKHR,
/// Alpha compositing mode (usually `vk::CompositeAlphaFlagsKHR::OPAQUE`).
pub composite_alpha: vk::CompositeAlphaFlagsKHR,
}
/// Represents the Vulkan swapchain, managing presentation images.
///
/// Owns the `vk::SwapchainKHR`, the `ash` Swapchain loader, the swapchain images,
/// and their corresponding image views.
pub struct Swapchain {
device: Arc<Device>,
swapchain_loader: SwapchainLoader,
swapchain: vk::SwapchainKHR,
images: Vec<vk::Image>,
image_views: Vec<vk::ImageView>,
format: vk::SurfaceFormatKHR,
extent: vk::Extent2D,
image_count: u32,
}
impl Swapchain {
/// Creates a new `Swapchain` or recreates an exisiting one.
///
/// # Arguments
/// * `device` - The logical device.
/// * `surface` - The surface to present to.
/// * `config` - Desired swapchain configuration.
/// * `old_swapchain` - Optional handle to a previous swapchain for smoother recreation.
///
/// # Safety
/// - `device` and `surface` must be valid and compatible.
/// - If `old_swapchain` is provided, it must be a valid handle previously created
/// with the same `surface`.
/// - The caller must ensure that the `old_swapchain` (and its associated resources like
/// image views) are no longer in use before calling this function and are properly
/// destroyed *after* the new swapchain is successfully created.
pub unsafe fn new(
device: Arc<Device>,
surface: Arc<Surface>,
config: SwapchainConfig,
old_swapchain: Option<vk::SwapchainKHR>,
) -> Result<Self> {
let physical_device = device.physical_device_handle();
let capabilities = surface.get_physical_device_surface_capabilities(physical_device)?;
let formats = surface.get_physical_device_surface_formats(physical_device)?;
let present_modes = surface.get_physical_device_surface_present_modes(physical_device)?;
if formats.is_empty() || present_modes.is_empty() {
return Err(GfxHalError::NoSuitableGpu(
"Swapchain creation failed: No formats or present modes available.".to_string(),
));
}
let surface_format = Self::choose_surface_format(&formats, config.desired_format);
let present_mode = Self::choose_present_mode(&present_modes, config.desired_present_mode);
let extent = Self::choose_extent(capabilities, config.extent);
let image_count = Self::choose_image_count(capabilities, config.desired_image_count);
tracing::info!("Creating swapchain: Format={:?}, ColorSpace={:?}, PresentMode={:?}, Extent={:?}, ImageCount={}", surface_format.format, surface_format.color_space, present_mode, extent, image_count);
let mut create_info = vk::SwapchainCreateInfoKHR::default()
.surface(surface.handle())
.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(config.image_usage)
.pre_transform(config.pre_transform)
.composite_alpha(config.composite_alpha)
.present_mode(present_mode)
.clipped(true);
let queue_family_indicies = [device.graphics_queue_family_index()];
create_info = create_info
.image_sharing_mode(vk::SharingMode::EXCLUSIVE)
.queue_family_indices(&queue_family_indicies);
if let Some(old) = old_swapchain {
create_info = create_info.old_swapchain(old);
tracing::debug!("Passing old swapchain handle for recreation.");
}
let swapchain_loader =
SwapchainLoader::new(surface.instance().ash_instance(), device.raw());
let swapchain = swapchain_loader.create_swapchain(&create_info, None)?;
tracing::info!("Swapchain created successfully.");
let images = swapchain_loader.get_swapchain_images(swapchain)?;
tracing::debug!("Retrieved {} swapchain images.", images.len());
let image_views = Self::create_image_views(device.raw(), &images, surface_format.format)?;
tracing::debug!("Created {} swapchain image views.", image_views.len());
Ok(Self {
device,
swapchain_loader,
swapchain,
images,
image_views,
format: surface_format,
extent,
image_count,
})
}
/// Acquires the next available image from the swapchain.
///
/// Returns the index of the acquired image and a boolean indicating if the
/// swapchain is suboptimal (needs recreation).
///
/// # Safety
/// - `signal_semaphore` and `signal_fence`, if provided, must be valid handles
/// that are not currently waited on by the GPU.
/// - The caller must ensure proper synchronization before using the returned image index.
pub unsafe fn acquire_next_image(
&self,
timeout_ns: u64,
signal_semaphore: Option<&Semaphore>,
signal_fence: Option<&Fence>,
) -> Result<(u32, bool)> {
let semaphore_handle = signal_semaphore.map_or(vk::Semaphore::null(), |s| s.handle());
let fence_handle = signal_fence.map_or(vk::Fence::null(), |f| f.handle());
match self.swapchain_loader.acquire_next_image(
self.swapchain,
timeout_ns,
semaphore_handle,
fence_handle,
) {
Ok((image_index, suboptimal)) => Ok((image_index, suboptimal)),
Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => Err(GfxHalError::SurfaceLost),
Err(e) => Err(GfxHalError::VulkanError(e)),
}
}
/// Gets the raw `vk::SwapchainKHR` handle.
pub fn handle(&self) -> vk::SwapchainKHR {
self.swapchain
}
/// Gets a reference to the `ash` Swapchain loader extension
pub fn loader(&self) -> &SwapchainLoader {
&self.swapchain_loader
}
/// Gets the chosen surface format of the swapchain.
pub fn format(&self) -> vk::SurfaceFormatKHR {
self.format
}
/// Gets the extent (dimensions) of the swapchain image
pub fn extent(&self) -> vk::Extent2D {
self.extent
}
/// Gets the actual number of images in the swapchain
pub fn image_count(&self) -> u32 {
self.image_count
}
/// Gets a slice containing the raw `vk::Image` handles.
pub fn images(&self) -> &[vk::Image] {
&self.images
}
/// Gets a slice containing the raw `vk::ImageView` handles.
pub fn image_views(&self) -> &[vk::ImageView] {
&self.image_views
}
fn choose_surface_format(
available_formats: &[vk::SurfaceFormatKHR],
desired_format: vk::SurfaceFormatKHR,
) -> vk::SurfaceFormatKHR {
for format in available_formats {
if format.format == desired_format.format
&& format.color_space == desired_format.color_space
{
return *format;
}
}
tracing::warn!(
"Desired swapchain format {:?}/{:?} not available. Falling back to {:?}/{:?}.",
desired_format.format,
desired_format.color_space,
available_formats[0].format,
available_formats[0].color_space
);
available_formats[0]
}
fn choose_present_mode(
available_modes: &[vk::PresentModeKHR],
desired_mode: vk::PresentModeKHR,
) -> vk::PresentModeKHR {
if desired_mode == vk::PresentModeKHR::MAILBOX
&& available_modes.contains(&vk::PresentModeKHR::MAILBOX)
{
return vk::PresentModeKHR::MAILBOX;
}
if desired_mode == vk::PresentModeKHR::IMMEDIATE
&& available_modes.contains(&vk::PresentModeKHR::IMMEDIATE)
{
return vk::PresentModeKHR::IMMEDIATE;
}
vk::PresentModeKHR::FIFO
}
fn choose_extent(
capabilities: vk::SurfaceCapabilitiesKHR,
desired_extent: vk::Extent2D,
) -> vk::Extent2D {
if capabilities.current_extent.width != u32::MAX {
capabilities.current_extent
} else {
vk::Extent2D {
width: desired_extent.width.clamp(
capabilities.min_image_extent.width,
capabilities.max_image_extent.width,
),
height: desired_extent.height.clamp(
capabilities.min_image_extent.height,
capabilities.max_image_extent.height,
),
}
}
}
fn choose_image_count(capabilities: vk::SurfaceCapabilitiesKHR, desired_count: u32) -> u32 {
let mut count = desired_count.max(capabilities.min_image_count);
if capabilities.max_image_count > 0 {
count = count.min(capabilities.max_image_count);
}
count
}
unsafe fn create_image_views(
device: &ash::Device,
images: &[vk::Image],
format: vk::Format,
) -> Result<Vec<vk::ImageView>> {
images
.iter()
.map(|image| {
let create_info = vk::ImageViewCreateInfo::default()
.image(*image)
.view_type(vk::ImageViewType::TYPE_2D)
.format(format)
.components(vk::ComponentMapping {
r: vk::ComponentSwizzle::IDENTITY,
g: vk::ComponentSwizzle::IDENTITY,
b: vk::ComponentSwizzle::IDENTITY,
a: vk::ComponentSwizzle::IDENTITY,
})
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
});
device.create_image_view(&create_info, None)
})
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(GfxHalError::VulkanError)
}
/// Destroys the image views associated with this swapchain.
/// Called internally by Drop and potentially during recreation.
unsafe fn destory_image_views(&mut self) {
tracing::debug!(
"Destroying {} swapchain image views...",
self.image_views.len()
);
for view in self.image_views.drain(..) {
self.device.raw().destroy_image_view(view, None);
}
tracing::debug!("Swapchain image views destroyed.")
}
}
impl Drop for Swapchain {
fn drop(&mut self) {
tracing::debug!("Destroying swapchain...");
unsafe {
self.destory_image_views();
self.swapchain_loader
.destroy_swapchain(self.swapchain, None);
}
tracing::debug!("Swapchain destroyed.")
}
}

183
crates/gfx_hal/src/sync.rs Normal file
View file

@ -0,0 +1,183 @@
use std::{sync::Arc, time::Duration};
use ash::vk;
use crate::{
device::Device,
error::{GfxHalError, Result},
};
/// Wraps a `vk::Fence`, used for CPU-GPU synchronization.
///
/// Owns the `vk::Fence` handle.
pub struct Fence {
device: Arc<Device>,
fence: vk::Fence,
}
impl Fence {
/// Creates a new `Fence`.
///
/// # Arguments
/// * `device` - The logical device.
/// * `signaled` - If true, the fence is created in the signaled state.
pub fn new(device: Arc<Device>, signaled: bool) -> Result<Self> {
let create_flags = if signaled {
vk::FenceCreateFlags::SIGNALED
} else {
vk::FenceCreateFlags::empty()
};
let create_info = vk::FenceCreateInfo::default().flags(create_flags);
let fence = unsafe { device.raw().create_fence(&create_info, None)? };
tracing::trace!("Created Fence (signaled: {})", signaled);
Ok(Self { device, fence })
}
/// Waits for the fence to become signaled.
///
/// # Arguments
/// * `timeout` - Maximum duration to wait. `None` waits indefinitely.
pub fn wait(&self, timeout: Option<Duration>) -> Result<()> {
let timeout_ns = timeout.map_or(u64::MAX, |d| d.as_nanos() as u64);
tracing::trace!("Waiting for Fence with timeout: {:?}", timeout);
let fences = [self.fence];
match unsafe { self.device.raw().wait_for_fences(&fences, true, timeout_ns) } {
Ok(_) => {
tracing::trace!("Fence signaled.");
Ok(())
}
Err(vk::Result::TIMEOUT) => {
tracing::trace!("Fence wait timed out.");
Err(GfxHalError::VulkanError(vk::Result::TIMEOUT)) // Return timeout error
}
Err(e) => Err(GfxHalError::VulkanError(e)),
}
}
/// Resets the fence to the unsignaled state.
/// Must only be called when the fence is not in use by pending GPU work.
pub fn reset(&self) -> Result<()> {
tracing::trace!("Resetting Fence.");
let fences = [self.fence];
unsafe { self.device.raw().reset_fences(&fences)? };
Ok(())
}
/// Checks the current status of the fence without waiting.
/// Returns `Ok(true)` if signaled, `Ok(false)` if unsignaled.
pub fn status(&self) -> Result<bool> {
match unsafe { self.device.raw().get_fence_status(self.fence) } {
Ok(signaled) => Ok(signaled),
// NOT_READY means unsignaled, not an error in this context
Err(vk::Result::NOT_READY) => Ok(false),
Err(e) => Err(GfxHalError::VulkanError(e)),
}
}
/// Gets the raw `vk::Fence` handle.
pub fn handle(&self) -> vk::Fence {
self.fence
}
}
impl Drop for Fence {
fn drop(&mut self) {
tracing::trace!("Destroying fence...");
unsafe {
self.device.raw().destroy_fence(self.fence, None);
}
tracing::trace!("Fence destroyed.")
}
}
/// Wraps a `vk::Semaphore`, used for GPU-GPU synchronization (within or across queues).
///
/// Owns the `vk::Semaphore` handle.
pub struct Semaphore {
device: Arc<Device>,
semaphore: vk::Semaphore,
}
impl Semaphore {
/// Creates a new `Semaphore`.
pub fn new(device: Arc<Device>) -> Result<Self> {
let create_info = vk::SemaphoreCreateInfo::default();
let semaphore = unsafe { device.raw().create_semaphore(&create_info, None)? };
tracing::trace!("Created Semaphore.");
Ok(Self { device, semaphore })
}
/// Gets the raw `vk::Semaphore` handle.
pub fn handle(&self) -> vk::Semaphore {
self.semaphore
}
}
impl Drop for Semaphore {
fn drop(&mut self) {
tracing::trace!("Destroying Semaphore...");
unsafe {
self.device.raw().destroy_semaphore(self.semaphore, None);
}
tracing::trace!("Semaphore destroyed.");
}
}
/// Wraps a `vk::Event`, used for fine-grained GPU-GPU or GPU-Host synchronization.
///
/// Owns the `vk::Event` handle.
pub struct Event {
device: Arc<Device>,
event: vk::Event,
}
impl Event {
/// Creates a new `Event`.
pub fn new(device: Arc<Device>) -> Result<Self> {
let create_info = vk::EventCreateInfo::default();
let event = unsafe { device.raw().create_event(&create_info, None)? };
tracing::trace!("Created Event.");
Ok(Self { device, event })
}
/// Sets the event from the host (CPU).
pub fn set(&self) -> Result<()> {
tracing::trace!("Setting Event from host.");
unsafe { self.device.raw().set_event(self.event)? };
Ok(())
}
/// Resets the event from the host (CPU).
pub fn reset(&self) -> Result<()> {
tracing::trace!("Resetting Event from host.");
unsafe { self.device.raw().reset_event(self.event)? };
Ok(())
}
/// Checks the status of the event from the host (CPU).
/// Returns `Ok(true)` if set, `Ok(false)` if reset.
pub fn status(&self) -> Result<bool> {
let res = unsafe { self.device.raw().get_event_status(self.event) }?;
Ok(res)
}
/// Gets the raw `vk::Event` handle.
pub fn handle(&self) -> vk::Event {
self.event
}
// Note: Setting/resetting/waiting on events from the GPU involves
// vkCmdSetEvent, vkCmdResetEvent, vkCmdWaitEvents within command buffers.
// These are not wrapped here but would be used via device.raw() when
// recording command buffers.
}
impl Drop for Event {
fn drop(&mut self) {
tracing::trace!("Destroying Event...");
unsafe {
self.device.raw().destroy_event(self.event, None);
}
tracing::trace!("Event destroyed.");
}
}

View file

@ -0,0 +1,13 @@
[package]
name = "resource_manager"
version = "0.1.0"
edition = "2021"
[dependencies]
ash.workspace = true
gpu-allocator.workspace = true
thiserror.workspace = true
parking_lot.workspace = true
tracing.workspace = true
gfx_hal = { path = "../gfx_hal" }

View file

@ -0,0 +1,48 @@
use ash::vk;
use thiserror::Error;
/// Error type for the resource_manager crate.
#[derive(Error, Debug)]
pub enum ResourceManagerError {
#[error("Vulkan API error: {0}")]
VulkanError(#[from] vk::Result),
#[error("GPU allocation error: {0}")]
AllocationError(#[from] gpu_allocator::AllocationError),
#[error("Resource handle {0} not found")]
HandleNotFound(u64),
#[error("Failed to map buffer memory")]
MappingFailed,
#[error("Buffer is not CPU visible or mapped")]
NotMapped,
#[error("Failed to find suitable memory type")]
NoSuitableMemoryType,
#[error("Failed to find a queue supporting transfer operations")]
NoTransferQueue,
#[error("Staging transfer failed: {0}")]
TransferFailed(String),
#[error("Resource lock poisoned: {0}")]
LockPoisoned(String),
#[error("Error occurred in GfxHal: {0}")]
GfxHalError(#[from] gfx_hal::error::GfxHalError),
#[error("An unexpected error occurred: {0}")]
Other(String),
}
// Implement conversion from Lock Poison errors
impl<T> From<std::sync::PoisonError<T>> for ResourceManagerError {
fn from(e: std::sync::PoisonError<T>) -> Self {
ResourceManagerError::LockPoisoned(e.to_string())
}
}
pub type Result<T, E = ResourceManagerError> = std::result::Result<T, E>;

View file

@ -0,0 +1,609 @@
mod error;
use std::{
collections::HashMap,
hash::Hash,
sync::{
atomic::{AtomicU64, Ordering},
Arc,
},
};
use ash::vk;
use gfx_hal::{device::Device, instance::Instance, queue::Queue};
use tracing::{debug, error, trace, warn};
pub use error::{ResourceManagerError, Result};
use gpu_allocator::{
vulkan::{Allocation, AllocationCreateDesc, Allocator, AllocatorCreateDesc},
MemoryLocation,
};
use parking_lot::Mutex;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct BufferHandle(u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ImageHandle(u64);
#[derive(Debug, Clone)]
pub struct BufferInfo {
pub handle: BufferHandle,
pub buffer: vk::Buffer,
pub size: vk::DeviceSize,
pub usage: vk::BufferUsageFlags,
pub mapped_ptr: Option<*mut u8>,
}
#[derive(Debug, Clone)]
pub struct ImageInfo {
pub handle: ImageHandle,
/// Non-owning handle.
pub image: vk::Image,
/// Non-owning handle.
pub view: vk::ImageView,
pub format: vk::Format,
pub extent: vk::Extent3D,
pub usage: vk::ImageUsageFlags,
pub layout: vk::ImageLayout,
}
struct InternalBufferInfo {
device: Arc<Device>, // Keep device alive for Drop
allocator: Arc<Mutex<Allocator>>, // Needed for Drop
buffer: vk::Buffer,
allocation: Option<Allocation>, // Option because it's taken in Drop
size: vk::DeviceSize,
usage: vk::BufferUsageFlags,
mapped_ptr: Option<*mut u8>,
handle: BufferHandle,
}
impl Drop for InternalBufferInfo {
fn drop(&mut self) {
trace!("Dropping InternalBufferInfo for handle: {:?}", self.handle);
if let Some(allocation) = self.allocation.take() {
let mut allc = self.allocator.lock();
if let Err(e) = allc.free(allocation) {
error!(
"Failed to free allocation for buffer handle {:?}, {}",
self.handle, e
);
} else {
trace!("Freed alloation for buffer handle: {:?}", self.handle);
}
}
unsafe {
self.device.raw().destroy_buffer(self.buffer, None);
}
trace!("Destroyed vk::Buffer for handle {:?}", self.handle);
}
}
struct InternalImageInfo {
device: Arc<Device>, // Keep device alive for Drop
allocator: Arc<Mutex<Allocator>>, // Needed for Drop
image: vk::Image,
view: vk::ImageView,
allocation: Option<Allocation>, // Option because it's taken in Drop
format: vk::Format,
extent: vk::Extent3D,
usage: vk::ImageUsageFlags,
layout: vk::ImageLayout,
handle: ImageHandle,
}
impl Drop for InternalImageInfo {
fn drop(&mut self) {
trace!("Dropping InternalImageInfo for handle {:?}", self.handle);
// Destroy view first
unsafe {
self.device.raw().destroy_image_view(self.view, None);
}
// Then free memory
if let Some(allocation) = self.allocation.take() {
let mut allocator = self.allocator.lock();
if let Err(e) = allocator.free(allocation) {
error!(
"Failed to free allocation for image handle {:?}: {}",
self.handle, e
);
} else {
trace!("Freed allocation for image handle {:?}", self.handle);
}
}
// Then destroy image
unsafe {
self.device.raw().destroy_image(self.image, None);
}
trace!(
"Destroyed vk::Image/vk::ImageView for handle {:?}",
self.handle
);
}
}
struct TransferSetup {
command_pool: vk::CommandPool,
queue: Arc<Queue>,
fence: vk::Fence,
}
pub struct ResourceManager {
_instance: Arc<Instance>,
device: Arc<Device>,
allocator: Arc<Mutex<Allocator>>,
buffers: Mutex<HashMap<u64, InternalBufferInfo>>,
images: Mutex<HashMap<u64, InternalImageInfo>>,
next_id: AtomicU64,
transfer_setup: Mutex<Option<TransferSetup>>,
}
impl ResourceManager {
/// Creates a new ResourceManager.
pub fn new(instance: Arc<Instance>, device: Arc<Device>) -> Result<Self> {
debug!("Initializing ResourceManager...");
let allocator = Allocator::new(&AllocatorCreateDesc {
instance: instance.ash_instance().clone(),
device: device.raw().clone(),
physical_device: device.physical_device_handle(),
debug_settings: Default::default(),
buffer_device_address: true,
allocation_sizes: Default::default(),
})?;
debug!("GPU Allocator created.");
Ok(Self {
_instance: instance,
device,
allocator: Arc::new(Mutex::new(allocator)),
buffers: Mutex::new(HashMap::new()),
images: Mutex::new(HashMap::new()),
next_id: AtomicU64::new(1),
transfer_setup: Mutex::new(None),
})
}
/// Gets or initializes the TransferSetup resources.
fn get_transfer_setup(&self) -> Result<TransferSetup> {
let mut setup_guard = self.transfer_setup.lock();
if let Some(setup) = setup_guard.as_ref() {
// Simple check: Reset fence before reusing
unsafe { self.device.raw().reset_fences(&[setup.fence])? };
return Ok(TransferSetup {
// Return a copy/clone
command_pool: setup.command_pool,
queue: setup.queue.clone(),
fence: setup.fence,
});
}
debug!("Initializing TransferSetup...");
// Find a queue that supports transfer (prefer dedicated, fallback to graphics)
let queue_family_index = self
.device
.transfer_queue_family_index()
.or(self.device.compute_queue_family_index()) // Try compute as fallback
.unwrap_or(self.device.graphics_queue_family_index()); // Graphics as last resort
let queue = self
.device
.get_queue(queue_family_index, 0)
.ok_or(ResourceManagerError::NoTransferQueue)?;
// Create command pool for transfer commands
let pool_info = vk::CommandPoolCreateInfo::default()
.flags(vk::CommandPoolCreateFlags::TRANSIENT) // Hint that buffers are short-lived
.queue_family_index(queue_family_index);
let command_pool = unsafe { self.device.raw().create_command_pool(&pool_info, None)? };
// Create a fence for waiting
let fence_info = vk::FenceCreateInfo::default();
let fence = unsafe { self.device.raw().create_fence(&fence_info, None)? };
let new_setup = TransferSetup {
command_pool,
queue,
fence,
};
*setup_guard = Some(new_setup); // Store it
debug!("TransferSetup initialized.");
// Return a new copy for use
Ok(TransferSetup {
command_pool: setup_guard.as_ref().unwrap().command_pool,
queue: setup_guard.as_ref().unwrap().queue.clone(),
fence: setup_guard.as_ref().unwrap().fence,
})
}
/// Helper to allocate, begin, end, submit, and wait for a single command buffer.
unsafe fn submit_commands_and_wait<F>(
&self,
transfer_setup: &TransferSetup,
record_fn: F,
) -> Result<()>
where
F: FnOnce(vk::CommandBuffer) -> Result<()>,
{
let device = self.device.raw();
// Allocate command buffer
let alloc_info = vk::CommandBufferAllocateInfo::default()
.command_pool(transfer_setup.command_pool)
.level(vk::CommandBufferLevel::PRIMARY)
.command_buffer_count(1);
let command_buffer = device.allocate_command_buffers(&alloc_info)?[0];
// Begin recording
let begin_info = vk::CommandBufferBeginInfo::default()
.flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT);
device.begin_command_buffer(command_buffer, &begin_info)?;
// Record user commands
let record_result = record_fn(command_buffer);
// End recording (even if user function failed, to allow cleanup)
device.end_command_buffer(command_buffer)?;
// Check user function result *after* ending buffer
record_result?;
let binding = [command_buffer];
// Submit
let submits = [vk::SubmitInfo::default().command_buffers(&binding)];
// Use the transfer queue and fence
transfer_setup.queue.submit(&submits, None)?; // Submit without fence initially
// Wait for completion using a separate wait call
// This avoids holding the queue's internal submit lock during the wait.
let fences = [transfer_setup.fence];
match device.wait_for_fences(&fences, true, u64::MAX) {
Ok(_) => {}
Err(vk::Result::TIMEOUT) => {
// Should not happen with u64::MAX
warn!("Transfer fence wait timed out unexpectedly.");
return Err(ResourceManagerError::TransferFailed(
"Fence wait timeout".to_string(),
));
}
Err(e) => return Err(e.into()),
}
// Free command buffer
device.free_command_buffers(transfer_setup.command_pool, &[command_buffer]);
Ok(())
}
/// Creates a Vulkan buffer and allocates/binds memory for it.
pub fn create_buffer(
&self,
size: vk::DeviceSize,
usage: vk::BufferUsageFlags,
location: MemoryLocation,
) -> Result<BufferHandle> {
trace!(
"Creating buffer: size={}, usage={:?}, location={:?}",
size,
usage,
location
);
let buffer_info = vk::BufferCreateInfo::default()
.size(size)
.usage(usage)
.sharing_mode(vk::SharingMode::EXCLUSIVE); // Assuming exclusive access
let buffer = unsafe { self.device.raw().create_buffer(&buffer_info, None)? };
let requirements = unsafe { self.device.raw().get_buffer_memory_requirements(buffer) };
let allocation = self.allocator.lock().allocate(&AllocationCreateDesc {
name: &format!("buffer_usage_{:?}_loc_{:?}", usage, location),
requirements,
location,
linear: true, // Buffers are linear
allocation_scheme: gpu_allocator::vulkan::AllocationScheme::GpuAllocatorManaged,
})?;
unsafe {
self.device.raw().bind_buffer_memory(
buffer,
allocation.memory(),
allocation.offset(),
)?;
}
trace!("Buffer memory bound.");
let mapped_ptr = allocation.mapped_ptr().map(|p| p.as_ptr() as *mut u8);
if mapped_ptr.is_some() {
trace!("Buffer memory is mapped.");
}
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
let handle = BufferHandle(id);
let internal_info = InternalBufferInfo {
device: self.device.clone(),
allocator: self.allocator.clone(),
buffer,
allocation: Some(allocation),
size,
usage,
mapped_ptr,
handle,
};
self.buffers.lock().insert(id, internal_info);
debug!("Buffer created successfully: handle={:?}", handle);
Ok(handle)
}
/// Creates a buffer, allocates memory, and uploads initial data using a staging buffer.
pub fn create_buffer_init(
&self,
usage: vk::BufferUsageFlags, // Usage for the *final* buffer
location: MemoryLocation, // Memory location for the *final* buffer
data: &[u8],
) -> Result<BufferHandle> {
let size = data.len() as vk::DeviceSize;
if size == 0 {
return Err(ResourceManagerError::Other(
"Cannot create buffer with empty data".to_string(),
));
}
debug!(
"Creating buffer with init data: size={}, usage={:?}, location={:?}",
size, usage, location
);
// 1. Create Staging Buffer
let staging_usage = vk::BufferUsageFlags::TRANSFER_SRC;
let staging_location = MemoryLocation::CpuToGpu; // Mapped memory for upload
let staging_handle = self.create_buffer(size, staging_usage, staging_location)?;
// 2. Map & Copy data to staging buffer
{
// Scope for buffer info and mapping pointer
let staging_info = self.get_buffer_info(staging_handle)?;
let mapping = staging_info
.mapped_ptr
.ok_or(ResourceManagerError::MappingFailed)?;
unsafe {
std::ptr::copy_nonoverlapping(data.as_ptr(), mapping, data.len());
}
// If memory is not HOST_COHERENT, need to flush here:
// let mem_range = vk::MappedMemoryRange::builder().memory(...).offset(...).size(size);
// unsafe { self.device.raw().flush_mapped_memory_ranges(&[mem_range])? };
trace!("Data copied to staging buffer.");
} // staging_info goes out of scope
// 3. Create Destination Buffer
let final_usage = usage | vk::BufferUsageFlags::TRANSFER_DST; // Add transfer dest usage
let dest_handle = self.create_buffer(size, final_usage, location)?;
// 4. Record and submit transfer command
let transfer_setup = self.get_transfer_setup()?;
let dest_info = self.get_buffer_info(dest_handle)?; // Get info for vk::Buffer handle
let staging_info_for_copy = self.get_buffer_info(staging_handle)?; // Get info again
trace!("Submitting buffer copy command...");
unsafe {
self.submit_commands_and_wait(&transfer_setup, |cmd| {
let region = vk::BufferCopy::default()
.src_offset(0)
.dst_offset(0)
.size(size);
self.device.raw().cmd_copy_buffer(
cmd,
staging_info_for_copy.buffer, // Use raw handle from info struct
dest_info.buffer, // Use raw handle from info struct
&[region],
);
Ok(()) // Return Ok inside the closure
})?;
}
trace!("Buffer copy command finished.");
// 5. Cleanup staging buffer
self.destroy_buffer(staging_handle)?; // This frees memory and destroys buffer
debug!("Staging buffer destroyed.");
Ok(dest_handle)
}
/// Creates a Vulkan image and allocates/binds memory for it.
/// Also creates a default `ImageView`.
/// Does not handle data uploads or layout transitions.
pub fn create_image(
&self,
create_info: &vk::ImageCreateInfo, // User provides image details
location: MemoryLocation,
) -> Result<ImageHandle> {
trace!(
"Creating image: format={:?}, extent={:?}, usage={:?}, location={:?}",
create_info.format,
create_info.extent,
create_info.usage,
location
);
let image = unsafe { self.device.raw().create_image(create_info, None)? };
let requirements = unsafe { self.device.raw().get_image_memory_requirements(image) };
let allocation = self.allocator.lock().allocate(&AllocationCreateDesc {
name: &format!(
"image_fmt_{:?}_usage_{:?}",
create_info.format, create_info.usage
),
requirements,
location,
linear: create_info.tiling == vk::ImageTiling::LINEAR,
allocation_scheme: gpu_allocator::vulkan::AllocationScheme::GpuAllocatorManaged,
})?;
unsafe {
self.device
.raw()
.bind_image_memory(image, allocation.memory(), allocation.offset())?;
}
trace!("Image memory bound.");
// Create a default image view
// TODO: Make view creation more flexible (allow different subresource ranges, types)
let view_info = vk::ImageViewCreateInfo::default()
.image(image)
.view_type(vk::ImageViewType::TYPE_2D) // Assuming 2D, adjust based on create_info
.format(create_info.format)
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR, // Assuming color, adjust based on usage
base_mip_level: 0,
level_count: create_info.mip_levels,
base_array_layer: 0,
layer_count: create_info.array_layers,
});
let view = unsafe { self.device.raw().create_image_view(&view_info, None)? };
trace!("Default image view created.");
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
let handle = ImageHandle(id);
let internal_info = InternalImageInfo {
device: self.device.clone(),
allocator: self.allocator.clone(),
image,
view,
allocation: Some(allocation),
format: create_info.format,
extent: create_info.extent,
usage: create_info.usage,
layout: create_info.initial_layout, // Store initial layout
handle,
};
self.images.lock().insert(id, internal_info);
debug!("Image created successfully: handle={:?}", handle);
Ok(handle)
}
// TODO: Implement create_image_init (similar to create_buffer_init but uses vkCmdCopyBufferToImage and layout transitions)
/// Destroys a buffer and frees its memory.
pub fn destroy_buffer(&self, handle: BufferHandle) -> Result<()> {
debug!("Requesting destroy for buffer handle {:?}", handle);
let mut buffers_map = self.buffers.lock();
// Remove the entry. The Drop impl of InternalBufferInfo handles the cleanup.
if buffers_map.remove(&handle.0).is_some() {
debug!("Buffer handle {:?} removed for destruction.", handle);
Ok(())
} else {
warn!(
"Attempted to destroy non-existent buffer handle {:?}",
handle
);
Err(ResourceManagerError::HandleNotFound(handle.0))
}
}
/// Destroys an image, its view, and frees its memory.
pub fn destroy_image(&self, handle: ImageHandle) -> Result<()> {
debug!("Requesting destroy for image handle {:?}", handle);
let mut images_map = self.images.lock();
// Remove the entry. The Drop impl of InternalImageInfo handles the cleanup.
if images_map.remove(&handle.0).is_some() {
debug!("Image handle {:?} removed for destruction.", handle);
Ok(())
} else {
warn!(
"Attempted to destroy non-existent image handle {:?}",
handle
);
Err(ResourceManagerError::HandleNotFound(handle.0))
}
}
/// Gets non-owning information about a buffer.
pub fn get_buffer_info(&self, handle: BufferHandle) -> Result<BufferInfo> {
let buffers_map = self.buffers.lock();
buffers_map
.get(&handle.0)
.map(|internal| BufferInfo {
handle: internal.handle,
buffer: internal.buffer,
size: internal.size,
usage: internal.usage,
mapped_ptr: internal.mapped_ptr,
})
.ok_or(ResourceManagerError::HandleNotFound(handle.0))
}
/// Gets non-owning information about an image.
pub fn get_image_info(&self, handle: ImageHandle) -> Result<ImageInfo> {
let images_map = self.images.lock();
images_map
.get(&handle.0)
.map(|internal| ImageInfo {
handle: internal.handle,
image: internal.image,
view: internal.view,
format: internal.format,
extent: internal.extent,
usage: internal.usage,
layout: internal.layout, // Note: Layout tracking is basic here
})
.ok_or(ResourceManagerError::HandleNotFound(handle.0))
}
/// Explicitly waits for the device to be idle. Useful before shutdown.
pub fn wait_device_idle(&self) -> Result<(), ResourceManagerError> {
self.device
.wait_idle()
.map_err(|e| ResourceManagerError::Other(format!("Device wait idle failed: {}", e)))
}
}
impl Drop for ResourceManager {
fn drop(&mut self) {
debug!("Destroying ResourceManager...");
// Ensure all GPU operations are finished before freeing memory/destroying resources
if let Err(e) = self.device.wait_idle() {
error!(
"Failed to wait for device idle during ResourceManager drop: {}",
e
);
// Proceeding with cleanup, but resources might still be in use!
}
// Clear resource maps. This triggers the Drop impl for each Internal*Info,
// which frees allocations and destroys Vulkan objects.
let mut buffers_map = self.buffers.lock();
debug!("Clearing {} buffer entries...", buffers_map.len());
buffers_map.clear();
let mut images_map = self.images.lock();
debug!("Clearing {} image entries...", images_map.len());
images_map.clear();
// Destroy transfer setup resources
let mut setup_guard = self.transfer_setup.lock();
if let Some(setup) = setup_guard.take() {
// take() removes it from the Option
debug!("Destroying TransferSetup resources...");
unsafe {
self.device.raw().destroy_fence(setup.fence, None);
self.device
.raw()
.destroy_command_pool(setup.command_pool, None);
}
debug!("TransferSetup resources destroyed.");
}
// The Allocator is wrapped in an Arc<Mutex<>>, so its Drop will be handled
// when the last Arc reference (including those held by Internal*Info) is dropped.
// gpu-allocator's Allocator Drop implementation should be empty, as memory
// is freed via allocator.free().
debug!("ResourceManager destroyed.");
}
}

View file

@ -1,8 +0,0 @@
[package]
name = "shaders-shared"
version = "0.1.0"
edition = "2021"
[dependencies]
spirv-std.workspace = true
bytemuck.workspace = true

View file

@ -1,31 +0,0 @@
#![cfg_attr(target_arch = "spirv", no_std)]
use spirv_std::glam::{Mat4, Vec3, Vec4};
#[derive(Clone, Copy, Debug)]
#[repr(C, align(16))]
pub struct Material {
pub base_color: Vec4,
pub metallic_factor: f32,
pub roughness_factor: f32,
pub _padding: [f32; 2],
}
#[repr(C, align(16))]
#[derive(Clone)]
pub struct UniformBufferObject {
pub model: Mat4,
pub view: Mat4,
pub proj: Mat4,
pub camera_pos: Vec3,
pub material: Material,
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct PushConstants {
pub texture_size: Vec4,
}
unsafe impl bytemuck::Pod for PushConstants {}
unsafe impl bytemuck::Zeroable for PushConstants {}

View file

@ -1,33 +0,0 @@
[package]
name = "vk-rs"
version = "0.1.0"
edition = "2021"
[dependencies]
ash.workspace = true
color-eyre.workspace = true
winit.workspace = true
raw-window-handle.workspace = true
cfg-if = "1.0.0"
cgmath = "0.18.0"
spirv-std.workspace = true
gpu-allocator.workspace = true
egui-ash = { version = "0.4.0", features = ["gpu-allocator"] }
tobj = "4.0.2"
egui = "0.25.0"
ash-window = "0.12.0"
shaders-shared = { path = "../shaders-shared" }
puffin = { git = "https://github.com/EmbarkStudios/puffin", rev = "5ac4e54164ee89bd68c29f288c2b5613afc2c929" }
puffin_egui = { git = "https://github.com/EmbarkStudios/puffin", rev = "5ac4e54164ee89bd68c29f288c2b5613afc2c929" }
gpu-profiler = { git = "https://github.com/zackartz/gpu-profiler", features = [
"use-ash",
] }
gltf = { version = "1.4.1", features = ["import"] }
image = "0.25.5"
rayon = "1.10.0"
bytemuck.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
[build-dependencies]
shaderc = "0.8"

View file

@ -1,71 +0,0 @@
use shaderc::{Compiler, ShaderKind};
use std::{
fs::{self, File},
io::Write,
path::{Path, PathBuf},
};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Tell Cargo to rerun if shaders directory changes
println!("cargo:rerun-if-changed=../../shaders");
let shader_dir = Path::new("../../shaders");
let cache_dir = Path::new("../../shader-cache");
// Create shader cache directory if it doesn't exist
fs::create_dir_all(cache_dir)?;
let compiler = Compiler::new().expect("Failed to create shader compiler");
// Compile all .vert and .frag files
for entry in fs::read_dir(shader_dir)? {
let entry = entry?;
let path = entry.path();
if let Some(extension) = path.extension() {
let kind = match extension.to_str() {
Some("vert") => ShaderKind::Vertex,
Some("frag") => ShaderKind::Fragment,
_ => continue,
};
let source = fs::read_to_string(&path)?;
let file_name = path.file_name().unwrap().to_str().unwrap();
// Create output path
let spirv_path = cache_dir.join(format!("{}.spv", file_name));
// Check if we need to recompile
if should_compile(&path, &spirv_path) {
println!("Compiling shader: {}", file_name);
let compiled =
compiler.compile_into_spirv(&source, kind, file_name, "main", None)?;
let mut file = File::create(&spirv_path)?;
file.write_all(compiled.as_binary_u8())?;
}
}
}
Ok(())
}
fn should_compile(source_path: &Path, output_path: &PathBuf) -> bool {
// If output doesn't exist, we need to compile
if !output_path.exists() {
return true;
}
// Get modification times
let source_modified = fs::metadata(source_path)
.and_then(|m| m.modified())
.unwrap_or(std::time::SystemTime::UNIX_EPOCH);
let output_modified = fs::metadata(output_path)
.and_then(|m| m.modified())
.unwrap_or(std::time::SystemTime::UNIX_EPOCH);
// Compile if source is newer than output
source_modified > output_modified
}

View file

@ -1,722 +0,0 @@
use std::{
collections::HashSet,
f32::consts::PI,
ffi::CString,
mem::ManuallyDrop,
sync::{Arc, Mutex},
};
use ash::{
extensions::{
ext::DebugUtils,
khr::{Surface, Swapchain},
},
vk::{self, KhrAccelerationStructureFn, KhrDeferredHostOperationsFn, KhrRayTracingPipelineFn},
Device, Entry, Instance,
};
use egui_ash::{
raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle},
winit, App, AppCreator, AshRenderState, CreationContext, HandleRedraw, RunOption, Theme,
};
use gpu_allocator::vulkan::{Allocator, AllocatorCreateDesc};
use renderer::Renderer;
use spirv_std::glam::Vec3;
mod renderer;
mod texture_cache;
struct Game {
entry: Entry,
instance: Instance,
device: Device,
debug_utils_loader: DebugUtils,
debug_messenger: vk::DebugUtilsMessengerEXT,
physical_device: vk::PhysicalDevice,
surface_loader: Surface,
swapchain_loader: Swapchain,
surface: vk::SurfaceKHR,
queue: vk::Queue,
command_pool: vk::CommandPool,
allocator: ManuallyDrop<Arc<Mutex<Allocator>>>,
pub renderer: Renderer,
theme: Theme,
rotate_y: f32,
camera_position: Vec3,
camera_yaw: f32,
camera_pitch: f32,
camera_fov: f32,
right_mouse_pressed: bool,
last_mouse_pos: Option<(f32, f32)>,
mouse_sensitivity: f32,
last_fps_update: std::time::Instant,
frame_count_since_last_update: i32,
current_fps: f32,
bg_color: [f32; 3],
model_color: [f32; 3],
show_profiler: bool,
}
impl App for Game {
fn ui(&mut self, ctx: &egui::Context) {
puffin::GlobalProfiler::lock().new_frame();
let now = std::time::Instant::now();
self.frame_count_since_last_update += 1;
if now.duration_since(self.last_fps_update).as_secs_f32() >= 0.1 {
self.current_fps = self.frame_count_since_last_update as f32
/ now.duration_since(self.last_fps_update).as_secs_f32();
self.frame_count_since_last_update = 0;
self.last_fps_update = now;
}
if self.show_profiler {
puffin_egui::profiler_window(ctx);
}
egui::SidePanel::left("my_side_panel").show(ctx, |ui| {
ui.horizontal(|ui| {
ui.label("Theme");
let id = ui.make_persistent_id("theme_combo_box_side");
egui::ComboBox::from_id_source(id)
.selected_text(format!("{:?}", self.theme))
.show_ui(ui, |ui| {
ui.selectable_value(&mut self.theme, Theme::Dark, "Dark");
ui.selectable_value(&mut self.theme, Theme::Light, "Light");
});
});
ui.separator();
ui.label("Rotate");
ui.add(egui::widgets::Slider::new(
&mut self.rotate_y,
-180.0..=180.0,
));
ui.separator();
ui.label("Camera Position");
ui.horizontal(|ui| {
ui.label("X:");
ui.add(egui::DragValue::new(&mut self.camera_position.x).speed(0.1));
});
ui.horizontal(|ui| {
ui.label("Y:");
ui.add(egui::DragValue::new(&mut self.camera_position.y).speed(0.1));
});
ui.horizontal(|ui| {
ui.label("Z:");
ui.add(egui::DragValue::new(&mut self.camera_position.z).speed(0.1));
});
ui.separator();
ui.label("FOV");
ui.add(egui::widgets::Slider::new(
&mut self.camera_fov,
10.0..=150.0,
));
ui.separator();
ui.label("Mouse Sensitivity");
ui.add(egui::widgets::Slider::new(
&mut self.mouse_sensitivity,
0.05..=4.0,
));
ui.separator();
ui.color_edit_button_rgb(&mut self.bg_color);
ui.color_edit_button_rgb(&mut self.model_color);
ui.separator();
if ui.button("Show Profiler").clicked() {
self.show_profiler = !self.show_profiler;
}
ui.label(format!("FPS: {:.1}", self.current_fps));
});
if !ctx.wants_keyboard_input() {
let movement_speed = 0.1;
let forward = Vec3::new(
self.camera_yaw.sin() * self.camera_pitch.cos(),
self.camera_pitch.sin(),
self.camera_yaw.cos() * self.camera_pitch.cos(),
)
.normalize();
// Calculate right direction
let right = Vec3::new(
(self.camera_yaw + std::f32::consts::FRAC_PI_2).sin(),
0.0,
(self.camera_yaw + std::f32::consts::FRAC_PI_2).cos(),
)
.normalize();
ctx.input(|i| {
if i.key_down(egui::Key::W) {
self.camera_position += forward * movement_speed;
}
if i.key_down(egui::Key::S) {
self.camera_position -= forward * movement_speed;
}
if i.key_down(egui::Key::A) {
self.camera_position -= right * movement_speed;
}
if i.key_down(egui::Key::D) {
self.camera_position += right * movement_speed;
}
});
}
// Handle mouse input for camera rotation
let is_right_mouse_down = ctx.input(|i| i.pointer.secondary_down());
let is_middle_mouse_down = ctx.input(|i| i.pointer.middle_down());
let hover_pos = ctx.input(|i| i.pointer.hover_pos());
// Set cursor visibility based on right mouse button
// if is_right_mouse_down != self.right_mouse_pressed {
// if is_right_mouse_down {
// ctx.send_viewport_cmd(egui::ViewportCommand::CursorVisible(false));
// ctx.send_viewport_cmd(egui::ViewportCommand::CursorGrab(
// egui::CursorGrab::Confined,
// ));
// } else {
// ctx.send_viewport_cmd(egui::ViewportCommand::CursorVisible(true));
// ctx.send_viewport_cmd(egui::ViewportCommand::CursorGrab(egui::CursorGrab::None));
// }
// }
self.right_mouse_pressed = is_right_mouse_down;
match (self.right_mouse_pressed, is_middle_mouse_down) {
(true, false) => {
if let Some(pos) = hover_pos {
if let Some((last_x, last_y)) = self.last_mouse_pos {
let delta_x = pos.x - last_x;
let delta_y = pos.y - last_y;
// Update camera rotation
let rotation_speed = self.mouse_sensitivity / 100.0;
self.camera_yaw += delta_x * rotation_speed;
self.camera_pitch = (self.camera_pitch - delta_y * rotation_speed)
.clamp(-89.0_f32.to_radians(), 89.0_f32.to_radians());
}
self.last_mouse_pos = Some((pos.x, pos.y));
}
}
(false, true) => {
if let Some(pos) = hover_pos {
if let Some((last_x, last_y)) = self.last_mouse_pos {
let delta_x = pos.x - last_x;
let delta_y = pos.y - last_y;
let move_speed = 0.005;
self.camera_position.x -= delta_x * move_speed;
self.camera_position.y += delta_y * move_speed;
}
self.last_mouse_pos = Some((pos.x, pos.y))
}
}
_ => {
self.last_mouse_pos = None;
}
}
match self.theme {
Theme::Dark => ctx.set_visuals(egui::style::Visuals::dark()),
Theme::Light => ctx.set_visuals(egui::style::Visuals::light()),
}
}
fn request_redraw(&mut self, _viewport_id: egui::ViewportId) -> HandleRedraw {
puffin::profile_function!();
HandleRedraw::Handle(Box::new({
let renderer = self.renderer.clone();
let rotate_y = self.rotate_y;
let camera_position = self.camera_position;
let camera_yaw = self.camera_yaw;
let camera_pitch = self.camera_pitch;
let camera_fov = self.camera_fov;
let bg_color = self.bg_color;
let model_color = self.model_color;
move |size, egui_cmd| {
let mut renderer = renderer.inner.lock().unwrap();
renderer.update_camera(camera_position, camera_yaw, camera_pitch, camera_fov);
renderer.update_colors(bg_color.into(), model_color.into());
renderer.render(size.width, size.height, egui_cmd, rotate_y)
}
}))
}
}
impl Drop for Game {
fn drop(&mut self) {
unsafe {
self.device.device_wait_idle().unwrap();
self.renderer.destroy();
self.device.destroy_command_pool(self.command_pool, None);
self.surface_loader.destroy_surface(self.surface, None);
ManuallyDrop::drop(&mut self.allocator);
self.device.destroy_device(None);
if self.debug_messenger != vk::DebugUtilsMessengerEXT::null() {
self.debug_utils_loader
.destroy_debug_utils_messenger(self.debug_messenger, None);
}
self.instance.destroy_instance(None);
}
}
}
struct MyAppCreator;
impl MyAppCreator {
#[cfg(debug_assertions)]
const ENABLE_VALIDATION_LAYERS: bool = true;
#[cfg(not(debug_assertions))]
const ENABLE_VALIDATION_LAYERS: bool = false;
const VALIDATION: [&'static str; 1] = ["VK_LAYER_KHRONOS_validation"];
#[cfg(debug_assertions)]
unsafe extern "system" fn vulkan_debug_utils_callback(
message_severity: vk::DebugUtilsMessageSeverityFlagsEXT,
message_types: vk::DebugUtilsMessageTypeFlagsEXT,
p_callback_data: *const vk::DebugUtilsMessengerCallbackDataEXT,
_p_user_data: *mut std::ffi::c_void,
) -> vk::Bool32 {
let types = match message_types {
vk::DebugUtilsMessageTypeFlagsEXT::GENERAL => "[GENERAL]",
vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE => "[PERFORMANCE]",
vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION => "[VALIDATION]",
_ => panic!("[UNKNOWN]"),
};
let message = std::ffi::CStr::from_ptr((*p_callback_data).p_message);
match message_severity {
vk::DebugUtilsMessageSeverityFlagsEXT::VERBOSE => tracing::trace!("{types}{message:?}"),
vk::DebugUtilsMessageSeverityFlagsEXT::WARNING => tracing::warn!("{types}{message:?}"),
vk::DebugUtilsMessageSeverityFlagsEXT::ERROR => tracing::error!("{types}{message:?}"),
vk::DebugUtilsMessageSeverityFlagsEXT::INFO => tracing::info!("{types}{message:?}"),
_ => tracing::debug!("{types}{message:?}"),
};
vk::FALSE
}
#[cfg(not(debug_assertions))]
unsafe extern "system" fn vulkan_debug_utils_callback(
_message_severity: vk::DebugUtilsMessageSeverityFlagsEXT,
_message_types: vk::DebugUtilsMessageTypeFlagsEXT,
_p_callback_data: *const vk::DebugUtilsMessengerCallbackDataEXT,
_p_user_data: *mut std::ffi::c_void,
) -> vk::Bool32 {
vk::FALSE
}
fn create_entry() -> Entry {
unsafe { Entry::load().unwrap() }
}
fn create_instance(
required_instance_extensions: &[CString],
entry: &Entry,
) -> (Instance, DebugUtils, vk::DebugUtilsMessengerEXT) {
let mut debug_utils_messenger_create_info = vk::DebugUtilsMessengerCreateInfoEXT::builder()
.flags(vk::DebugUtilsMessengerCreateFlagsEXT::empty())
.message_severity(
vk::DebugUtilsMessageSeverityFlagsEXT::WARNING
// | vk::DebugUtilsMessageSeverityFlagsEXT::VERBOSE
// | vk::DebugUtilsMessageSeverityFlagsEXT::INFO
| vk::DebugUtilsMessageSeverityFlagsEXT::ERROR,
)
.message_type(
vk::DebugUtilsMessageTypeFlagsEXT::GENERAL
| vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE
| vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION,
)
.pfn_user_callback(Some(Self::vulkan_debug_utils_callback))
.build();
let app_name = std::ffi::CString::new("egui-winit-ash example simple").unwrap();
let app_info = vk::ApplicationInfo::builder()
.application_name(&app_name)
.application_version(vk::make_api_version(1, 0, 0, 0))
.api_version(vk::API_VERSION_1_2);
let mut extension_names = vec![DebugUtils::name().as_ptr()];
for ext in required_instance_extensions {
let name = ext.as_ptr();
extension_names.push(name);
}
let raw_layer_names = Self::VALIDATION
.iter()
.map(|l| std::ffi::CString::new(*l).unwrap())
.collect::<Vec<_>>();
let layer_names = raw_layer_names
.iter()
.map(|l| l.as_ptr())
.collect::<Vec<*const i8>>();
let instance_create_info = vk::InstanceCreateInfo::builder()
.push_next(&mut debug_utils_messenger_create_info)
.application_info(&app_info)
.enabled_extension_names(&extension_names);
let instance_create_info = if Self::ENABLE_VALIDATION_LAYERS {
instance_create_info.enabled_layer_names(&layer_names)
} else {
instance_create_info
};
let instance = unsafe {
entry
.create_instance(&instance_create_info, None)
.expect("Failed to create instance")
};
// setup debug utils
let debug_utils_loader = DebugUtils::new(entry, &instance);
let debug_messenger = if Self::ENABLE_VALIDATION_LAYERS {
unsafe {
debug_utils_loader
.create_debug_utils_messenger(&debug_utils_messenger_create_info, None)
.expect("Failed to create debug utils messenger")
}
} else {
vk::DebugUtilsMessengerEXT::null()
};
(instance, debug_utils_loader, debug_messenger)
}
fn create_surface_loader(entry: &Entry, instance: &Instance) -> Surface {
Surface::new(entry, instance)
}
fn create_swapchain_loader(instance: &Instance, device: &Device) -> Swapchain {
Swapchain::new(instance, device)
}
fn create_surface(
entry: &Entry,
instance: &Instance,
window: &winit::window::Window,
) -> vk::SurfaceKHR {
unsafe {
ash_window::create_surface(
entry,
instance,
window.raw_display_handle(),
window.raw_window_handle(),
None,
)
.expect("Failed to create surface")
}
}
fn create_physical_device(
instance: &Instance,
surface_loader: &Surface,
surface: vk::SurfaceKHR,
required_device_extensions: &[CString],
) -> (
vk::PhysicalDevice,
vk::PhysicalDeviceProperties,
vk::PhysicalDeviceMemoryProperties,
u32,
) {
let mut queue_family_index: Option<usize> = None;
let (physical_device, phsyical_device_properties, physical_device_memory_properties) = {
let physical_devices = unsafe {
instance
.enumerate_physical_devices()
.expect("Failed to enumerate physical devices")
};
let physical_device = physical_devices.into_iter().find(|physical_device| {
let queue_families = unsafe {
instance.get_physical_device_queue_family_properties(*physical_device)
};
for (i, queue_family) in queue_families.iter().enumerate() {
let mut graphics_queue = false;
let mut present_queue = false;
if queue_family.queue_flags.contains(vk::QueueFlags::GRAPHICS) {
graphics_queue = true;
}
let present_support = unsafe {
surface_loader
.get_physical_device_surface_support(
*physical_device,
i as u32,
surface,
)
.unwrap()
};
if present_support {
present_queue = true;
}
if graphics_queue && present_queue {
queue_family_index = Some(i);
break;
}
}
let is_queue_family_supported = queue_family_index.is_some();
// check device extensions
let device_extensions = unsafe {
instance
.enumerate_device_extension_properties(*physical_device)
.unwrap()
};
let mut device_extensions_name = vec![];
for device_extension in device_extensions {
let name = unsafe {
std::ffi::CStr::from_ptr(device_extension.extension_name.as_ptr())
.to_owned()
};
device_extensions_name.push(name);
}
let mut required_extensions = HashSet::new();
for extension in required_device_extensions.iter() {
required_extensions.insert(extension.to_owned());
}
for extension_name in device_extensions_name {
required_extensions.remove(&extension_name);
}
let is_device_extension_supported = required_extensions.is_empty();
// check swapchain support
let surface_formats = unsafe {
surface_loader
.get_physical_device_surface_formats(*physical_device, surface)
.unwrap()
};
let surface_present_modes = unsafe {
surface_loader
.get_physical_device_surface_present_modes(*physical_device, surface)
.unwrap()
};
let is_swapchain_supported =
!surface_formats.is_empty() && !surface_present_modes.is_empty();
is_queue_family_supported && is_swapchain_supported && is_device_extension_supported
});
let physical_device = physical_device.expect("Failed to get physical device");
let physical_device_memory_properties =
unsafe { instance.get_physical_device_memory_properties(physical_device) };
let physical_device_properties =
unsafe { instance.get_physical_device_properties(physical_device) };
(
physical_device,
physical_device_properties,
physical_device_memory_properties,
)
};
(
physical_device,
phsyical_device_properties,
physical_device_memory_properties,
queue_family_index.unwrap() as u32,
)
}
fn create_device(
instance: &Instance,
physical_device: vk::PhysicalDevice,
queue_family_index: u32,
required_device_extensions: &[CString],
) -> (Device, vk::Queue) {
let queue_priorities = [1.0_f32];
let mut queue_create_infos = vec![];
let queue_create_info = vk::DeviceQueueCreateInfo::builder()
.queue_family_index(queue_family_index)
.queue_priorities(&queue_priorities)
.build();
queue_create_infos.push(queue_create_info);
let physical_device_features = vk::PhysicalDeviceFeatures::builder()
.sampler_anisotropy(true)
.build();
let enable_extension_names = required_device_extensions
.iter()
.map(|s| s.as_ptr())
.collect::<Vec<_>>();
// device create info
let device_create_info = vk::DeviceCreateInfo::builder()
.queue_create_infos(&queue_create_infos)
.enabled_features(&physical_device_features)
.enabled_extension_names(&enable_extension_names);
// create device
let device = unsafe {
instance
.create_device(physical_device, &device_create_info, None)
.expect("Failed to create device")
};
// get device queue
let queue = unsafe { device.get_device_queue(queue_family_index, 0) };
(device, queue)
}
fn create_command_pool(device: &Device, queue_family_index: u32) -> vk::CommandPool {
let command_pool_create_info = vk::CommandPoolCreateInfo::builder()
.flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER)
.queue_family_index(queue_family_index);
unsafe {
device
.create_command_pool(&command_pool_create_info, None)
.expect("Failed to create command pool")
}
}
}
impl AppCreator<Arc<Mutex<Allocator>>> for MyAppCreator {
type App = Game;
fn create(&self, cc: CreationContext) -> (Self::App, AshRenderState<Arc<Mutex<Allocator>>>) {
// create vk objects
let entry = Self::create_entry();
let (instance, debug_utils_loader, debug_messenger) =
Self::create_instance(&cc.required_instance_extensions, &entry);
let surface_loader = Self::create_surface_loader(&entry, &instance);
let surface = Self::create_surface(&entry, &instance, cc.main_window);
let mut req_ext = vec![
KhrDeferredHostOperationsFn::name().to_owned(),
KhrRayTracingPipelineFn::name().to_owned(),
KhrAccelerationStructureFn::name().to_owned(),
];
for ext in &cc.required_device_extensions {
req_ext.push(ext.to_owned());
}
let (
physical_device,
_physical_device_properties,
_physical_device_memory_properties,
queue_family_index,
) = Self::create_physical_device(&instance, &surface_loader, surface, &req_ext);
let (device, queue) = Self::create_device(
&instance,
physical_device,
queue_family_index,
&cc.required_device_extensions,
);
let swapchain_loader = Self::create_swapchain_loader(&instance, &device);
let command_pool = Self::create_command_pool(&device, queue_family_index);
// create allocator
let allocator = {
Allocator::new(&AllocatorCreateDesc {
instance: instance.clone(),
device: device.clone(),
physical_device,
debug_settings: Default::default(),
buffer_device_address: false,
allocation_sizes: Default::default(),
})
.expect("Failed to create allocator")
};
let allocator = Arc::new(Mutex::new(allocator));
// setup context
cc.context.set_visuals(egui::style::Visuals::dark());
let app = Game {
entry,
instance,
device: device.clone(),
debug_utils_loader,
debug_messenger,
physical_device,
surface_loader: surface_loader.clone(),
swapchain_loader: swapchain_loader.clone(),
surface,
queue,
command_pool,
allocator: ManuallyDrop::new(allocator.clone()),
show_profiler: false,
renderer: Renderer::new(
physical_device,
device,
surface_loader,
swapchain_loader,
allocator.clone(),
surface,
queue_family_index,
queue,
command_pool,
1000,
800,
),
theme: if cc.context.style().visuals.dark_mode {
Theme::Dark
} else {
Theme::Light
},
rotate_y: 0.0,
mouse_sensitivity: 0.4,
camera_position: Vec3::new(0.0, 0.0, -5.0),
camera_pitch: 0.,
camera_yaw: 0.,
camera_fov: 90.,
bg_color: Vec3::splat(0.1).into(),
model_color: Vec3::splat(0.8).into(),
last_mouse_pos: None,
right_mouse_pressed: false,
last_fps_update: std::time::Instant::now(),
frame_count_since_last_update: 0,
current_fps: 0.0,
};
let ash_render_state = AshRenderState {
entry: app.entry.clone(),
instance: app.instance.clone(),
physical_device: app.physical_device,
device: app.device.clone(),
surface_loader: app.surface_loader.clone(),
swapchain_loader: app.swapchain_loader.clone(),
queue: app.queue,
queue_family_index,
command_pool: app.command_pool,
allocator: allocator.clone(),
};
(app, ash_render_state)
}
}
fn main() -> std::process::ExitCode {
tracing_subscriber::fmt().pretty().init();
puffin::set_scopes_on(true);
egui_ash::run(
"vulkan",
MyAppCreator,
RunOption {
viewport_builder: Some(
egui::ViewportBuilder::default()
.with_title("vulkan")
.with_inner_size(egui::vec2(1000.0, 800.0)),
),
follow_system_theme: false,
default_theme: Theme::Dark,
..Default::default()
},
)
}

File diff suppressed because it is too large Load diff

View file

@ -1,46 +0,0 @@
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
use ash::Device;
use gpu_allocator::vulkan::Allocator;
use super::renderer::Texture;
pub struct TextureCache {
cache: HashMap<String, Arc<Texture>>,
}
impl TextureCache {
pub fn new() -> Self {
Self {
cache: HashMap::new(),
}
}
pub fn get_or_load_texture(
&mut self,
key: String,
load_fn: impl FnOnce() -> Option<Texture>,
) -> Option<Arc<Texture>> {
if let Some(texture) = self.cache.get(&key) {
Some(Arc::clone(texture))
} else {
load_fn().map(|texture| {
let texture = Arc::new(texture);
self.cache.insert(key, Arc::clone(&texture));
texture
})
}
}
pub fn cleanup(&mut self, device: &Device, allocator: &mut Allocator) {
for (_, texture) in self.cache.drain() {
if let Ok(texture) = Arc::try_unwrap(texture) {
let mut texture = texture;
texture.destroy(device, allocator);
}
}
}
}

View file

@ -18,17 +18,7 @@
nixpkgs,
flake-parts,
...
}: let
# For details on these options, See
# https://github.com/oxalica/rust-overlay?tab=readme-ov-file#cheat-sheet-common-usage-of-rust-bin
#
# Channel of the Rust toolchain (stable or beta).
rustChannel = "nightly";
# Version (latest or specific date/semantic version)
rustVersion = "latest";
# Profile (default or minimal)
rustProfile = "default";
in
}:
flake-parts.lib.mkFlake {inherit inputs;} {
systems = import inputs.systems;
@ -77,6 +67,7 @@
# WINIT_UNIX_BACKEND=wayland
wayland
spirv-tools
spirv-cross
vulkan-loader
];
};

View file

@ -1,134 +0,0 @@
#version 450
layout(location = 0) in vec3 frag_world_position;
layout(location = 1) in vec3 frag_world_normal;
layout(location = 2) in vec2 frag_tex_coord;
struct Material {
vec4 base_color;
float metallic_factor;
float roughness_factor;
vec2 _padding;
};
layout(set = 0, binding = 0) uniform UniformBufferObject {
mat4 model;
mat4 view;
mat4 proj;
vec3 camera_pos;
float _padding;
Material material;
} ubo;
layout(set = 0, binding = 1) uniform sampler2D albedo_map;
layout(set = 0, binding = 2) uniform sampler2D metallic_roughness_map;
layout(set = 0, binding = 3) uniform sampler2D normal_map;
layout(location = 0) out vec4 out_color;
const float PI = 3.14159265359;
// PBR functions
vec3 fresnelSchlick(float cosTheta, vec3 F0) {
return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}
float DistributionGGX(vec3 N, vec3 H, float roughness) {
float a = roughness*roughness;
float a2 = a*a;
float NdotH = max(dot(N, H), 0.0);
float NdotH2 = NdotH*NdotH;
float nom = a2;
float denom = (NdotH2 * (a2 - 1.0) + 1.0);
denom = PI * denom * denom;
return nom / max(denom, 0.0000001);
}
float GeometrySchlickGGX(float NdotV, float roughness) {
float r = (roughness + 1.0);
float k = (r*r) / 8.0;
float nom = NdotV;
float denom = NdotV * (1.0 - k) + k;
return nom / denom;
}
float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) {
float NdotV = max(dot(N, V), 0.0);
float NdotL = max(dot(N, L), 0.0);
float ggx2 = GeometrySchlickGGX(NdotV, roughness);
float ggx1 = GeometrySchlickGGX(NdotL, roughness);
return ggx1 * ggx2;
}
void main() {
// Sample textures
vec4 albedo = texture(albedo_map, frag_tex_coord);
vec2 metallic_roughness = texture(metallic_roughness_map, frag_tex_coord).bg;
vec3 normal = normalize(2.0 * texture(normal_map, frag_tex_coord).rgb - 1.0);
float metallic = metallic_roughness.x * ubo.material.metallic_factor;
float roughness = metallic_roughness.y * ubo.material.roughness_factor;
vec3 N = normalize(normal);
vec3 V = normalize(ubo.camera_pos - frag_world_position);
// Calculate reflectance at normal incidence
vec3 F0 = vec3(0.04);
F0 = mix(F0, albedo.rgb, metallic);
// Light parameters
vec3 light_positions[4] = vec3[](
vec3(5.0, 5.0, 5.0),
vec3(-5.0, 5.0, 5.0),
vec3(5.0, -5.0, 5.0),
vec3(-5.0, -5.0, 5.0)
);
vec3 light_colors[4] = vec3[](
vec3(23.47, 21.31, 20.79),
vec3(23.47, 21.31, 20.79),
vec3(23.47, 21.31, 20.79),
vec3(23.47, 21.31, 20.79)
);
// Reflectance equation
vec3 Lo = vec3(0.0);
for(int i = 0; i < 4; ++i) {
vec3 L = normalize(light_positions[i] - frag_world_position);
vec3 H = normalize(V + L);
float distance = length(light_positions[i] - frag_world_position);
float attenuation = 1.0 / (distance * distance);
vec3 radiance = light_colors[i] * attenuation;
// Cook-Torrance BRDF
float NDF = DistributionGGX(N, H, roughness);
float G = GeometrySmith(N, V, L, roughness);
vec3 F = fresnelSchlick(clamp(dot(H, V), 0.0, 1.0), F0);
vec3 numerator = NDF * G * F;
float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0);
vec3 specular = numerator / max(denominator, 0.001);
vec3 kS = F;
vec3 kD = vec3(1.0) - kS;
kD *= 1.0 - metallic;
float NdotL = max(dot(N, L), 0.0);
Lo += (kD * albedo.rgb / PI + specular) * radiance * NdotL;
}
vec3 ambient = vec3(0.03) * albedo.rgb;
vec3 color = ambient + Lo;
// HDR tonemapping
color = color / (color + vec3(1.0));
// gamma correction
color = pow(color, vec3(1.0/2.2));
out_color = vec4(color, albedo.a);
}

View file

@ -1,32 +0,0 @@
#version 450
// Vertex inputs
layout(location = 0) in vec3 in_pos;
layout(location = 1) in vec3 in_normal;
layout(location = 2) in vec2 in_tex_coord;
// Uniform buffer
layout(set = 0, binding = 0) uniform UniformBufferObject {
mat4 model;
mat4 view;
mat4 proj;
} ubo;
// Vertex outputs
layout(location = 0) out vec3 out_world_position;
layout(location = 1) out vec3 out_world_normal;
layout(location = 2) out vec2 out_tex_coord;
void main() {
// Transform position to world space
vec4 pos = ubo.model * vec4(in_pos, 1.0);
out_world_position = (pos / pos.w).xyz;
// Transform normal to world space
mat3 normal_matrix = transpose(inverse(mat3(ubo.model)));
out_world_normal = normal_matrix * in_normal;
// Calculate clip space position
gl_Position = ubo.proj * ubo.view * pos;
out_tex_coord = in_tex_coord;
}