From 12b20be584d819b7757908a28cb9551cc002be35 Mon Sep 17 00:00:00 2001 From: zack Date: Fri, 28 Mar 2025 17:31:16 -0400 Subject: [PATCH] =?UTF-8?q?cross-platform=20=F0=9F=91=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + Cargo.lock | 1 + Cargo.toml | 2 +- crates/engine/Cargo.toml | 1 + crates/engine/src/main.rs | 204 ++++++++++++++++------------- crates/renderer/src/lib.rs | 91 +++++++++++-- crates/resource_manager/src/lib.rs | 3 +- 7 files changed, 193 insertions(+), 110 deletions(-) diff --git a/.gitignore b/.gitignore index ca37050..2b3a96f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ sponza/ .direnv/ shader-cache/ +log-debug.log diff --git a/Cargo.lock b/Cargo.lock index b58fac3..ef2e401 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -412,6 +412,7 @@ name = "engine" version = "0.1.0" dependencies = [ "ash", + "ash-window", "egui", "gfx_hal", "raw-window-handle", diff --git a/Cargo.toml b/Cargo.toml index 5179e1f..ed32bcf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ members = [ ash = { version = "0.38" } ash-window = "0.13.0" color-eyre = "0.6.3" -winit = { version = "0.30.7", features = ["rwh_06"] } +winit = { version = "0.30.9", features = ["rwh_06"] } raw-window-handle = "0.6" gpu-allocator = { version = "0.27.0", features = ["vulkan"] } glam = { version = "0.22", default-features = false, features = [ diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml index b8f8fa0..e1eee25 100644 --- a/crates/engine/Cargo.toml +++ b/crates/engine/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] egui.workspace = true ash.workspace = true +ash-window.workspace = true tracing.workspace = true tracing-subscriber.workspace = true winit.workspace = true diff --git a/crates/engine/src/main.rs b/crates/engine/src/main.rs index 8970c5d..0e3a3fe 100644 --- a/crates/engine/src/main.rs +++ b/crates/engine/src/main.rs @@ -1,9 +1,9 @@ use std::{ error::Error, - ffi::{CStr, CString}, + ffi::{c_char, CStr, CString}, fs::OpenOptions, sync::Arc, - time::Instant, + time::{Duration, Instant}, }; use ash::vk; @@ -11,23 +11,20 @@ use gfx_hal::{ device::Device, error::GfxHalError, instance::Instance, instance::InstanceConfig, physical_device::PhysicalDevice, queue::Queue, surface::Surface, }; -use raw_window_handle::HasDisplayHandle; +use raw_window_handle::{HasDisplayHandle, HasRawDisplayHandle}; 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::{ + application::ApplicationHandler, 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"; +const APP_NAME: &str = "BeginDisregard"; +const ENGINE_NAME: &str = "Engine"; // --- Error Handling --- #[derive(Debug, thiserror::Error)] @@ -50,14 +47,12 @@ enum AppError { 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, + _graphics_queue: Arc, + _surface: Arc, // Resource Management resource_manager: Arc, @@ -68,9 +63,39 @@ struct Application { // Windowing window: Arc, // Use Arc for potential multi-threading later - // State + frame_count: u32, + last_fps_update_time: Instant, last_frame_time: Instant, - ui_show_demo: bool, +} + +#[derive(Default)] +struct ApplicationWrapper { + app: Option, +} + +impl ApplicationHandler for ApplicationWrapper { + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + let window = Arc::new( + event_loop + .create_window( + Window::default_attributes() + .with_title(format!("{} - {}", ENGINE_NAME, APP_NAME,)), + ) + .expect("Windows to be able to be created"), + ); + self.app = Some(Application::new(window).expect("Unable to create the Application.")); + } + + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + _window_id: winit::window::WindowId, + event: WindowEvent, + ) { + if let Some(app) = &mut self.app { + app.handle_event(&event, event_loop); + } + } } impl Application { @@ -78,20 +103,25 @@ impl Application { 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 = + ash_window::enumerate_required_extensions(window.display_handle().unwrap().into()) + .unwrap(); + let instance_extensions_c: Vec = instance_extensions .iter() - .map(|&s| CString::new(s.to_bytes()).unwrap()) + .map(|&ptr| { + // Safety: We are trusting that the pointers returned by + // ash_window::enumerate_required_extensions are valid, non-null, + // null-terminated C strings. This is a standard assumption when + // working with C APIs via FFI. + unsafe { + // 1. Create a borrowed CStr reference from the raw pointer. + let c_str = CStr::from_ptr(ptr as *const c_char); // Cast is optional but common + + // 2. Convert the borrowed CStr into an owned CString. + c_str.to_owned() + } + }) .collect(); let instance_config = InstanceConfig { @@ -137,7 +167,6 @@ impl Application { &pd, &surface, &required_device_extensions_cstr, - &required_dynamic_rendering_features, ) { Ok(indices) => Some((pd, indices)), Err(e) => { @@ -238,66 +267,67 @@ impl Application { _instance: instance, _physical_device: physical_device, device, - graphics_queue, - surface, + _graphics_queue: graphics_queue, + _surface: surface, resource_manager, renderer, window, + frame_count: 0, + last_fps_update_time: Instant::now(), last_frame_time: Instant::now(), - ui_show_demo: true, }) } - fn handle_event(&mut self, event: &Event<()>, active_event_loop: &ActiveEventLoop) { + fn handle_event(&mut self, event: &WindowEvent, 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 { .. } => {} - _ => {} - } + WindowEvent::CloseRequested => { + info!("Close requested. Exiting..."); + active_event_loop.exit(); } - // 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. + 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); + } + 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 { .. } => {} + WindowEvent::RedrawRequested => { let now = Instant::now(); let _delta_time = now.duration_since(self.last_frame_time); self.last_frame_time = now; + let elapsed_sice_last_update = now.duration_since(self.last_fps_update_time); + self.frame_count += 1; + + if elapsed_sice_last_update >= Duration::from_secs(1) { + let fps = self.frame_count as f64 / elapsed_sice_last_update.as_secs_f64(); + + let new_title = format!("{} - {} - {:.0} FPS", ENGINE_NAME, APP_NAME, fps); + self.window.set_title(&new_title); + + self.frame_count = 0; + self.last_fps_update_time = now; + } + // --- Render Frame --- match self.renderer.render_frame() { - Ok(_) => {} + Ok(_) => { + self.window.request_redraw(); + } Err(RendererError::SwapchainSuboptimal) => { // Swapchain is suboptimal, recreate it next frame by triggering resize warn!("Swapchain suboptimal, forcing resize."); @@ -310,14 +340,8 @@ impl Application { 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."); + + self.window.as_ref().request_redraw(); } _ => {} } @@ -331,7 +355,6 @@ 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 { @@ -425,7 +448,7 @@ fn main() -> Result<(), Box> { .with_ansi(true) .with_file(false) .with_line_number(false) - .without_time(); + .with_filter(filter::LevelFilter::DEBUG); let log_file = OpenOptions::new() .append(true) @@ -433,9 +456,10 @@ fn main() -> Result<(), Box> { .open("log-debug.log")?; let json_layer = tracing_subscriber::fmt::layer() - .json() + .with_ansi(false) + .without_time() .with_writer(log_file) - .with_filter(filter::LevelFilter::TRACE); + .with_filter(filter::LevelFilter::DEBUG); tracing_subscriber::registry() .with(fmt_layer) @@ -444,19 +468,11 @@ fn main() -> Result<(), Box> { // --- 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); - })?; + let mut app = ApplicationWrapper::default(); + event_loop.run_app(&mut app)?; Ok(()) } diff --git a/crates/renderer/src/lib.rs b/crates/renderer/src/lib.rs index 7a9f482..909d415 100644 --- a/crates/renderer/src/lib.rs +++ b/crates/renderer/src/lib.rs @@ -10,7 +10,7 @@ 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 +// Assuming winit is used by the app // Re-export ash for convenience if needed elsewhere pub use ash; @@ -179,16 +179,17 @@ impl Renderer { frame_data.in_flight_fence.wait(None)?; // Wait indefinitely // --- Acquire Swapchain Image --- + let swapchain_ref = self + .swapchain + .as_ref() + .ok_or(RendererError::SwapchainAcquisitionFailed)?; 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 - )? + swapchain_ref.acquire_next_image( + u64::MAX, // Timeout + Some(&frame_data.image_available_semaphore), + None, // Don't need a fence here + )? }; if suboptimal { @@ -221,10 +222,40 @@ impl Renderer { .begin_command_buffer(command_buffer, &cmd_begin_info)?; } + let current_swapchain_image = swapchain_ref.images()[image_index as usize]; + + let initial_layout_transition_barrier = vk::ImageMemoryBarrier::default() + .src_access_mask(vk::AccessFlags::empty()) // No need to wait for writes from previous frame/present + .dst_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE) // Will be written as attachment + .old_layout(vk::ImageLayout::UNDEFINED) // Assume undefined or present_src; UNDEFINED is safer + .new_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL) // Layout needed for rendering + .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .image(current_swapchain_image) + .subresource_range(vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + base_mip_level: 0, + level_count: 1, + base_array_layer: 0, + layer_count: 1, + }); + + unsafe { + self.device.raw().cmd_pipeline_barrier( + command_buffer, + vk::PipelineStageFlags::TOP_OF_PIPE, // Source stage (nothing before this) + vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT, // Destination stage (where write happens) + vk::DependencyFlags::empty(), + &[], // No memory barriers + &[], // No buffer memory barriers + &[initial_layout_transition_barrier], // The image barrier + ); + } + // --- 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) + .image_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL) .load_op(vk::AttachmentLoadOp::CLEAR) .store_op(vk::AttachmentStoreOp::STORE) .clear_value(vk::ClearValue { @@ -303,6 +334,36 @@ impl Renderer { self.device.raw().cmd_end_rendering(command_buffer); } + let current_swapchain_image = swapchain_ref.images()[image_index as usize]; + + let layout_transition_barrier = vk::ImageMemoryBarrier::default() + .src_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE) + .dst_access_mask(vk::AccessFlags::empty()) + .old_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL) + .new_layout(vk::ImageLayout::PRESENT_SRC_KHR) + .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .image(current_swapchain_image) + .subresource_range(vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + base_mip_level: 0, + level_count: 1, + base_array_layer: 0, + layer_count: 1, + }); + + unsafe { + self.device.raw().cmd_pipeline_barrier( + command_buffer, + vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT, + vk::PipelineStageFlags::BOTTOM_OF_PIPE, + vk::DependencyFlags::empty(), + &[], + &[], + &[layout_transition_barrier], + ); + } + // --- End Command Buffer --- unsafe { // Need unsafe for Vulkan commands @@ -347,9 +408,7 @@ impl Renderer { let suboptimal_present = unsafe { // Need unsafe for queue_present - self.swapchain - .as_ref() - .unwrap() // Safe unwrap + swapchain_ref .loader() .queue_present(self.graphics_queue.handle(), &present_info) .map_err(|e| { @@ -536,7 +595,11 @@ impl Renderer { .samples(vk::SampleCountFlags::TYPE_1) .sharing_mode(vk::SharingMode::EXCLUSIVE); - let handle = resource_manager.create_image(&image_create_info, MemoryLocation::GpuOnly)?; + let handle = resource_manager.create_image( + &image_create_info, + MemoryLocation::GpuOnly, + vk::ImageAspectFlags::DEPTH, + )?; // Get the vk::Image handle to create the view let image_info = resource_manager.get_image_info(handle)?; diff --git a/crates/resource_manager/src/lib.rs b/crates/resource_manager/src/lib.rs index f489436..bec4110 100644 --- a/crates/resource_manager/src/lib.rs +++ b/crates/resource_manager/src/lib.rs @@ -427,6 +427,7 @@ impl ResourceManager { &self, create_info: &vk::ImageCreateInfo, // User provides image details location: MemoryLocation, + aspect_flags: vk::ImageAspectFlags, ) -> Result { trace!( "Creating image: format={:?}, extent={:?}, usage={:?}, location={:?}", @@ -465,7 +466,7 @@ impl ResourceManager { .view_type(vk::ImageViewType::TYPE_2D) // Assuming 2D, adjust based on create_info .format(create_info.format) .subresource_range(vk::ImageSubresourceRange { - aspect_mask: vk::ImageAspectFlags::COLOR, // Assuming color, adjust based on usage + aspect_mask: aspect_flags, base_mip_level: 0, level_count: create_info.mip_levels, base_array_layer: 0,