This commit is contained in:
zack 2025-03-28 16:33:40 -04:00
parent 9cfd9d8b17
commit 8a1c5237d5
No known key found for this signature in database
GPG key ID: EE8A2B709E2401D1
19 changed files with 1952 additions and 175 deletions

View file

@ -4,15 +4,14 @@ version = "0.1.0"
edition = "2021"
[dependencies]
egui.workspace = true
ash.workspace = true
winit.workspace = true
raw-window-handle.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
glam.workspace = true
bytemuck.workspace = true
winit.workspace = true
raw-window-handle.workspace = true
thiserror.workspace = true
gpu-allocator.workspace = true
gfx_hal = { path = "../gfx_hal" }
renderer = { path = "../renderer" }
resource_manager = { path = "../resource_manager" }

View file

@ -1,5 +1,462 @@
mod scene_data;
use std::{
error::Error,
ffi::{CStr, CString},
fs::OpenOptions,
sync::Arc,
time::Instant,
};
fn main() {
println!("Hello, world!");
use ash::vk;
use gfx_hal::{
device::Device, error::GfxHalError, instance::Instance, instance::InstanceConfig,
physical_device::PhysicalDevice, queue::Queue, surface::Surface,
};
use raw_window_handle::HasDisplayHandle;
use renderer::{Renderer, RendererError};
use resource_manager::{ResourceManager, ResourceManagerError};
use tracing::{debug, error, info, warn};
use tracing_subscriber::{filter, layer::SubscriberExt, util::SubscriberInitExt, Layer};
use winit::{
event::{Event, WindowEvent},
event_loop::{ActiveEventLoop, EventLoop},
window::{Window, WindowAttributes},
};
// --- Configuration ---
const WINDOW_TITLE: &str = "Rust Vulkan Egui Engine";
const INITIAL_WIDTH: u32 = 1280;
const INITIAL_HEIGHT: u32 = 720;
const APP_NAME: &str = "My App";
const ENGINE_NAME: &str = "My Engine";
// --- Error Handling ---
#[derive(Debug, thiserror::Error)]
enum AppError {
#[error("Window Creation Error: {0}")]
WindowCreation(#[from] winit::error::OsError),
#[error("Graphics HAL Error: {0}")]
GfxHal(#[from] GfxHalError),
#[error("Resource Manager Error: {0}")]
ResourceManager(#[from] ResourceManagerError),
#[error("Renderer Error: {0}")]
Renderer(#[from] RendererError),
#[error("Suitable physical device not found")]
NoSuitableDevice,
#[error("Required queue family not found")]
NoSuitableQueueFamily,
#[error("Failed to create CString: {0}")]
NulError(#[from] std::ffi::NulError),
#[error("Missing required Vulkan extension: {0}")]
MissingExtension(String),
}
// --- Main Application Structure ---
struct Application {
// Core Vulkan Objects (managed by gfx_hal)
_instance: Arc<Instance>, // Keep instance alive
_physical_device: PhysicalDevice, // Keep info, though Device holds handle
device: Arc<Device>,
graphics_queue: Arc<Queue>,
surface: Arc<Surface>,
// Resource Management
resource_manager: Arc<ResourceManager>,
// Renderer
renderer: Renderer,
// Windowing
window: Arc<Window>, // Use Arc for potential multi-threading later
// State
last_frame_time: Instant,
ui_show_demo: bool,
}
impl Application {
fn new(window: Arc<Window>) -> Result<Self, AppError> {
info!("Initializing Application...");
// --- 1. gfx_hal Setup ---
let instance_extensions = [
// Add extensions required by the platform (e.g., from winit)
// ash::extensions::ext::DebugUtils::name(), // If using validation
ash::khr::surface::NAME,
// Platform specific (example for Xlib/Wayland)
#[cfg(target_os = "linux")]
ash::khr::xlib_surface::NAME,
#[cfg(target_os = "linux")]
ash::khr::wayland_surface::NAME,
// Add other platform extensions as needed (Win32, Metal, etc.)
];
let instance_extensions_c: Vec<CString> = instance_extensions
.iter()
.map(|&s| CString::new(s.to_bytes()).unwrap())
.collect();
let instance_config = InstanceConfig {
application_name: APP_NAME.to_string(),
engine_name: ENGINE_NAME.to_string(),
enable_validation: cfg!(debug_assertions), // Enable validation in debug
..Default::default()
};
let instance = Instance::new(
&instance_config,
&window.display_handle().unwrap(),
&instance_extensions_c, // Pass external extensions
)?;
info!("Vulkan Instance created.");
let surface = unsafe {
// Need unsafe for create_surface
instance.create_surface(window.as_ref())? // Pass window ref
};
info!("Vulkan Surface created.");
// --- 2. Physical Device Selection ---
let required_device_extensions =
[ash::khr::swapchain::NAME, ash::khr::dynamic_rendering::NAME];
let required_device_extensions_cstr: Vec<&CStr> = required_device_extensions
.iter()
.map(|s| CStr::from_bytes_with_nul(s.to_bytes_with_nul()).unwrap())
.collect();
// Define required features (Dynamic Rendering is crucial)
let required_dynamic_rendering_features =
vk::PhysicalDeviceDynamicRenderingFeaturesKHR::default().dynamic_rendering(true);
// Chain other required features if necessary (e.g., mesh shader)
// let mut required_mesh_shader_features = vk::PhysicalDeviceMeshShaderFeaturesEXT::builder()...
// required_dynamic_rendering_features = required_dynamic_rendering_features.push_next(&mut required_mesh_shader_features);
let physical_devices = unsafe { instance.enumerate_physical_devices()? };
let (physical_device, queue_family_indices) = physical_devices
.into_iter()
.find_map(|pd| {
match find_suitable_device_and_queues(
&pd,
&surface,
&required_device_extensions_cstr,
&required_dynamic_rendering_features,
) {
Ok(indices) => Some((pd, indices)),
Err(e) => {
warn!(
"Skipping physical device {:?}: {}",
unsafe {
instance
.ash_instance()
.get_physical_device_properties(pd.handle())
.device_name_as_c_str()
},
e
);
None
}
}
})
.ok_or(AppError::NoSuitableDevice)?;
let pd_props = unsafe {
instance
.ash_instance()
.get_physical_device_properties(physical_device.handle())
};
info!(
"Selected Physical Device: {}",
pd_props.device_name_as_c_str().unwrap().to_string_lossy()
);
debug!("Using Queue Families: {:?}", queue_family_indices);
// --- 3. Logical Device and Queues ---
// Enable required features
let enabled_features = vk::PhysicalDeviceFeatures::default(); // Add base features if needed
let enabled_buffer_device_address =
vk::PhysicalDeviceBufferDeviceAddressFeatures::default().buffer_device_address(true);
let enabled_dynamic_rendering = required_dynamic_rendering_features; // Copy the builder state
let device = unsafe {
// Need unsafe for create_logical_device
physical_device.create_logical_device(
&required_device_extensions_cstr,
&queue_family_indices,
&enabled_features,
None,
&enabled_dynamic_rendering, // Pass features to enable
&enabled_buffer_device_address,
)?
};
let device_handle_at_creation = device.raw().handle();
info!(
"App: Created Device handle: {:?}",
device_handle_at_creation
);
// Get specific queues (assuming graphics and present are the same for simplicity)
let graphics_queue = device.get_graphics_queue();
let queue_associated_device_handle = graphics_queue.device().raw().handle();
info!(
"App: Queue is associated with Device handle: {:?}",
queue_associated_device_handle
);
assert_eq!(
device_handle_at_creation, queue_associated_device_handle,
"Device handle mismatch immediately after queue creation!"
);
// --- 4. Resource Manager ---
let resource_manager = Arc::new(ResourceManager::new(instance.clone(), device.clone())?);
info!("Resource Manager initialized.");
let renderer_device_handle_to_pass = device.raw().handle();
let renderer_queue_device_handle_to_pass = graphics_queue.device().raw().handle();
info!(
"App: Passing Device handle to Renderer: {:?}",
renderer_device_handle_to_pass
);
info!(
"App: Passing Queue associated with Device handle: {:?}",
renderer_queue_device_handle_to_pass
);
// --- 5. Renderer ---
let initial_size = window.inner_size();
let renderer = Renderer::new(
instance.clone(), // Pass instance for allocator creation
device.clone(),
graphics_queue.clone(),
surface.clone(),
resource_manager.clone(),
initial_size.width,
initial_size.height,
)?;
info!("Renderer initialized.");
Ok(Self {
_instance: instance,
_physical_device: physical_device,
device,
graphics_queue,
surface,
resource_manager,
renderer,
window,
last_frame_time: Instant::now(),
ui_show_demo: true,
})
}
fn handle_event(&mut self, event: &Event<()>, active_event_loop: &ActiveEventLoop) {
match event {
Event::WindowEvent { event, window_id } if *window_id == self.window.id() => {
match event {
WindowEvent::CloseRequested => {
info!("Close requested. Exiting...");
active_event_loop.exit();
}
WindowEvent::Resized(physical_size) => {
info!(
"Window resized to: {}x{}",
physical_size.width, physical_size.height
);
// Important: Resize renderer *before* the next frame
self.renderer
.resize(physical_size.width, physical_size.height);
// Egui also needs the new screen descriptor info, though
// egui_winit_state might handle this internally via on_window_event.
// Explicitly setting it might be safer depending on version.
// self.egui_winit_state.set_max_size_points(...) // If needed
}
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
info!("Scale factor changed: {}", scale_factor);
// May also need to resize renderer if size depends on scale factor
let new_inner_size = self.window.inner_size();
self.renderer
.resize(new_inner_size.width, new_inner_size.height);
}
// Handle other inputs if not consumed by egui
WindowEvent::KeyboardInput { .. }
| WindowEvent::CursorMoved { .. }
| WindowEvent::MouseInput { .. } => {}
_ => {}
}
}
// Event::MainEventsCleared => { // Use AboutToWait for newer winit
// // Application update code.
// self.window.request_redraw();
// }
Event::AboutToWait => {
// Application update code and redraw request.
// This is the main place to prepare and trigger rendering.
let now = Instant::now();
let _delta_time = now.duration_since(self.last_frame_time);
self.last_frame_time = now;
// --- Render Frame ---
match self.renderer.render_frame() {
Ok(_) => {}
Err(RendererError::SwapchainSuboptimal) => {
// Swapchain is suboptimal, recreate it next frame by triggering resize
warn!("Swapchain suboptimal, forcing resize.");
let size = self.window.inner_size();
self.renderer.resize(size.width, size.height);
}
Err(e) => {
error!("Failed to render frame: {}", e);
// Decide how to handle persistent errors (e.g., exit)
active_event_loop.exit();
}
}
}
Event::LoopExiting => {
info!("Event loop exiting. Cleaning up...");
// Wait for GPU to finish before dropping resources
if let Err(e) = self.device.wait_idle() {
error!("Error waiting for device idle on exit: {}", e);
}
info!("GPU idle. Cleanup complete.");
}
_ => {}
}
}
}
// --- Helper Functions ---
/// Finds queue family indices for graphics and presentation.
fn find_suitable_device_and_queues(
physical_device: &PhysicalDevice,
surface: &Surface,
required_extensions: &[&CStr],
required_dynamic_rendering_features: &vk::PhysicalDeviceDynamicRenderingFeaturesKHR,
) -> Result<gfx_hal::physical_device::QueueFamilyIndices, Box<dyn Error>> {
// 1. Check Extension Support
let supported_extensions = unsafe {
physical_device
.instance()
.ash_instance()
.enumerate_device_extension_properties(physical_device.handle())?
};
let supported_extension_names: std::collections::HashSet<&CStr> = supported_extensions
.iter()
.map(|ext| unsafe { CStr::from_ptr(ext.extension_name.as_ptr()) })
.collect();
for &required in required_extensions {
if !supported_extension_names.contains(required) {
return Err(
format!("Missing required extension: {}", required.to_string_lossy()).into(),
);
}
}
// 2. Check Feature Support (Dynamic Rendering)
let mut dynamic_rendering_features = vk::PhysicalDeviceDynamicRenderingFeaturesKHR::default();
let mut features2 =
vk::PhysicalDeviceFeatures2::default().push_next(&mut dynamic_rendering_features);
unsafe {
physical_device
.instance()
.ash_instance()
.get_physical_device_features2(physical_device.handle(), &mut features2);
}
if dynamic_rendering_features.dynamic_rendering == vk::FALSE {
return Err("Dynamic Rendering feature not supported".into());
}
// Add checks for other required features here...
// 3. Check Queue Family Support
let queue_family_properties = unsafe {
physical_device
.instance()
.ash_instance()
.get_physical_device_queue_family_properties(physical_device.handle())
};
let mut graphics_family = None;
let mut present_family = None;
for (i, queue_family) in queue_family_properties.iter().enumerate() {
let index = i as u32;
// Check for graphics support
if queue_family.queue_flags.contains(vk::QueueFlags::GRAPHICS) {
graphics_family = Some(index);
}
// Check for presentation support
let present_support = unsafe {
surface
.surface_loader()
.get_physical_device_surface_support(
physical_device.handle(),
index,
surface.handle(),
)?
};
if present_support {
present_family = Some(index);
}
if graphics_family.is_some() && present_family.is_some() {
break; // Found suitable families
}
}
match (graphics_family, present_family) {
(Some(graphics), Some(present)) => Ok(gfx_hal::physical_device::QueueFamilyIndices {
graphics_family: Some(graphics),
present_family: Some(present), // Could be the same as graphics
compute_family: None, // Not needed for this example
transfer_family: None, // Not needed for this example
}),
_ => Err("Could not find suitable queue families".into()),
}
}
// --- Entry Point ---
fn main() -> Result<(), Box<dyn Error>> {
let fmt_layer = tracing_subscriber::fmt::layer()
.with_ansi(true)
.with_file(false)
.with_line_number(false)
.without_time();
let log_file = OpenOptions::new()
.append(true)
.create(true)
.open("log-debug.log")?;
let json_layer = tracing_subscriber::fmt::layer()
.json()
.with_writer(log_file)
.with_filter(filter::LevelFilter::TRACE);
tracing_subscriber::registry()
.with(fmt_layer)
.with(json_layer)
.init();
// --- Winit Setup ---
let event_loop = EventLoop::new()?;
let window = Arc::new(event_loop.create_window(WindowAttributes::default())?);
info!("Window created.");
// --- Application Setup ---
let mut app = Application::new(window.clone())?;
// --- Event Loop ---
info!("Starting event loop...");
event_loop.run(move |event, elwt| {
// elwt is EventLoopWindowTarget, not needed directly here often
app.handle_event(&event, elwt);
})?;
Ok(())
}

View file

@ -1,67 +0,0 @@
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)
}

View file

@ -1,6 +1,8 @@
use ash::vk;
use parking_lot::Mutex;
use std::collections::HashSet;
use std::ffi::CStr;
use std::sync::Weak;
use std::{collections::HashMap, sync::Arc};
use crate::error::{GfxHalError, Result};
@ -23,12 +25,13 @@ pub struct Device {
impl Device {
/// Creates a new logical device. Typically called via `PhysicalDevice::create_logical_device`.
/// Uses a two-stage initialization to avoid Arc::new_cyclic issues.
///
/// # Saftey
/// # Safety
/// - `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`.
/// - All feature structs passed must be supported by the `physical_device_handle`.
pub(crate) unsafe fn new(
instance: Arc<Instance>,
physical_device_handle: vk::PhysicalDevice,
@ -36,22 +39,26 @@ impl Device {
required_extensions: &[&CStr],
enabled_features: &vk::PhysicalDeviceFeatures,
mesh_features: Option<&vk::PhysicalDeviceMeshShaderFeaturesEXT>,
dynamic_rendering_features: &vk::PhysicalDeviceDynamicRenderingFeatures,
buffer_device_address_features: &vk::PhysicalDeviceBufferDeviceAddressFeatures,
// Add other feature structs here as needed...
) -> Result<Arc<Self>> {
// --- 1. Prepare Queue Create Infos (Same as before) ---
let mut queue_create_infos = Vec::new();
let mut unique_queue_families = std::collections::HashSet::new();
let mut unique_queue_families = 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);
}
if let Some(present_family) = queue_family_indicies.present_family {
unique_queue_families.insert(present_family);
}
let queue_priorities = [1.0f32];
for &family_index in &unique_queue_families {
let queue_create_info = vk::DeviceQueueCreateInfo::default()
@ -60,74 +67,94 @@ impl Device {
queue_create_infos.push(queue_create_info);
}
// --- 2. Prepare Feature Chain (Same as before) ---
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 mut dyn_rendering_feats_copy = *dynamic_rendering_features;
if dyn_rendering_feats_copy.dynamic_rendering != vk::TRUE {
return Err(GfxHalError::MissingFeature("Dynamic Rendering".to_string()));
}
features2 = features2.push_next(&mut dyn_rendering_feats_copy);
let mut bda_features_copy = *buffer_device_address_features;
if bda_features_copy.buffer_device_address != vk::TRUE {
return Err(GfxHalError::MissingFeature(
"Buffer Device Address".to_string(),
));
}
features2 = features2.push_next(&mut bda_features_copy);
// Chain other features here...
// --- 3. Create the SINGLE ash::Device (Same as before) ---
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(
let ash_device = instance.ash_instance().create_device(
physical_device_handle,
&device_create_info,
None,
)?;
tracing::info!("logical device created successfully.");
tracing::info!(
"Logical device created successfully (ash::Device handle: {:?}).",
ash_device.handle()
);
let mut queues_map = HashMap::new();
let arc_device_placeholder = Arc::new(Self {
instance,
// --- 4. Create the Device struct in an Arc (Stage 1) ---
// Initialize the queues map as empty for now.
let device_arc = Arc::new(Device {
instance: instance.clone(),
physical_device: physical_device_handle,
device,
queues: Mutex::new(HashMap::new()),
device: ash_device, // Move the created ash::Device here
queues: Mutex::new(HashMap::new()), // Start with empty 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,
});
tracing::debug!(
"Device Arc created (Stage 1) with ash::Device handle: {:?}",
device_arc.raw().handle()
);
// --- 5. Create Queues and Populate Map (Stage 2) ---
// Now that we have the final Arc<Device>, we can create the Queues.
let mut queues_to_insert = HashMap::new();
for &family_index in &unique_queue_families {
let queue_handler = arc_device_placeholder
.device
.get_device_queue(family_index, 0);
// Get the Vulkan queue handle using the device stored in the Arc
// Assuming queue index 0 for simplicity
let vk_queue_handle = device_arc.device.get_device_queue(family_index, 0);
// Create the Queue wrapper, passing a clone of the device_arc
let queue_wrapper = Arc::new(Queue::new(
Arc::clone(&arc_device_placeholder),
queue_handler,
device_arc.clone(), // Pass the Arc<Device>
vk_queue_handle,
family_index,
));
queues_map.insert((family_index, 0), queue_wrapper);
queues_to_insert.insert((family_index, 0), queue_wrapper);
tracing::trace!("Created queue wrapper for family {}", family_index);
}
let device_handle = unsafe {
arc_device_placeholder
.instance
.ash_instance()
.create_device(physical_device_handle, &device_create_info, None)?
};
// Lock the mutex and insert the created queues into the map within the Arc<Device>
{
// Scope for the mutex guard
let mut queues_map_guard = device_arc.queues.lock();
*queues_map_guard = queues_to_insert; // Replace the empty map with the populated one
tracing::debug!(
"Device Arc populated with {} queues (Stage 2).",
queues_map_guard.len()
);
} // Mutex guard is dropped here
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)
Ok(device_arc) // Return the fully initialized Arc<Device>
}
/// Provides raw access to the underlying `ash::Device`.
@ -210,6 +237,8 @@ impl PhysicalDevice {
queue_family_indices: &QueueFamilyIndices,
enabled_features: &vk::PhysicalDeviceFeatures,
mesh_features: Option<&vk::PhysicalDeviceMeshShaderFeaturesEXT>,
dynamic_rendering_features: &vk::PhysicalDeviceDynamicRenderingFeatures,
buffer_device_address_features: &vk::PhysicalDeviceBufferDeviceAddressFeatures,
) -> Result<Arc<Device>> {
Device::new(
Arc::clone(self.instance()),
@ -218,6 +247,8 @@ impl PhysicalDevice {
required_extensions,
enabled_features,
mesh_features,
dynamic_rendering_features,
buffer_device_address_features,
)
}
}

View file

@ -21,8 +21,8 @@ pub enum GfxHalError {
MissingExtension(String),
/// A required Vulkan feature is not supported by the physical device.
#[error("Missing required Vulkan feature.")]
MissingFeature,
#[error("Missing required Vulkan feature: {0}")]
MissingFeature(String),
/// Failed to find a suitable queue family (e.g., graphics, present).
#[error("Could not find required queue family: {0}")]

View file

@ -5,9 +5,13 @@ use std::{
};
use ash::{ext::debug_utils, vk};
use winit::raw_window_handle::{DisplayHandle, HasDisplayHandle};
use winit::raw_window_handle::{DisplayHandle, HasDisplayHandle, HasWindowHandle};
use crate::error::{GfxHalError, Result};
use crate::{
error::{GfxHalError, Result},
physical_device::PhysicalDevice,
surface::Surface,
};
unsafe extern "system" fn vulkan_debug_callback(
message_severity: vk::DebugUtilsMessageSeverityFlagsEXT,
@ -303,39 +307,39 @@ impl Instance {
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)
// }
/// 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_physical_devices(self: &Arc<Self>) -> Result<Vec<PhysicalDevice>> {
let physical_device_handles = self.instance.enumerate_physical_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)
// }
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 {

View file

@ -2,7 +2,7 @@ use ash::vk;
use crate::{error::GfxHalError, instance::Instance};
use std::{ffi::CStr, sync::Arc};
use std::sync::Arc;
/// Represents a physical Vulkan device (GPU).
///
@ -21,7 +21,7 @@ pub struct PhysicalDevice {
}
/// Holds information about queue families found on a `PhysicalDevice`.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Default)]
pub struct QueueFamilyIndices {
/// Queue family index supporting graphics operations.
pub graphics_family: Option<u32>,
@ -100,16 +100,23 @@ impl PhysicalDevice {
) -> (
vk::PhysicalDeviceFeatures,
vk::PhysicalDeviceMeshShaderFeaturesEXT,
vk::PhysicalDeviceDynamicRenderingFeatures,
) {
let mut mesh_shader_features = vk::PhysicalDeviceMeshShaderFeaturesEXT::default();
let mut features2 =
vk::PhysicalDeviceFeatures2::default().push_next(&mut mesh_shader_features);
let mut dynamic_rendering_features = vk::PhysicalDeviceDynamicRenderingFeatures::default();
let mut features2 = vk::PhysicalDeviceFeatures2::default()
.push_next(&mut mesh_shader_features)
.push_next(&mut dynamic_rendering_features);
self.instance
.ash_instance()
.get_physical_device_features2(self.handle, &mut features2);
(features2.features, mesh_shader_features)
(
features2.features,
mesh_shader_features,
dynamic_rendering_features,
)
}
/// Queries the properties of all queue families available on the device.

View file

@ -1,6 +1,6 @@
use std::sync::Arc;
use ash::vk;
use ash::{vk, Device as AshDevice};
use parking_lot::Mutex;
use crate::device::Device;
@ -46,25 +46,41 @@ impl Queue {
&self.device
}
/// Submits command buffers to the queue.
/// Submits command buffers to the queue using the provided device handle.
///
/// 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
/// * `submit_device_raw` - The `ash::Device` handle corresponding to the device that owns the resources in `submits` and the `signal_fence`.
/// * `submits` - A slice of `vk::SubmitInfo` describing the work to submit.
/// * `signal_fence` - An optional `Fence` to signal when the submission completes.
/// * `signal_fence` - An optional `Fence` to signal when the submission completes. The fence must have been created with the same logical device as `submit_device_raw`.
///
/// # Safety
/// - The command buffers and synchronization primitieves within `submits` must be valid.
/// - The `signal_fence`, if provided, must be valid and unsignaled.
/// - `submit_device_raw` must be the correct, valid `ash::Device` handle associated with the resources being submitted.
/// - The command buffers and synchronization primitives within `submits` must be valid and owned by the same logical device as `submit_device_raw`.
/// - The `signal_fence`, if provided, must be valid, unsignaled, and owned by the same logical device as `submit_device_raw`.
pub unsafe fn submit(
&self,
submit_device_raw: &AshDevice, // <<< Accept the ash::Device to use
submits: &[vk::SubmitInfo],
signal_fence: Option<&Fence>,
) -> Result<()> {
debug_assert!(
self.device.raw().handle() == submit_device_raw.handle(),
"Queue::submit called with an ash::Device from a different logical VkDevice than the queue belongs to!"
);
// Optional: Check fence device consistency
if let Some(fence) = signal_fence {
debug_assert!(
fence.device().raw().handle() == submit_device_raw.handle(),
"Fence passed to Queue::submit belongs to a different logical device than submit_device_raw!"
);
}
let fence_handle = signal_fence.map_or(vk::Fence::null(), |f| f.handle());
// Keep the lock for thread-safety on the VkQueue object itself
let _lock = self.submit_lock.lock();
tracing::trace!(
@ -72,9 +88,10 @@ impl Queue {
submits.len(),
self.family_index
);
self.device
.raw()
.queue_submit(self.queue, submits, fence_handle)?;
// Use the EXPLICITLY PASSED submit_device_raw for the Vulkan call
submit_device_raw.queue_submit(self.queue, submits, fence_handle)?;
tracing::trace!("Submission successful.");
Ok(())
}

View file

@ -3,9 +3,9 @@ use std::sync::Arc;
use ash::khr::swapchain::Device as SwapchainLoader;
use ash::vk;
use crate::device::{self, Device};
use crate::device::Device;
use crate::error::{GfxHalError, Result};
use crate::surface::{self, Surface};
use crate::surface::Surface;
use crate::sync::{Fence, Semaphore};
/// Configuration for creating or recreating a `Swapchain`.

View file

@ -33,6 +33,11 @@ impl Fence {
Ok(Self { device, fence })
}
/// Returns the device used by the fence.
pub fn device(&self) -> &Arc<Device> {
&self.device
}
/// Waits for the fence to become signaled.
///
/// # Arguments

View file

@ -0,0 +1,24 @@
[package]
name = "renderer"
version = "0.1.0"
edition = "2021"
[dependencies]
ash.workspace = true
tracing.workspace = true
thiserror.workspace = true
glam.workspace = true
bytemuck.workspace = true
gpu-allocator.workspace = true
egui.workspace = true
egui-ash-renderer.workspace = true
winit.workspace = true
parking_lot.workspace = true
gfx_hal = { path = "../gfx_hal" }
resource_manager = { path = "../resource_manager" }
[build-dependencies]
shaderc = "0.9.1"
walkdir = "2"
anyhow = "1.0"

152
crates/renderer/build.rs Normal file
View file

@ -0,0 +1,152 @@
use anyhow::{Context, Result};
use shaderc::{CompileOptions, Compiler, ShaderKind};
use std::{
env,
fs::{self, File},
io::Write,
path::PathBuf,
};
use walkdir::WalkDir;
// Configuration
const SHADER_SOURCE_DIR: &str = "../../shaders"; // Directory containing GLSL shaders
// Output directory will be determined by Cargo (OUT_DIR)
fn main() -> Result<()> {
let out_dir = PathBuf::from(env::var("OUT_DIR")?).join("shaders"); // Put shaders in a subdirectory for clarity
fs::create_dir_all(&out_dir).context("Failed to create shader output directory")?;
let compiler = Compiler::new().context("Failed to create shader compiler")?;
let mut options = CompileOptions::new().context("Failed to create compile options")?;
// --- Optional: Add compile options ---
// Example: Optimize for performance in release builds
if env::var("PROFILE")? == "release" {
options.set_optimization_level(shaderc::OptimizationLevel::Performance);
eprintln!("Build.rs: Compiling shaders with Performance optimization.");
} else {
options.set_optimization_level(shaderc::OptimizationLevel::Zero); // Faster compile for debug
options.set_generate_debug_info(); // Include debug info for debug builds
eprintln!("Build.rs: Compiling shaders with Zero optimization and Debug info.");
}
// Add other options like defines if needed:
// options.add_macro_definition("MY_DEFINE", Some("1"));
options.set_target_env(
shaderc::TargetEnv::Vulkan,
shaderc::EnvVersion::Vulkan1_3 as u32,
); // Specify Vulkan version if needed
eprintln!(
"Build.rs: Compiling shaders from '{}' to '{}'",
SHADER_SOURCE_DIR,
out_dir.display()
);
// --- Find and Compile Shaders ---
for entry in WalkDir::new(SHADER_SOURCE_DIR)
.into_iter()
.filter_map(|e| e.ok()) // Ignore directory reading errors
.filter(|e| e.file_type().is_file())
// Only process files
{
let in_path = entry.path();
// Determine shader kind from extension
let extension = match in_path.extension().and_then(|s| s.to_str()) {
Some(ext) => ext,
None => {
eprintln!(
"cargo:warning=Skipping file with no extension: {}",
in_path.display()
);
continue; // Skip files without extensions
}
};
let shader_kind = match extension {
"vert" => ShaderKind::Vertex,
"frag" => ShaderKind::Fragment,
"comp" => ShaderKind::Compute,
"geom" => ShaderKind::Geometry,
"tesc" => ShaderKind::TessControl,
"tese" => ShaderKind::TessEvaluation,
// Add other shader kinds if needed (ray tracing, mesh, etc.)
_ => {
eprintln!(
"cargo:warning=Skipping file with unknown shader extension ({}): {}",
extension,
in_path.display()
);
continue; // Skip unknown shader types
}
};
let source_text = fs::read_to_string(in_path)
.with_context(|| format!("Failed to read shader source: {}", in_path.display()))?;
let input_file_name = in_path.to_string_lossy(); // For error messages
// Compile the shader
let compiled_spirv = compiler
.compile_into_spirv(
&source_text,
shader_kind,
&input_file_name, // Source file name for errors
"main", // Entry point function name
Some(&options), // Pass compile options
)
.with_context(|| format!("Failed to compile shader: {}", input_file_name))?;
let spirv_bytes = compiled_spirv.as_binary_u8();
let byte_count = spirv_bytes.len();
eprintln!(
"Build.rs: SPIR-V for {} has {} bytes.",
input_file_name, byte_count
);
// Check if it's a multiple of 4 right here
if byte_count % 4 != 0 {
eprintln!(
"cargo:warning=Byte count for {} ({}) is NOT a multiple of 4!",
input_file_name, byte_count
);
// Optionally bail out here:
// bail!("Generated SPIR-V for {} has invalid byte count {}", input_file_name, byte_count);
}
// Check for warnings
if compiled_spirv.get_num_warnings() > 0 {
eprintln!(
"cargo:warning=Shader compilation warnings for {}:\n{}",
input_file_name,
compiled_spirv.get_warning_messages()
);
}
// Determine output path
let out_filename = format!(
"{}.spv",
in_path
.file_stem() // Get filename without extension
.unwrap_or_default() // Handle potential weird filenames
.to_string_lossy()
);
let out_path = out_dir.join(out_filename);
// Determine output path...
// ...
// Write the compiled SPIR-V binary
let mut outfile = File::create(&out_path)
.with_context(|| format!("Failed to create output file: {}", out_path.display()))?;
outfile
.write_all(spirv_bytes) // Use the stored bytes
.with_context(|| format!("Failed to write SPIR-V to: {}", out_path.display()))?;
eprintln!(
"Build.rs: Compiled {} -> {}",
in_path.display(),
out_path.display()
);
}
eprintln!("Build.rs: Shader compilation finished.");
Ok(())
}

927
crates/renderer/src/lib.rs Normal file
View file

@ -0,0 +1,927 @@
use std::{ffi::CStr, sync::Arc};
use ash::vk;
use gfx_hal::{
device::Device, error::GfxHalError, queue::Queue, surface::Surface, swapchain::Swapchain,
swapchain::SwapchainConfig, sync::Fence, sync::Semaphore,
};
use gpu_allocator::{vulkan::Allocator, MemoryLocation};
use parking_lot::Mutex;
use resource_manager::{ImageHandle, ResourceManager, ResourceManagerError};
use thiserror::Error;
use tracing::{debug, error, info, warn};
// Assuming winit is used by the app
// Re-export ash for convenience if needed elsewhere
pub use ash;
const MAX_FRAMES_IN_FLIGHT: usize = 2;
#[derive(Debug, Error)]
pub enum RendererError {
#[error("Graphics HAL Error: {0}")]
GfxHal(#[from] GfxHalError),
#[error("Resource Manager Error: {0}")]
ResourceManager(#[from] ResourceManagerError),
#[error("Egui Ash Renderer Error: {0}")]
EguiRenderer(#[from] egui_ash_renderer::RendererError),
#[error("Vulkan Error: {0}")]
Vulkan(#[from] vk::Result),
#[error("Failed to create shader module: {0}")]
ShaderCreation(vk::Result),
#[error("Failed to create pipeline layout: {0}")]
PipelineLayoutCreation(vk::Result),
#[error("Failed to create graphics pipeline: {0}")]
PipelineCreation(vk::Result),
#[error("Failed to create command pool: {0}")]
CommandPoolCreation(vk::Result),
#[error("Failed to allocate command buffers: {0}")]
CommandBufferAllocation(vk::Result),
#[error("Failed to begin command buffer: {0}")]
CommandBufferBegin(vk::Result),
#[error("Failed to end command buffer: {0}")]
CommandBufferEnd(vk::Result),
#[error("Swapchain acquisition failed")]
SwapchainAcquisitionFailed,
#[error("Swapchain is suboptimal")]
SwapchainSuboptimal,
#[error("Window reference is missing")] // If using raw-window-handle directly
MissingWindow,
#[error("Failed to get image info from resource manager")]
ImageInfoUnavailable,
#[error("Failed to get allocator from resource manager")]
AllocatorUnavailable, // Added based on egui requirement
}
struct FrameData {
command_pool: vk::CommandPool,
command_buffer: vk::CommandBuffer,
image_available_semaphore: Semaphore,
render_finished_semaphore: Semaphore,
in_flight_fence: Fence,
}
struct SwapchainSupportDetails {
capabilities: vk::SurfaceCapabilitiesKHR,
formats: Vec<vk::SurfaceFormatKHR>,
present_modes: Vec<vk::PresentModeKHR>,
}
pub struct Renderer {
device: Arc<Device>,
graphics_queue: Arc<Queue>,
resource_manager: Arc<ResourceManager>,
allocator: Arc<Mutex<Allocator>>, // Need direct access for egui
surface: Arc<Surface>, // Keep surface for recreation
swapchain: Option<Swapchain>, // Option<> because it's recreated
swapchain_image_views: Vec<vk::ImageView>,
swapchain_format: vk::SurfaceFormatKHR,
swapchain_extent: vk::Extent2D,
depth_image_handle: ImageHandle,
depth_image_view: vk::ImageView, // Store the view directly
depth_format: vk::Format,
triangle_pipeline_layout: vk::PipelineLayout,
triangle_pipeline: vk::Pipeline,
frames_data: Vec<FrameData>,
current_frame: usize,
// Window state tracking (needed for recreation)
window_resized: bool,
current_width: u32,
current_height: u32,
}
impl Renderer {
pub fn new(
instance: Arc<gfx_hal::instance::Instance>, // Needed for allocator
device: Arc<Device>,
graphics_queue: Arc<Queue>,
surface: Arc<Surface>,
resource_manager: Arc<ResourceManager>,
initial_width: u32,
initial_height: u32,
) -> Result<Self, RendererError> {
info!("Initializing Renderer...");
let allocator = resource_manager.allocator();
let (swapchain, format, extent, image_views) = Self::create_swapchain_and_views(
&device,
&surface,
initial_width,
initial_height,
None, // No old swapchain initially
)?;
let depth_format = Self::find_depth_format(&instance, &device)?;
let (depth_image_handle, depth_image_view) =
Self::create_depth_resources(&device, &resource_manager, extent, depth_format)?;
let (triangle_pipeline_layout, triangle_pipeline) =
Self::create_triangle_pipeline(&device, format.format, depth_format)?;
let frames_data = Self::create_frame_data(&device)?;
info!("Renderer initialized successfully.");
Ok(Self {
device,
graphics_queue,
resource_manager,
allocator, // Store the allocator Arc
surface,
swapchain: Some(swapchain),
swapchain_image_views: image_views,
swapchain_format: format,
swapchain_extent: extent,
depth_image_handle,
depth_image_view,
depth_format,
triangle_pipeline_layout,
triangle_pipeline,
frames_data,
current_frame: 0,
window_resized: false,
current_width: initial_width,
current_height: initial_height,
})
}
pub fn resize(&mut self, width: u32, height: u32) {
if width > 0 && height > 0 {
self.window_resized = true;
self.current_width = width;
self.current_height = height;
debug!("Window resize requested to {}x{}", width, height);
} else {
debug!("Ignoring resize to 0 dimensions");
}
}
pub fn render_frame(&mut self) -> Result<(), RendererError> {
// --- Handle Resize ---
if self.window_resized {
self.window_resized = false;
debug!("Executing resize...");
self.recreate_swapchain()?;
// Skip rendering this frame as swapchain is new
return Ok(());
}
// --- Wait for Previous Frame ---
let frame_index = self.current_frame;
let frame_data = &self.frames_data[frame_index];
frame_data.in_flight_fence.wait(None)?; // Wait indefinitely
// --- Acquire Swapchain Image ---
let (image_index, suboptimal) = unsafe {
// Need unsafe block for acquire_next_image
self.swapchain
.as_ref()
.ok_or(RendererError::SwapchainAcquisitionFailed)? // Should exist
.acquire_next_image(
u64::MAX, // Timeout
Some(&frame_data.image_available_semaphore),
None, // Don't need a fence here
)?
};
if suboptimal {
warn!("Swapchain is suboptimal, scheduling recreation.");
self.window_resized = true; // Trigger recreation next frame
// Reset fence *before* returning, otherwise we deadlock next frame
frame_data.in_flight_fence.reset()?;
return Ok(()); // Skip rendering
}
// --- Reset Fence (only after successful acquisition) ---
frame_data.in_flight_fence.reset()?;
// --- Record Command Buffer ---
unsafe {
// Need unsafe for Vulkan commands
self.device
.raw()
.reset_command_pool(frame_data.command_pool, vk::CommandPoolResetFlags::empty())?;
}
let command_buffer = frame_data.command_buffer;
let cmd_begin_info = vk::CommandBufferBeginInfo::default()
.flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT);
unsafe {
// Need unsafe for Vulkan commands
self.device
.raw()
.begin_command_buffer(command_buffer, &cmd_begin_info)?;
}
// --- Dynamic Rendering Setup ---
let color_attachment = vk::RenderingAttachmentInfo::default()
.image_view(self.swapchain_image_views[image_index as usize])
.image_layout(vk::ImageLayout::ATTACHMENT_OPTIMAL)
.load_op(vk::AttachmentLoadOp::CLEAR)
.store_op(vk::AttachmentStoreOp::STORE)
.clear_value(vk::ClearValue {
color: vk::ClearColorValue {
float32: [0.1, 0.1, 0.1, 1.0],
},
});
let depth_attachment = vk::RenderingAttachmentInfo::default()
.image_view(self.depth_image_view)
.image_layout(vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL)
.load_op(vk::AttachmentLoadOp::CLEAR)
.store_op(vk::AttachmentStoreOp::DONT_CARE) // Or STORE if needed
.clear_value(vk::ClearValue {
depth_stencil: vk::ClearDepthStencilValue {
depth: 1.0,
stencil: 0,
},
});
let rendering_info = vk::RenderingInfo::default()
.render_area(vk::Rect2D {
offset: vk::Offset2D { x: 0, y: 0 },
extent: self.swapchain_extent,
})
.layer_count(1)
.color_attachments(std::slice::from_ref(&color_attachment))
.depth_attachment(&depth_attachment);
// --- Begin Dynamic Rendering ---
unsafe {
// Need unsafe for Vulkan commands
self.device
.raw()
.cmd_begin_rendering(command_buffer, &rendering_info);
}
// --- Set Viewport & Scissor ---
let viewport = vk::Viewport {
x: 0.0,
y: 0.0,
width: self.swapchain_extent.width as f32,
height: self.swapchain_extent.height as f32,
min_depth: 0.0,
max_depth: 1.0,
};
let scissor = vk::Rect2D {
offset: vk::Offset2D { x: 0, y: 0 },
extent: self.swapchain_extent,
};
unsafe {
// Need unsafe for Vulkan commands
self.device
.raw()
.cmd_set_viewport(command_buffer, 0, &[viewport]);
self.device
.raw()
.cmd_set_scissor(command_buffer, 0, &[scissor]);
}
// --- Draw Triangle ---
unsafe {
// Need unsafe for Vulkan commands
self.device.raw().cmd_bind_pipeline(
command_buffer,
vk::PipelineBindPoint::GRAPHICS,
self.triangle_pipeline,
);
// Draw 3 vertices, 1 instance, 0 first vertex, 0 first instance
self.device.raw().cmd_draw(command_buffer, 3, 1, 0, 0);
}
// --- End Dynamic Rendering ---
unsafe {
// Need unsafe for Vulkan commands
self.device.raw().cmd_end_rendering(command_buffer);
}
// --- End Command Buffer ---
unsafe {
// Need unsafe for Vulkan commands
self.device.raw().end_command_buffer(command_buffer)?;
}
// --- Submit Command Buffer ---
let wait_semaphores = [frame_data.image_available_semaphore.handle()];
let wait_stages = [vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT];
let signal_semaphores = [frame_data.render_finished_semaphore.handle()];
let command_buffers = [command_buffer];
let submit_info = vk::SubmitInfo::default()
.wait_semaphores(&wait_semaphores)
.wait_dst_stage_mask(&wait_stages)
.command_buffers(&command_buffers)
.signal_semaphores(&signal_semaphores);
// assert_eq!(
// self.graphics_queue.device().raw().handle(), // Device from Queue
// self.device.raw().handle(), // Device stored in Renderer
// "Device handle mismatch between Renderer and Graphics Queue!"
// );
unsafe {
// Need unsafe for queue submit
self.graphics_queue.submit(
self.device.raw(),
&[submit_info],
Some(&frame_data.in_flight_fence),
)?;
}
// --- Present ---
let swapchains = [self.swapchain.as_ref().unwrap().handle()]; // Safe unwrap after acquire
let image_indices = [image_index];
let present_info = vk::PresentInfoKHR::default()
.wait_semaphores(&signal_semaphores)
.swapchains(&swapchains)
.image_indices(&image_indices);
let suboptimal_present = unsafe {
// Need unsafe for queue_present
self.swapchain
.as_ref()
.unwrap() // Safe unwrap
.loader()
.queue_present(self.graphics_queue.handle(), &present_info)
.map_err(|e| {
// Handle VK_ERROR_OUT_OF_DATE_KHR specifically
if e == vk::Result::ERROR_OUT_OF_DATE_KHR {
RendererError::SwapchainSuboptimal
} else {
RendererError::Vulkan(e)
}
})? // Returns true if suboptimal
};
if suboptimal_present {
warn!("Swapchain is suboptimal after present, scheduling recreation.");
self.window_resized = true; // Trigger recreation next frame
}
// --- Advance Frame Counter ---
self.current_frame = (self.current_frame + 1) % MAX_FRAMES_IN_FLIGHT;
Ok(())
}
// --- Helper: Swapchain Recreation ---
fn recreate_swapchain(&mut self) -> Result<(), RendererError> {
info!("Recreating swapchain...");
self.device.wait_idle()?; // Wait until device is idle
// 1. Cleanup old resources
self.cleanup_swapchain_resources(); // Destroys views, depth, old swapchain
// 2. Create new resources
let (new_swapchain, new_format, new_extent, new_image_views) =
Self::create_swapchain_and_views(
&self.device,
&self.surface,
self.current_width,
self.current_height,
self.swapchain.as_ref().map(|s| s.handle()), // Pass old handle
)?;
let (new_depth_handle, new_depth_view) = Self::create_depth_resources(
&self.device,
&self.resource_manager,
new_extent,
self.depth_format, // Keep the same depth format
)?;
// 3. Update Renderer state
self.swapchain = Some(new_swapchain);
self.swapchain_format = new_format;
self.swapchain_extent = new_extent;
self.swapchain_image_views = new_image_views;
self.depth_image_handle = new_depth_handle;
self.depth_image_view = new_depth_view;
// 4. Update Egui Renderer (if necessary, depends on its implementation)
// It might need the new extent or recreate internal resources.
// Assuming it handles extent changes via update_screen_descriptor called earlier.
info!(
"Swapchain recreated successfully ({}x{}).",
new_extent.width, new_extent.height
);
Ok(())
}
// --- Helper: Cleanup Swapchain Dependent Resources ---
fn cleanup_swapchain_resources(&mut self) {
debug!("Cleaning up swapchain resources...");
// Destroy depth buffer view
unsafe {
self.device
.raw()
.destroy_image_view(self.depth_image_view, None);
}
// Destroy depth buffer image via resource manager
if let Err(e) = self.resource_manager.destroy_image(self.depth_image_handle) {
error!("Failed to destroy depth image: {}", e);
// Continue cleanup even if this fails
}
// Drop the old swapchain object (RAII in gfx_hal::Swapchain handles vkDestroySwapchainKHR)
self.swapchain = None;
debug!("Swapchain resources cleaned up.");
}
// --- Helper: Create Swapchain ---
fn create_swapchain_and_views(
device: &Arc<Device>,
surface: &Arc<Surface>,
width: u32,
height: u32,
old_swapchain: Option<vk::SwapchainKHR>,
) -> Result<
(
Swapchain,
vk::SurfaceFormatKHR,
vk::Extent2D,
Vec<vk::ImageView>,
),
RendererError,
> {
let details = Self::query_swapchain_support(device.physical_device_handle(), surface)?;
let surface_format = Self::choose_swapchain_format(&details.formats);
let present_mode = Self::choose_swapchain_present_mode(&details.present_modes);
let extent = Self::choose_swapchain_extent(&details.capabilities, width, height);
let mut image_count = details.capabilities.min_image_count + 1;
if details.capabilities.max_image_count > 0
&& image_count > details.capabilities.max_image_count
{
image_count = details.capabilities.max_image_count;
}
let config = SwapchainConfig {
desired_format: surface_format,
desired_present_mode: present_mode,
desired_image_count: image_count,
extent,
image_usage: vk::ImageUsageFlags::COLOR_ATTACHMENT,
pre_transform: details.capabilities.current_transform,
composite_alpha: vk::CompositeAlphaFlagsKHR::OPAQUE,
};
let swapchain =
unsafe { Swapchain::new(device.clone(), surface.clone(), config, old_swapchain)? };
// Create Image Views
let image_views = swapchain
.image_views() // Assuming Swapchain::new creates and stores these
.to_vec(); // Clone the slice into a Vec
// If Swapchain::new doesn't create views, we need to do it here:
/*
let images = swapchain.images()?; // Assuming this method exists
let mut image_views = Vec::with_capacity(images.len());
for &image in images.iter() {
let create_info = vk::ImageViewCreateInfo::default()
.image(image)
.view_type(vk::ImageViewType::TYPE_2D)
.format(surface_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,
});
let view = unsafe { device.raw().create_image_view(&create_info, None)? };
image_views.push(view);
}
*/
Ok((swapchain, surface_format, extent, image_views))
}
// --- Helper: Create Depth Resources ---
fn create_depth_resources(
device: &Arc<Device>,
resource_manager: &Arc<ResourceManager>,
extent: vk::Extent2D,
depth_format: vk::Format,
) -> Result<(ImageHandle, vk::ImageView), RendererError> {
let image_create_info = vk::ImageCreateInfo::default()
.image_type(vk::ImageType::TYPE_2D)
.extent(vk::Extent3D {
width: extent.width,
height: extent.height,
depth: 1,
})
.mip_levels(1)
.array_layers(1)
.format(depth_format)
.tiling(vk::ImageTiling::OPTIMAL)
.initial_layout(vk::ImageLayout::UNDEFINED)
.usage(vk::ImageUsageFlags::DEPTH_STENCIL_ATTACHMENT)
.samples(vk::SampleCountFlags::TYPE_1)
.sharing_mode(vk::SharingMode::EXCLUSIVE);
let handle = resource_manager.create_image(&image_create_info, MemoryLocation::GpuOnly)?;
// Get the vk::Image handle to create the view
let image_info = resource_manager.get_image_info(handle)?;
let view_create_info = vk::ImageViewCreateInfo::default()
.image(image_info.image)
.view_type(vk::ImageViewType::TYPE_2D)
.format(depth_format)
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::DEPTH,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
});
let view = unsafe { device.raw().create_image_view(&view_create_info, None)? };
Ok((handle, view))
}
// --- Helper: Create Triangle Pipeline ---
fn create_triangle_pipeline(
device: &Arc<Device>,
color_format: vk::Format,
depth_format: vk::Format,
) -> Result<(vk::PipelineLayout, vk::Pipeline), RendererError> {
// --- Shaders (Hardcoded example) ---
// Vertex Shader (GLSL) - outputs clip space position based on vertex index
/*
#version 450
vec2 positions[3] = vec2[](
vec2(0.0, -0.5),
vec2(0.5, 0.5),
vec2(-0.5, 0.5)
);
void main() {
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
}
*/
// Fragment Shader (GLSL) - outputs solid orange
/*
#version 450
layout(location = 0) out vec4 outColor;
void main() {
outColor = vec4(1.0, 0.5, 0.0, 1.0); // Orange
}
*/
// Load compiled SPIR-V (replace with actual loading)
let vert_shader_code = include_bytes!(concat!(env!("OUT_DIR"), "/shaders/vert.glsl.spv")); // Placeholder path
let frag_shader_code = include_bytes!(concat!(env!("OUT_DIR"), "/shaders/frag.glsl.spv")); // Placeholder path
let vert_module = Self::create_shader_module(device, vert_shader_code)?;
let frag_module = Self::create_shader_module(device, frag_shader_code)?;
let main_function_name = CStr::from_bytes_with_nul(b"main\0").unwrap();
let vert_stage_info = vk::PipelineShaderStageCreateInfo::default()
.stage(vk::ShaderStageFlags::VERTEX)
.module(vert_module)
.name(main_function_name);
let frag_stage_info = vk::PipelineShaderStageCreateInfo::default()
.stage(vk::ShaderStageFlags::FRAGMENT)
.module(frag_module)
.name(main_function_name);
let shader_stages = [vert_stage_info, frag_stage_info];
// --- Fixed Function State ---
let vertex_input_info = vk::PipelineVertexInputStateCreateInfo::default(); // No vertex buffers/attributes
let input_assembly = vk::PipelineInputAssemblyStateCreateInfo::default()
.topology(vk::PrimitiveTopology::TRIANGLE_LIST)
.primitive_restart_enable(false);
let viewport_state = vk::PipelineViewportStateCreateInfo::default()
.viewport_count(1) // Dynamic viewport
.scissor_count(1); // Dynamic scissor
let rasterizer = vk::PipelineRasterizationStateCreateInfo::default()
.depth_clamp_enable(false)
.rasterizer_discard_enable(false)
.polygon_mode(vk::PolygonMode::FILL)
.line_width(1.0)
.cull_mode(vk::CullModeFlags::NONE) // Draw front face
.front_face(vk::FrontFace::CLOCKWISE) // Doesn't matter for hardcoded triangle
.depth_bias_enable(false);
let multisampling = vk::PipelineMultisampleStateCreateInfo::default()
.sample_shading_enable(false)
.rasterization_samples(vk::SampleCountFlags::TYPE_1);
let depth_stencil = vk::PipelineDepthStencilStateCreateInfo::default()
.depth_test_enable(true)
.depth_write_enable(true)
.depth_compare_op(vk::CompareOp::LESS)
.depth_bounds_test_enable(false)
.stencil_test_enable(false);
let color_blend_attachment = vk::PipelineColorBlendAttachmentState::default()
.color_write_mask(vk::ColorComponentFlags::RGBA)
.blend_enable(false); // No blending for opaque triangle
let color_blending = vk::PipelineColorBlendStateCreateInfo::default()
.logic_op_enable(false)
.attachments(std::slice::from_ref(&color_blend_attachment));
let dynamic_states = [vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR];
let dynamic_state =
vk::PipelineDynamicStateCreateInfo::default().dynamic_states(&dynamic_states);
// --- Pipeline Layout ---
let layout_info = vk::PipelineLayoutCreateInfo::default(); // No descriptors/push constants
let pipeline_layout = unsafe {
device
.raw()
.create_pipeline_layout(&layout_info, None)
.map_err(RendererError::PipelineLayoutCreation)?
};
// --- Dynamic Rendering Info ---
let mut pipeline_rendering_info = vk::PipelineRenderingCreateInfo::default()
.color_attachment_formats(std::slice::from_ref(&color_format))
.depth_attachment_format(depth_format);
// --- Graphics Pipeline ---
let pipeline_info = vk::GraphicsPipelineCreateInfo::default()
.stages(&shader_stages)
.vertex_input_state(&vertex_input_info)
.input_assembly_state(&input_assembly)
.viewport_state(&viewport_state)
.rasterization_state(&rasterizer)
.multisample_state(&multisampling)
.depth_stencil_state(&depth_stencil)
.color_blend_state(&color_blending)
.dynamic_state(&dynamic_state)
.layout(pipeline_layout)
// No render pass needed with dynamic rendering!
.push_next(&mut pipeline_rendering_info); // Chain dynamic rendering info
let pipeline = unsafe {
device
.raw()
.create_graphics_pipelines(vk::PipelineCache::null(), &[pipeline_info], None)
.map_err(|(_, e)| RendererError::PipelineCreation(e))?[0] // Get the first pipeline from the result Vec
};
// --- Cleanup Shader Modules ---
unsafe {
device.raw().destroy_shader_module(vert_module, None);
device.raw().destroy_shader_module(frag_module, None);
}
Ok((pipeline_layout, pipeline))
}
fn create_shader_module(
device: &Arc<Device>,
code: &[u8],
) -> Result<vk::ShaderModule, RendererError> {
// 1. Check if byte count is a multiple of 4 (Vulkan requirement)
let byte_count = code.len();
if byte_count == 0 {
// Handle empty shader code case if necessary, maybe return error
return Err(RendererError::ShaderCreation(
vk::Result::ERROR_INITIALIZATION_FAILED,
)); // Or a custom error
}
if byte_count % 4 != 0 {
// This indicates an invalid SPIR-V file was loaded.
// Panicking here is reasonable during development, or return a specific error.
error!(
"Shader code size ({}) is not a multiple of 4 bytes! Check the .spv file generation.",
byte_count
);
// You could return an error instead of panicking:
return Err(RendererError::ShaderCreation(
vk::Result::ERROR_INITIALIZATION_FAILED,
)); // Or a custom error like InvalidShaderData
// panic!(
// "Shader code size ({}) is not a multiple of 4 bytes!",
// byte_count
// );
}
// --- Alternative: Copying to Vec<u32> (Safest, but allocates/copies) ---
let code_u32: Vec<u32> = code
.chunks_exact(4)
.map(|chunk| {
u32::from_ne_bytes(chunk.try_into().expect("Chunk size is guaranteed to be 4"))
}) // Use from_le_bytes if SPIR-V endianness matters (it's LE)
.collect();
let code_slice_ref = &code_u32; // Use this slice below
// --------------------------------------------------------------------
// 3. Create the shader module
let create_info = vk::ShaderModuleCreateInfo::default().code(&code_slice_ref); // Pass the &[u32] slice
unsafe {
device
.raw()
.create_shader_module(&create_info, None)
.map_err(|e| {
error!("Failed to create shader module: {:?}", e); // Add logging
RendererError::ShaderCreation(e)
})
}
}
// --- Helper: Create Frame Sync Objects & Command Resources ---
fn create_frame_data(device: &Arc<Device>) -> Result<Vec<FrameData>, RendererError> {
let mut frames_data = Vec::with_capacity(MAX_FRAMES_IN_FLIGHT);
for _ in 0..MAX_FRAMES_IN_FLIGHT {
let image_available_semaphore = Semaphore::new(device.clone())?;
let render_finished_semaphore = Semaphore::new(device.clone())?;
let in_flight_fence = Fence::new(device.clone(), true)?; // Create signaled
// Create Command Pool
let pool_info = vk::CommandPoolCreateInfo::default()
.flags(
vk::CommandPoolCreateFlags::TRANSIENT
| vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER,
) // Allow resetting individual buffers
.queue_family_index(device.graphics_queue_family_index());
let command_pool = unsafe {
device
.raw()
.create_command_pool(&pool_info, None)
.map_err(RendererError::CommandPoolCreation)?
};
// Allocate Command Buffer
let alloc_info = vk::CommandBufferAllocateInfo::default()
.command_pool(command_pool)
.level(vk::CommandBufferLevel::PRIMARY)
.command_buffer_count(1);
let command_buffer = unsafe {
device
.raw()
.allocate_command_buffers(&alloc_info)
.map_err(RendererError::CommandBufferAllocation)?[0]
};
frames_data.push(FrameData {
command_pool,
command_buffer, // Stays allocated, just reset/rerecorded
image_available_semaphore,
render_finished_semaphore,
in_flight_fence,
});
}
Ok(frames_data)
}
// --- Swapchain Support Helpers --- (Simplified versions)
fn query_swapchain_support(
physical_device: vk::PhysicalDevice,
surface: &Arc<Surface>,
) -> Result<SwapchainSupportDetails, GfxHalError> {
unsafe {
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)?;
Ok(SwapchainSupportDetails {
capabilities,
formats,
present_modes,
})
}
}
fn choose_swapchain_format(available_formats: &[vk::SurfaceFormatKHR]) -> vk::SurfaceFormatKHR {
available_formats
.iter()
.find(|format| {
format.format == vk::Format::B8G8R8A8_SRGB // Prefer SRGB
&& format.color_space == vk::ColorSpaceKHR::SRGB_NONLINEAR
})
.unwrap_or(&available_formats[0]) // Fallback to first available
.clone()
}
fn choose_swapchain_present_mode(available_modes: &[vk::PresentModeKHR]) -> vk::PresentModeKHR {
available_modes
.iter()
.find(|&&mode| mode == vk::PresentModeKHR::MAILBOX) // Prefer Mailbox (low latency)
.unwrap_or(&vk::PresentModeKHR::FIFO) // Guaranteed fallback
.clone()
}
fn choose_swapchain_extent(
capabilities: &vk::SurfaceCapabilitiesKHR,
window_width: u32,
window_height: u32,
) -> vk::Extent2D {
if capabilities.current_extent.width != u32::MAX {
// Window manager dictates extent
capabilities.current_extent
} else {
// We can choose extent within bounds
vk::Extent2D {
width: window_width.clamp(
capabilities.min_image_extent.width,
capabilities.max_image_extent.width,
),
height: window_height.clamp(
capabilities.min_image_extent.height,
capabilities.max_image_extent.height,
),
}
}
}
// --- Helper: Find Depth Format ---
fn find_depth_format(
instance: &Arc<gfx_hal::instance::Instance>,
device: &Arc<Device>,
) -> Result<vk::Format, RendererError> {
let candidates = [
vk::Format::D32_SFLOAT,
vk::Format::D32_SFLOAT_S8_UINT,
vk::Format::D24_UNORM_S8_UINT,
];
for &format in candidates.iter() {
let props = unsafe {
instance
.ash_instance()
.get_physical_device_format_properties(device.physical_device_handle(), format)
};
if props
.optimal_tiling_features
.contains(vk::FormatFeatureFlags::DEPTH_STENCIL_ATTACHMENT)
{
return Ok(format);
}
}
Err(RendererError::Vulkan(
vk::Result::ERROR_FORMAT_NOT_SUPPORTED,
)) // Or custom error
}
}
// --- Drop Implementation ---
impl Drop for Renderer {
fn drop(&mut self) {
info!("Dropping Renderer...");
// Ensure GPU is idle before destroying anything
if let Err(e) = self.device.wait_idle() {
error!("Error waiting for device idle during drop: {}", e);
// Continue cleanup regardless
}
// Cleanup swapchain resources (views, depth buffer, swapchain object)
self.cleanup_swapchain_resources();
// Drop egui renderer explicitly before allocator/device go away
// Assuming egui_renderer has a drop impl that cleans its Vulkan resources
// std::mem::drop(self.egui_renderer); // Not needed if it implements Drop
// Destroy pipelines
unsafe {
self.device
.raw()
.destroy_pipeline(self.triangle_pipeline, None);
self.device
.raw()
.destroy_pipeline_layout(self.triangle_pipeline_layout, None);
}
// Destroy frame data (fences, semaphores, command pools)
// Fences/Semaphores are handled by gfx_hal::Drop
// Command buffers are freed with the pool
for frame_data in self.frames_data.drain(..) {
unsafe {
self.device
.raw()
.destroy_command_pool(frame_data.command_pool, None);
}
}
// Arcs (device, queue, resource_manager, surface, allocator) will drop automatically.
// ResourceManager's Drop impl should handle allocator destruction if needed.
info!("Renderer dropped.");
}
}

View file

@ -164,6 +164,11 @@ impl ResourceManager {
})
}
/// Gets a shared reference to the Allocator
pub fn allocator(&self) -> Arc<Mutex<Allocator>> {
self.allocator.clone()
}
/// Gets or initializes the TransferSetup resources.
fn get_transfer_setup(&self) -> Result<TransferSetup> {
let mut setup_guard = self.transfer_setup.lock();
@ -254,7 +259,9 @@ impl ResourceManager {
// 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
transfer_setup
.queue
.submit(self.device.raw(), &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.