From 8a1c5237d5f9e1abe2a9a3ed1778d0ce65f4ebe2 Mon Sep 17 00:00:00 2001 From: zack Date: Fri, 28 Mar 2025 16:33:40 -0400 Subject: [PATCH] Add tri --- Cargo.lock | 200 +++++- Cargo.toml | 14 +- crates/engine/Cargo.toml | 9 +- crates/engine/src/main.rs | 463 ++++++++++++- crates/engine/src/scene_data.rs | 67 -- crates/gfx_hal/src/device.rs | 109 +-- crates/gfx_hal/src/error.rs | 4 +- crates/gfx_hal/src/instance.rs | 72 +- crates/gfx_hal/src/physical_device.rs | 17 +- crates/gfx_hal/src/queue.rs | 33 +- crates/gfx_hal/src/swapchain.rs | 4 +- crates/gfx_hal/src/sync.rs | 5 + crates/renderer/Cargo.toml | 24 + crates/renderer/build.rs | 152 +++++ crates/renderer/src/lib.rs | 927 ++++++++++++++++++++++++++ crates/resource_manager/src/lib.rs | 9 +- flake.nix | 1 + shaders/frag.glsl.frag | 7 + shaders/vert.glsl.vert | 10 + 19 files changed, 1952 insertions(+), 175 deletions(-) delete mode 100644 crates/engine/src/scene_data.rs create mode 100644 crates/renderer/Cargo.toml create mode 100644 crates/renderer/build.rs create mode 100644 crates/renderer/src/lib.rs create mode 100644 shaders/frag.glsl.frag create mode 100644 shaders/vert.glsl.vert diff --git a/Cargo.lock b/Cargo.lock index 1bcd1f4..b58fac3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -58,6 +58,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" +[[package]] +name = "anyhow" +version = "1.0.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" + [[package]] name = "arrayref" version = "0.3.9" @@ -152,9 +158,9 @@ dependencies = [ [[package]] name = "bytemuck_derive" -version = "1.8.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fa76293b4f7bb636ab88fd78228235b5248b4d05cc589aed610f954af5d7c7a" +checksum = "2ff22c2722516255d1823ce3cc4bc0b154dbc9364be5c905d6baa6eccbbc8774" dependencies = [ "proc-macro2", "quote", @@ -222,6 +228,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "cmake" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] + [[package]] name = "cocoa" version = "0.25.0" @@ -350,16 +365,57 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" +[[package]] +name = "ecolor" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc4feb366740ded31a004a0e4452fbf84e80ef432ecf8314c485210229672fd1" +dependencies = [ + "emath", +] + +[[package]] +name = "egui" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25dd34cec49ab55d85ebf70139cb1ccd29c977ef6b6ba4fe85489d6877ee9ef3" +dependencies = [ + "ahash", + "bitflags 2.9.0", + "emath", + "epaint", + "nohash-hasher", + "profiling", +] + +[[package]] +name = "egui-ash-renderer" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98634d0948894c6108149c954624d61ea952070649550cbb23e19e1913a6515" +dependencies = [ + "ash", + "egui", + "gpu-allocator", + "log", + "thiserror 2.0.12", +] + +[[package]] +name = "emath" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e4cadcff7a5353ba72b7fea76bf2122b5ebdbc68e8155aa56dfdea90083fe1b" + [[package]] name = "engine" version = "0.1.0" dependencies = [ "ash", - "bytemuck", + "egui", "gfx_hal", - "glam", - "gpu-allocator", "raw-window-handle", + "renderer", "resource_manager", "thiserror 2.0.12", "tracing", @@ -367,6 +423,28 @@ dependencies = [ "winit", ] +[[package]] +name = "epaint" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fcc0f5a7c613afd2dee5e4b30c3e6acafb8ad6f0edb06068811f708a67c562" +dependencies = [ + "ab_glyph", + "ahash", + "ecolor", + "emath", + "epaint_default_fonts", + "nohash-hasher", + "parking_lot", + "profiling", +] + +[[package]] +name = "epaint_default_fonts" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7e7a64c02cf7a5b51e745a9e45f60660a286f151c238b9d397b3e923f5082f" + [[package]] name = "equivalent" version = "1.0.2" @@ -488,6 +566,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + [[package]] name = "jni" version = "0.21.1" @@ -568,6 +652,15 @@ dependencies = [ "redox_syscall 0.5.10", ] +[[package]] +name = "link-cplusplus" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6f6da007f968f9def0d65a05b187e2960183de70c160204ecfccf0ee330212" +dependencies = [ + "cc", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -644,6 +737,12 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -899,9 +998,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.1" +version = "1.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" +checksum = "c2806eaa3524762875e21c3dcd057bc4b7bfa01ce4da8d46be1cd43649e1cc6b" [[package]] name = "orbclient" @@ -1027,6 +1126,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" + [[package]] name = "quick-xml" version = "0.37.3" @@ -1081,6 +1186,27 @@ dependencies = [ "bitflags 2.9.0", ] +[[package]] +name = "renderer" +version = "0.1.0" +dependencies = [ + "anyhow", + "ash", + "bytemuck", + "egui", + "egui-ash-renderer", + "gfx_hal", + "glam", + "gpu-allocator", + "parking_lot", + "resource_manager", + "shaderc", + "thiserror 2.0.12", + "tracing", + "walkdir", + "winit", +] + [[package]] name = "resource_manager" version = "0.1.0" @@ -1093,6 +1219,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + [[package]] name = "rustix" version = "0.38.44" @@ -1112,6 +1244,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +[[package]] +name = "ryu" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" + [[package]] name = "same-file" version = "1.0.6" @@ -1166,6 +1304,41 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "shaderc" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cdc8a26f751f141968dbc08fc01cfa3f4a288351f81cfd9148db41aa189f635" +dependencies = [ + "libc", + "shaderc-sys", +] + +[[package]] +name = "shaderc-sys" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "275f0ea572da7183c0cd0a060ba67c9fb54934523d4c9a9494ce5828c533d40b" +dependencies = [ + "cmake", + "libc", + "link-cplusplus", + "pkg-config", + "roxmltree", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -1382,6 +1555,16 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.19" @@ -1389,11 +1572,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "nu-ansi-term", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing-core", "tracing-log", + "tracing-serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a8bfc22..5179e1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,12 @@ [workspace] resolver = "2" -members = ["crates/engine", "crates/gfx_hal", "crates/resource_manager"] +members = [ + "crates/engine", + "crates/gfx_hal", + "crates/renderer", + "crates/resource_manager", +] [workspace.dependencies] ash = { version = "0.38" } @@ -14,9 +19,14 @@ glam = { version = "0.22", default-features = false, features = [ "libm", "bytemuck", ] } +egui-ash-renderer = { version = "0.8.0", features = [ + "gpu-allocator", + "dynamic-rendering", +] } +egui = "0.31" bytemuck = { version = "1.21.0", features = ["derive"] } tracing = "0.1" -tracing-subscriber = "0.3" +tracing-subscriber = { version = "0.3", features = ["json"] } parking_lot = "0.12.3" thiserror = "2.0.12" diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml index a632523..b8f8fa0 100644 --- a/crates/engine/Cargo.toml +++ b/crates/engine/Cargo.toml @@ -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" } diff --git a/crates/engine/src/main.rs b/crates/engine/src/main.rs index dbe8ad8..8970c5d 100644 --- a/crates/engine/src/main.rs +++ b/crates/engine/src/main.rs @@ -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, // Keep instance alive + _physical_device: PhysicalDevice, // Keep info, though Device holds handle + device: Arc, + graphics_queue: Arc, + surface: Arc, + + // Resource Management + resource_manager: Arc, + + // Renderer + renderer: Renderer, + + // Windowing + window: Arc, // Use Arc for potential multi-threading later + + // State + last_frame_time: Instant, + ui_show_demo: bool, +} + +impl Application { + fn new(window: Arc) -> Result { + 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 = 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> { + // 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> { + 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(()) } diff --git a/crates/engine/src/scene_data.rs b/crates/engine/src/scene_data.rs deleted file mode 100644 index 0a97760..0000000 --- a/crates/engine/src/scene_data.rs +++ /dev/null @@ -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, Vec) { - 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) -} diff --git a/crates/gfx_hal/src/device.rs b/crates/gfx_hal/src/device.rs index b4e087d..a26e36b 100644 --- a/crates/gfx_hal/src/device.rs +++ b/crates/gfx_hal/src/device.rs @@ -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, 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> { + // --- 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, 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 + 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 + { + // 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 } /// 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> { 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, ) } } diff --git a/crates/gfx_hal/src/error.rs b/crates/gfx_hal/src/error.rs index 4192b31..0e3e8d6 100644 --- a/crates/gfx_hal/src/error.rs +++ b/crates/gfx_hal/src/error.rs @@ -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}")] diff --git a/crates/gfx_hal/src/instance.rs b/crates/gfx_hal/src/instance.rs index b188805..7b200d1 100644 --- a/crates/gfx_hal/src/instance.rs +++ b/crates/gfx_hal/src/instance.rs @@ -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`. - // pub unsafe fn enumerate_phyiscal_devices(self: &Arc) -> Result> { - // 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`. + pub unsafe fn enumerate_physical_devices(self: &Arc) -> Result> { + 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, - // window_handle_trait_obj: &(impl HasWindowHandle + HasDisplayHandle), - // ) -> Result> { - // 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, + window_handle_trait_obj: &(impl HasWindowHandle + HasDisplayHandle), + ) -> Result> { + Surface::new(Arc::clone(self), window_handle_trait_obj) + } } impl Drop for Instance { diff --git a/crates/gfx_hal/src/physical_device.rs b/crates/gfx_hal/src/physical_device.rs index 4463dca..4fbdf96 100644 --- a/crates/gfx_hal/src/physical_device.rs +++ b/crates/gfx_hal/src/physical_device.rs @@ -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, @@ -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. diff --git a/crates/gfx_hal/src/queue.rs b/crates/gfx_hal/src/queue.rs index d10d469..8aa4513 100644 --- a/crates/gfx_hal/src/queue.rs +++ b/crates/gfx_hal/src/queue.rs @@ -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(()) } diff --git a/crates/gfx_hal/src/swapchain.rs b/crates/gfx_hal/src/swapchain.rs index b88153a..d39b88f 100644 --- a/crates/gfx_hal/src/swapchain.rs +++ b/crates/gfx_hal/src/swapchain.rs @@ -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`. diff --git a/crates/gfx_hal/src/sync.rs b/crates/gfx_hal/src/sync.rs index c16d1f5..0f684aa 100644 --- a/crates/gfx_hal/src/sync.rs +++ b/crates/gfx_hal/src/sync.rs @@ -33,6 +33,11 @@ impl Fence { Ok(Self { device, fence }) } + /// Returns the device used by the fence. + pub fn device(&self) -> &Arc { + &self.device + } + /// Waits for the fence to become signaled. /// /// # Arguments diff --git a/crates/renderer/Cargo.toml b/crates/renderer/Cargo.toml new file mode 100644 index 0000000..6f4bbee --- /dev/null +++ b/crates/renderer/Cargo.toml @@ -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" diff --git a/crates/renderer/build.rs b/crates/renderer/build.rs new file mode 100644 index 0000000..16afb10 --- /dev/null +++ b/crates/renderer/build.rs @@ -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(()) +} diff --git a/crates/renderer/src/lib.rs b/crates/renderer/src/lib.rs new file mode 100644 index 0000000..7a9f482 --- /dev/null +++ b/crates/renderer/src/lib.rs @@ -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, + present_modes: Vec, +} + +pub struct Renderer { + device: Arc, + graphics_queue: Arc, + resource_manager: Arc, + allocator: Arc>, // Need direct access for egui + + surface: Arc, // Keep surface for recreation + swapchain: Option, // Option<> because it's recreated + swapchain_image_views: Vec, + 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, + 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, // Needed for allocator + device: Arc, + graphics_queue: Arc, + surface: Arc, + resource_manager: Arc, + initial_width: u32, + initial_height: u32, + ) -> Result { + 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, + surface: &Arc, + width: u32, + height: u32, + old_swapchain: Option, + ) -> Result< + ( + Swapchain, + vk::SurfaceFormatKHR, + vk::Extent2D, + Vec, + ), + 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, + resource_manager: &Arc, + 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, + 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, + code: &[u8], + ) -> Result { + // 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 (Safest, but allocates/copies) --- + let code_u32: Vec = 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) -> Result, 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, + ) -> Result { + 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, + device: &Arc, + ) -> Result { + 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."); + } +} diff --git a/crates/resource_manager/src/lib.rs b/crates/resource_manager/src/lib.rs index fe476a0..f489436 100644 --- a/crates/resource_manager/src/lib.rs +++ b/crates/resource_manager/src/lib.rs @@ -164,6 +164,11 @@ impl ResourceManager { }) } + /// Gets a shared reference to the Allocator + pub fn allocator(&self) -> Arc> { + self.allocator.clone() + } + /// Gets or initializes the TransferSetup resources. fn get_transfer_setup(&self) -> Result { 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. diff --git a/flake.nix b/flake.nix index 162a4ff..71450ae 100644 --- a/flake.nix +++ b/flake.nix @@ -64,6 +64,7 @@ buildInputs = with pkgs; [ libxkbcommon libGL + python3 # WINIT_UNIX_BACKEND=wayland wayland spirv-tools diff --git a/shaders/frag.glsl.frag b/shaders/frag.glsl.frag new file mode 100644 index 0000000..fe7b418 --- /dev/null +++ b/shaders/frag.glsl.frag @@ -0,0 +1,7 @@ +#version 450 + +layout(location = 0) out vec4 outColor; + +void main() { + outColor = vec4(1.0, 0.5, 0.0, 1.0); // Orange +} diff --git a/shaders/vert.glsl.vert b/shaders/vert.glsl.vert new file mode 100644 index 0000000..f114dbb --- /dev/null +++ b/shaders/vert.glsl.vert @@ -0,0 +1,10 @@ +#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); +}