Compare commits

...

10 commits

Author SHA1 Message Date
74a1be796f
feat: render and manage textures :3 2025-04-05 20:54:38 -04:00
2501390225
fix: all clippy errors 2025-04-03 17:33:35 -04:00
926515e6b2 Merge branch 'feat/3d-rendering' into 'main'
impl: 3D Rendering

See merge request zoey/vk-rs!4
2025-04-03 00:58:03 +00:00
ae09e61f40
Merge branch 'feat/egui' into 'main'
feat: add init egui impl

See merge request zoey/vk-rs!2
2025-04-01 21:46:30 -04:00
70176bb86a
impl: 3D Rendering 2025-04-01 21:41:24 -04:00
dbf9544e80
fix: remove log spam 2025-04-01 14:22:04 -04:00
6c70f7bc2e
impl: add egui!! 2025-04-01 14:20:23 -04:00
b1c164dc6a
wip: add init egui impl 2025-03-31 14:54:56 -04:00
51e4a4727e Merge branch 'feat/mutex' into 'main'
feat: switch away from `parking_lot::Mutex`

See merge request zoey/vk-rs!1
2025-03-31 15:41:32 +00:00
f71f0d8e10
feat: switch away from parking_lot::Mutex
Switching away from `parking_lot::Mutex` as `std::sync::Mutex` performs
better in all scenarios on both of our targets (linux/windows).
2025-03-30 16:27:35 -04:00
29 changed files with 4465 additions and 465 deletions

1645
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -6,6 +6,8 @@ members = [
"crates/gfx_hal",
"crates/renderer",
"crates/resource_manager",
"crates/scene",
"crates/shared",
]
[workspace.dependencies]
@ -24,11 +26,12 @@ egui-ash-renderer = { version = "0.8.0", features = [
"dynamic-rendering",
] }
egui = "0.31"
egui_tiles = "0.12"
bytemuck = { version = "1.21.0", features = ["derive"] }
tracing = "0.1"
tracing = { features = ["release_max_level_warn"], version = "0.1" }
tracing-subscriber = { version = "0.3", features = ["json"] }
parking_lot = "0.12.3"
thiserror = "2.0.12"
gltf = "1.4.1"
# # Enable incremental by default in release mode.
@ -57,4 +60,7 @@ thiserror = "2.0.12"
#
# rustflags = ["-Zshare-generics=off"]
# codegen-units = 1
opt-level = 1
[profile.dev.package."*"]
opt-level = 3

View file

@ -5,15 +5,22 @@ edition = "2021"
[dependencies]
egui.workspace = true
egui_tiles.workspace = true
ash.workspace = true
ash-window.workspace = true
color-eyre.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
winit.workspace = true
raw-window-handle.workspace = true
thiserror.workspace = true
glam.workspace = true
gfx_hal = { path = "../gfx_hal" }
renderer = { path = "../renderer" }
resource_manager = { path = "../resource_manager" }
shared = { path = "../shared" }
scene = { path = "../scene" }
clap = { version = "4.5.34", features = ["derive"] }
egui-winit = "0.31.1"

View file

@ -8,21 +8,28 @@ use std::{
use ash::vk;
use clap::Parser;
use egui::{Context, Slider, ViewportId};
use egui_winit::State;
use gfx_hal::{
device::Device, error::GfxHalError, instance::Instance, instance::InstanceConfig,
physical_device::PhysicalDevice, queue::Queue, surface::Surface,
};
use glam::Vec3;
use raw_window_handle::HasDisplayHandle;
use renderer::{Renderer, RendererError};
use resource_manager::{ResourceManager, ResourceManagerError};
use scene::Scene;
use shared::CameraInfo;
use tracing::{debug, error, info, warn};
use tracing_subscriber::{filter, layer::SubscriberExt, util::SubscriberInitExt, Layer};
use winit::{
application::ApplicationHandler,
event::WindowEvent,
event::{ElementState, KeyEvent, MouseButton, WindowEvent},
event_loop::{ActiveEventLoop, EventLoop},
keyboard::{KeyCode, PhysicalKey},
window::Window,
};
// --- Configuration ---
const APP_NAME: &str = "BeginDisregard";
const ENGINE_NAME: &str = "Engine";
@ -42,6 +49,8 @@ enum AppError {
NoSuitableDevice,
#[error("Failed to create CString: {0}")]
NulError(#[from] std::ffi::NulError),
#[error("Scene Error: {0}")]
SceneError(#[from] scene::SceneError),
}
struct Application {
@ -57,12 +66,125 @@ struct Application {
// Renderer
renderer: Renderer,
egui_ctx: Context,
egui_winit: State,
egui_app: EditorUI,
// --- Camera State ---
camera_info: CameraInfo,
camera_speed: f32,
camera_sensitivity: f32,
// --- Input State ---
is_forward_pressed: bool,
is_backward_pressed: bool,
is_left_pressed: bool,
is_right_pressed: bool,
is_up_pressed: bool, // Optional: For flying up
is_down_pressed: bool, // Optional: For flying down
is_rmb_pressed: bool, // Right mouse button
last_mouse_pos: Option<(f64, f64)>,
mouse_delta: (f64, f64),
capture_mouse: bool, // Flag to indicate if mouse should control camera
// Windowing
window: Arc<Window>, // Use Arc for potential multi-threading later
frame_count: u32,
last_fps_update_time: Instant,
last_frame_time: Instant,
current_fps: f64,
}
#[derive(Default)]
struct EditorUI {}
impl EditorUI {
fn title() -> String {
"engine".to_string()
}
fn build_ui(&mut self, ctx: &egui::Context, current_fps: f64, camera_info: &mut CameraInfo) {
egui::SidePanel::new(egui::panel::Side::Left, Self::title()).show(ctx, |ui| {
ui.label(format!("FPS - {:.2}", current_fps));
ui.separator();
egui::Grid::new("main_grid")
.spacing([40.0, 4.0])
.striped(true)
.show(ui, |ui| {
ui.label("FOV");
// Modify the passed-in camera_info
ui.add(Slider::new(&mut camera_info.camera_fov, 10.0..=120.0));
ui.end_row(); // Good practice in grids
// You could add more camera controls here if needed
// e.g., sliders for position, target (though direct manipulation is better)
ui.label("Camera Pos");
ui.label(format!(
"({:.1}, {:.1}, {:.1})",
camera_info.camera_pos.x,
camera_info.camera_pos.y,
camera_info.camera_pos.z
));
ui.end_row();
ui.label("Camera Target");
ui.label(format!(
"({:.1}, {:.1}, {:.1})",
camera_info.camera_target.x,
camera_info.camera_target.y,
camera_info.camera_target.z
));
ui.end_row();
});
ui.separator();
ui.label("Controls:");
ui.label("RMB + Drag: Look");
ui.label("WASD: Move");
ui.label("Space: Up");
ui.label("Shift: Down");
ui.label("Hold RMB to activate controls.");
});
// let mut tree = create_tree();
//
// egui::panel::SidePanel::new(egui::panel::Side::Left, Id::new("main_panel")).show(
// ctx,
// |ui| {
// let mut behavior = TreeBehavior {};
// tree.ui(&mut behavior, ui);
// },
// );
}
}
fn create_tree() -> egui_tiles::Tree<EditorUI> {
let mut next_view_nr = 0;
let mut gen_pane = || {
let pane = EditorUI {};
next_view_nr += 1;
pane
};
let mut tiles = egui_tiles::Tiles::default();
let mut tabs = vec![];
tabs.push({
let children = (0..7).map(|_| tiles.insert_pane(gen_pane())).collect();
tiles.insert_horizontal_tile(children)
});
tabs.push({
let cells = (0..11).map(|_| tiles.insert_pane(gen_pane())).collect();
tiles.insert_grid_tile(cells)
});
tabs.push(tiles.insert_pane(gen_pane()));
let root = tiles.insert_tab_tile(tabs);
egui_tiles::Tree::new("my_tree", root, tiles)
}
#[derive(Default)]
@ -222,30 +344,15 @@ impl Application {
// 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
);
let scene = Scene::from_gltf(
"./sponza/NewSponza_Main_glTF_003.gltf",
resource_manager.clone(),
)?;
// --- 5. Renderer ---
let initial_size = window.inner_size();
@ -255,11 +362,26 @@ impl Application {
graphics_queue.clone(),
surface.clone(),
resource_manager.clone(),
scene,
initial_size.width,
initial_size.height,
)?;
let egui_ctx = Context::default();
let egui_winit = State::new(
egui_ctx.clone(),
ViewportId::ROOT,
&window,
None,
None,
None,
);
let egui_app = EditorUI::default();
info!("Renderer initialized.");
let camera_info = CameraInfo::default(); // Get default camera settings
Ok(Self {
_instance: instance,
_physical_device: physical_device,
@ -269,13 +391,40 @@ impl Application {
_resource_manager: resource_manager,
renderer,
window,
egui_winit,
egui_ctx,
egui_app,
// --- Camera ---
camera_info, // Store the camera state here
camera_speed: 5.0, // Adjust as needed
camera_sensitivity: 0.002, // Adjust as needed
// --- Input ---
is_forward_pressed: false,
is_backward_pressed: false,
is_left_pressed: false,
is_right_pressed: false,
is_up_pressed: false,
is_down_pressed: false,
is_rmb_pressed: false,
last_mouse_pos: None,
mouse_delta: (0.0, 0.0),
capture_mouse: false, // Start with mouse free
frame_count: 0,
current_fps: 0.,
last_fps_update_time: Instant::now(),
last_frame_time: Instant::now(),
})
}
fn handle_event(&mut self, event: &WindowEvent, active_event_loop: &ActiveEventLoop) {
// Let egui process the event first
let egui_consumed_event = self.egui_winit.on_window_event(&self.window, event);
// Only process input for camera if egui didn't consume it AND we are capturing
let process_camera_input = !egui_consumed_event.consumed && self.capture_mouse;
match event {
WindowEvent::CloseRequested => {
info!("Close requested. Exiting...");
@ -298,30 +447,153 @@ impl Application {
.resize(new_inner_size.width, new_inner_size.height);
}
// Handle other inputs if not consumed by egui
WindowEvent::KeyboardInput { .. }
| WindowEvent::CursorMoved { .. }
| WindowEvent::MouseInput { .. } => {}
WindowEvent::MouseInput { state, button, .. } => {
if *button == MouseButton::Right {
let is_pressed = *state == ElementState::Pressed;
self.is_rmb_pressed = is_pressed;
// Decide whether to capture/release mouse based on RMB
// Only capture if pressed *outside* an egui interactive area
if is_pressed && !self.egui_ctx.is_pointer_over_area() {
self.capture_mouse = true;
self.window
.set_cursor_grab(winit::window::CursorGrabMode::Confined)
.or_else(|_| {
self.window
.set_cursor_grab(winit::window::CursorGrabMode::Locked)
})
.unwrap_or(());
self.window.set_cursor_visible(false);
self.last_mouse_pos = None; // Reset last pos on capture start
} else if !is_pressed {
self.capture_mouse = false;
self.window
.set_cursor_grab(winit::window::CursorGrabMode::None)
.unwrap_or(());
self.window.set_cursor_visible(true);
self.mouse_delta = (0.0, 0.0); // Stop camera movement
}
}
// Let egui handle its mouse clicks regardless of capture state
// (handled by on_window_event)
}
WindowEvent::CursorMoved { position, .. } => {
let current_pos = (position.x, position.y);
if self.capture_mouse {
// Only calculate delta if capturing
if let Some(last_pos) = self.last_mouse_pos {
self.mouse_delta.0 += current_pos.0 - last_pos.0;
self.mouse_delta.1 += current_pos.1 - last_pos.1;
}
// Store position relative to window center might be more robust
// with set_cursor_position, but this works with grab/confine too.
self.last_mouse_pos = Some(current_pos);
} else {
// Still update egui's pointer position even if not capturing
// (handled by on_window_event)
self.last_mouse_pos = None; // Reset if not capturing
}
}
// Use PhysicalKey for layout-independent keys
WindowEvent::KeyboardInput {
event:
KeyEvent {
physical_key,
state,
..
},
..
} => {
// Let egui handle keyboard input first if it wants it
if egui_consumed_event.consumed {
return;
}
let is_pressed = *state == ElementState::Pressed;
match physical_key {
PhysicalKey::Code(KeyCode::KeyW) | PhysicalKey::Code(KeyCode::ArrowUp) => {
self.is_forward_pressed = is_pressed;
}
PhysicalKey::Code(KeyCode::KeyS) | PhysicalKey::Code(KeyCode::ArrowDown) => {
self.is_backward_pressed = is_pressed;
}
PhysicalKey::Code(KeyCode::KeyA) | PhysicalKey::Code(KeyCode::ArrowLeft) => {
self.is_left_pressed = is_pressed;
}
PhysicalKey::Code(KeyCode::KeyD) | PhysicalKey::Code(KeyCode::ArrowRight) => {
self.is_right_pressed = is_pressed;
}
PhysicalKey::Code(KeyCode::Space) => {
self.is_up_pressed = is_pressed;
}
PhysicalKey::Code(KeyCode::ShiftLeft)
| PhysicalKey::Code(KeyCode::ShiftRight) => {
self.is_down_pressed = is_pressed;
}
// Optional: Escape to release mouse capture
PhysicalKey::Code(KeyCode::Escape) if is_pressed && self.capture_mouse => {
self.capture_mouse = false;
self.is_rmb_pressed = false; // Ensure RMB state is also reset
self.window
.set_cursor_grab(winit::window::CursorGrabMode::None)
.unwrap_or(());
self.window.set_cursor_visible(true);
self.mouse_delta = (0.0, 0.0);
}
_ => {}
}
}
WindowEvent::RedrawRequested => {
let now = Instant::now();
let _delta_time = now.duration_since(self.last_frame_time);
let delta_time = now.duration_since(self.last_frame_time).as_secs_f32();
self.last_frame_time = now;
let elapsed_sice_last_update = now.duration_since(self.last_fps_update_time);
// --- FPS Calculation ---
let elapsed_since_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);
if elapsed_since_last_update >= Duration::from_secs(1) {
self.current_fps =
self.frame_count as f64 / elapsed_since_last_update.as_secs_f64();
let new_title = format!(
"{} - {} - {:.0} FPS",
ENGINE_NAME, APP_NAME, self.current_fps
);
self.window.set_title(&new_title);
self.frame_count = 0;
self.last_fps_update_time = now;
}
self.update_camera(delta_time); // Call the new update function
let raw_input = self.egui_winit.take_egui_input(&self.window);
let egui::FullOutput {
platform_output,
textures_delta,
shapes,
pixels_per_point,
..
} = self.egui_ctx.run(raw_input, |ctx| {
self.egui_app
.build_ui(ctx, self.current_fps, &mut self.camera_info);
});
self.renderer.update_textures(textures_delta).unwrap();
self.egui_winit
.handle_platform_output(&self.window, platform_output);
let clipped_primitives = self.egui_ctx.tessellate(shapes, pixels_per_point);
// --- Render Frame ---
match self.renderer.render_frame() {
match self.renderer.render_frame(
pixels_per_point,
&clipped_primitives,
self.camera_info,
) {
Ok(_) => {
self.window.request_redraw();
}
@ -343,6 +615,106 @@ impl Application {
_ => {}
}
}
// --- New Camera Update Function ---
fn update_camera(&mut self, dt: f32) {
if !self.capture_mouse
&& self.mouse_delta == (0.0, 0.0)
&& !self.is_forward_pressed
&& !self.is_backward_pressed
&& !self.is_left_pressed
&& !self.is_right_pressed
&& !self.is_up_pressed
&& !self.is_down_pressed
{
return; // No input, no update needed
}
let mut cam_pos = self.camera_info.camera_pos;
let mut cam_target = self.camera_info.camera_target;
let cam_up = self.camera_info.camera_up; // Usually Vec3::Y
// --- Mouse Look (Rotation) ---
if self.capture_mouse && self.mouse_delta != (0.0, 0.0) {
let (delta_x, delta_y) = self.mouse_delta;
self.mouse_delta = (0.0, 0.0); // Consume the delta
let sensitivity = self.camera_sensitivity;
let yaw_delta = delta_x as f32 * sensitivity;
let pitch_delta = delta_y as f32 * sensitivity;
let forward_dir = (cam_target - cam_pos).normalize();
let right_dir = forward_dir.cross(cam_up).normalize();
// Recalculate up to prevent roll if needed, though cross product handles it here
let current_up = right_dir.cross(forward_dir).normalize();
// --- Pitch (Up/Down) ---
// Calculate new forward direction based on pitch rotation around right axis
let pitch_quat = glam::Quat::from_axis_angle(right_dir, -pitch_delta); // Negative for standard mouse look
let mut new_forward = pitch_quat * forward_dir;
// Clamp pitch to avoid flipping over (e.g., +/- 89 degrees)
let max_pitch_angle = 89.0f32.to_radians();
let current_pitch = new_forward.angle_between(cam_up) - 90.0f32.to_radians();
if current_pitch.abs() > max_pitch_angle {
// Revert pitch if it exceeds limits
new_forward = forward_dir; // Keep previous forward if clamp needed
}
// --- Yaw (Left/Right) ---
// Rotate the (potentially pitch-adjusted) forward direction and right vector around the global up axis (Y)
let yaw_quat = glam::Quat::from_axis_angle(Vec3::Y, -yaw_delta); // Negative for standard mouse look
new_forward = yaw_quat * new_forward;
// Update target based on the new forward direction
cam_target = cam_pos + new_forward;
// Update the camera's internal up vector based on yaw rotation as well
// This prevents weird tilting when looking straight up/down if up wasn't Vec3::Y
// self.camera_info.camera_up = yaw_quat * current_up; // Optional: only if up can change
}
// --- Keyboard Movement ---
let forward_dir = (cam_target - cam_pos).normalize();
// Use Vec3::Y for world-relative right/up movement, or calculate from forward/up
let right_dir = forward_dir.cross(Vec3::Y).normalize();
// let up_dir = right_dir.cross(forward_dir).normalize(); // Camera's local up
let world_up_dir = Vec3::Y; // Use world up for space/shift
let effective_speed = self.camera_speed * dt;
let mut move_delta = Vec3::ZERO;
if self.is_forward_pressed {
move_delta += forward_dir;
}
if self.is_backward_pressed {
move_delta -= forward_dir;
}
if self.is_left_pressed {
move_delta -= right_dir;
}
if self.is_right_pressed {
move_delta += right_dir;
}
if self.is_up_pressed {
move_delta += world_up_dir; // Move along world Y
}
if self.is_down_pressed {
move_delta -= world_up_dir; // Move along world Y
}
// Normalize move_delta if non-zero to ensure consistent speed diagonally
if move_delta != Vec3::ZERO {
let move_vec = move_delta.normalize() * effective_speed;
cam_pos += move_vec;
cam_target += move_vec; // Move target along with position
}
// --- Apply Changes ---
self.camera_info.camera_pos = cam_pos;
self.camera_info.camera_target = cam_target;
// self.camera_info.camera_up remains Vec3::Y usually
}
}
// --- Helper Functions ---
@ -450,6 +822,7 @@ struct Args {
// --- Entry Point ---
fn main() -> Result<(), Box<dyn Error>> {
color_eyre::install()?;
let args = Args::parse();
let fmt_layer = tracing_subscriber::fmt::layer()

View file

@ -9,4 +9,4 @@ ash-window.workspace = true
thiserror.workspace = true
tracing.workspace = true
winit.workspace = true
parking_lot.workspace = true
gpu-allocator.workspace = true

View file

@ -1,9 +1,10 @@
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 std::{
collections::HashMap,
sync::{Arc, Mutex},
};
use crate::error::{GfxHalError, Result};
use crate::instance::Instance;
@ -14,7 +15,7 @@ use crate::queue::Queue;
///
/// Owns the `ash::Device` and provides access to device functions and queues.
pub struct Device {
instance: Arc<Instance>,
_instance: Arc<Instance>,
physical_device: vk::PhysicalDevice,
device: ash::Device,
queues: Mutex<HashMap<(u32, u32), Arc<Queue>>>,
@ -32,6 +33,7 @@ impl Device {
/// - `queue_family_indicies` must be valid indicies obtained from the `physical_device_handle`.
/// - `required_extensions` must be supported by the `physical_device_handle`.
/// - All feature structs passed must be supported by the `physical_device_handle`.
#[allow(clippy::too_many_arguments)]
pub(crate) unsafe fn new(
instance: Arc<Instance>,
physical_device_handle: vk::PhysicalDevice,
@ -112,7 +114,7 @@ impl Device {
// --- 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(),
_instance: instance.clone(),
physical_device: physical_device_handle,
device: ash_device, // Move the created ash::Device here
queues: Mutex::new(HashMap::new()), // Start with empty map
@ -146,7 +148,7 @@ impl Device {
// Lock the mutex and insert the created queues into the map within the Arc<Device>
{
// Scope for the mutex guard
let mut queues_map_guard = device_arc.queues.lock();
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).",
@ -185,15 +187,21 @@ impl Device {
/// Gets a wrapped queue handle.
/// Currently only supports queue index 0 for each family.
pub fn get_queue(&self, family_index: u32, queue_index: u32) -> Option<Arc<Queue>> {
pub fn get_queue(&self, family_index: u32, queue_index: u32) -> Result<Arc<Queue>> {
if queue_index != 0 {
tracing::warn!("get_queue currently only supports queue_index 0");
return None;
return Err(GfxHalError::MissingQueueFamily(
"get_queue only supports queue_index 0".to_string(),
));
}
self.queues
.lock()
.lock()?
.get(&(family_index, queue_index))
.cloned()
.ok_or(GfxHalError::MissingQueueFamily(
"could not get queue family".to_string(),
))
}
/// Gets the primary graphics queue (family index from `graphics_queue_family_index`, queue index 0).

View file

@ -56,9 +56,23 @@ pub enum GfxHalError {
#[error("Error loading the ash entry.")]
AshEntryError(#[from] ash::LoadingError),
/// Poisoned Mutex
#[error("Error from poisoned mutex: {0}")]
MutexPoisoned(String),
/// Placeholder for other specific errors.
#[error("An unexpected error occurred: {0}")]
Other(String),
/// Size for Buffer is invalid.
#[error("Buffer size is invalid.")]
BufferSizeInvalid,
}
pub type Result<T, E = GfxHalError> = std::result::Result<T, E>;
impl<T> From<std::sync::PoisonError<T>> for GfxHalError {
fn from(e: std::sync::PoisonError<T>) -> Self {
Self::MutexPoisoned(e.to_string())
}
}

View file

@ -6,3 +6,12 @@ pub mod queue;
pub mod surface;
pub mod swapchain;
pub mod sync;
pub use device::*;
pub use error::*;
pub use instance::*;
pub use physical_device::*;
pub use queue::*;
pub use surface::*;
pub use swapchain::*;
pub use sync::*;

View file

@ -1,7 +1,6 @@
use std::sync::Arc;
use std::sync::{Arc, Mutex};
use ash::{vk, Device as AshDevice};
use parking_lot::Mutex;
use crate::device::Device;
use crate::error::Result;
@ -66,18 +65,6 @@ impl Queue {
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

View file

@ -10,6 +10,7 @@ use crate::{
/// Wraps a `vk::Fence`, used for CPU-GPU synchronization.
///
/// Owns the `vk::Fence` handle.
#[derive(Clone)]
pub struct Fence {
device: Arc<Device>,
fence: vk::Fence,

View file

@ -13,10 +13,11 @@ 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" }
shared = { path = "../shared" }
scene = { path = "../scene" }
[build-dependencies]
shaderc = "0.9.1"

View file

@ -16,6 +16,8 @@ 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")?;
println!("cargo:rerun-if-changed=build.rs");
let compiler = Compiler::new().context("Failed to create shader compiler")?;
let mut options = CompileOptions::new().context("Failed to create compile options")?;
@ -49,6 +51,7 @@ fn main() -> Result<()> {
.filter(|e| e.file_type().is_file())
// Only process files
{
println!("cargo:rerun-if-changed={:?}", entry.path());
let in_path = entry.path();
// Determine shader kind from extension

View file

@ -1,21 +1,32 @@
use std::{ffi::CStr, sync::Arc};
use std::{
collections::HashMap,
ffi::c_void,
mem,
sync::{Arc, Mutex},
time::Instant,
};
use ash::vk;
use egui::{ClippedPrimitive, TextureId, TexturesDelta};
use egui_ash_renderer::{DynamicRendering, Options, Renderer as EguiRenderer};
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 glam::{Mat4, Vec3};
use gpu_allocator::{
vulkan::{Allocation, AllocationCreateDesc, Allocator},
MemoryLocation,
};
use resource_manager::{
ImageHandle, Material, ResourceManager, ResourceManagerError, SamplerHandle, Texture,
};
use shared::{CameraInfo, UniformBufferObject};
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;
const MAX_MATERIALS: usize = 150;
#[derive(Debug, Error)]
pub enum RendererError {
@ -51,6 +62,17 @@ pub enum RendererError {
ImageInfoUnavailable,
#[error("Failed to get allocator from resource manager")]
AllocatorUnavailable, // Added based on egui requirement
#[error("Allocator Error: {0}")]
AllocatorError(#[from] gpu_allocator::AllocationError),
#[error("Other Error: {0}")]
Other(String),
}
impl<T> From<std::sync::PoisonError<T>> for RendererError {
fn from(_: std::sync::PoisonError<T>) -> Self {
Self::AllocatorUnavailable
}
}
struct FrameData {
@ -58,7 +80,14 @@ struct FrameData {
command_buffer: vk::CommandBuffer,
image_available_semaphore: Semaphore,
render_finished_semaphore: Semaphore,
textures_to_free: Option<Vec<TextureId>>,
in_flight_fence: Fence,
descriptor_set: vk::DescriptorSet,
uniform_buffer_object: UniformBufferObject,
uniform_buffer: vk::Buffer,
uniform_buffer_allocation: Allocation,
uniform_buffer_mapped_ptr: *mut c_void,
}
struct SwapchainSupportDetails {
@ -79,12 +108,26 @@ pub struct Renderer {
swapchain_format: vk::SurfaceFormatKHR,
swapchain_extent: vk::Extent2D,
scene: scene::Scene,
descriptor_set_layout: vk::DescriptorSetLayout,
descriptor_pool: vk::DescriptorPool,
material_descriptor_set_layout: vk::DescriptorSetLayout,
egui_renderer: EguiRenderer,
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,
model_pipeline_layout: vk::PipelineLayout,
model_pipeline: vk::Pipeline,
material_descriptor_sets: HashMap<usize, vk::DescriptorSet>,
default_white_texture: Option<Arc<Texture>>,
default_sampler: SamplerHandle,
frames_data: Vec<FrameData>,
current_frame: usize,
@ -93,15 +136,19 @@ pub struct Renderer {
window_resized: bool,
current_width: u32,
current_height: u32,
start_time: Instant,
}
impl Renderer {
#[allow(clippy::too_many_arguments)]
pub fn new(
instance: Arc<gfx_hal::instance::Instance>, // Needed for allocator
device: Arc<Device>,
graphics_queue: Arc<Queue>,
surface: Arc<Surface>,
resource_manager: Arc<ResourceManager>,
scene: scene::Scene,
initial_width: u32,
initial_height: u32,
) -> Result<Self, RendererError> {
@ -121,33 +168,215 @@ impl Renderer {
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 descriptor_set_layout = Self::create_descriptor_set_layout(&device)?;
let material_descriptor_set_layout = Self::create_material_descriptor_set_layout(&device)?;
let frames_data = Self::create_frame_data(&device)?;
let descriptor_set_layouts = [descriptor_set_layout, material_descriptor_set_layout];
let descriptor_pool = Self::create_descriptor_pool(&device)?;
let (model_pipeline_layout, model_pipeline) = Self::create_model_pipeline(
&device,
format.format,
depth_format,
&descriptor_set_layouts,
)?;
let start_time = Instant::now();
let frames_data = Self::create_frame_data(
&device,
&resource_manager,
descriptor_pool,
&descriptor_set_layouts,
swapchain.extent(),
)?;
info!("Renderer initialized successfully.");
let egui_renderer = EguiRenderer::with_gpu_allocator(
resource_manager.allocator(),
device.raw().clone(),
DynamicRendering {
color_attachment_format: swapchain.format().format,
depth_attachment_format: Some(depth_format),
},
Options {
srgb_framebuffer: true,
in_flight_frames: MAX_FRAMES_IN_FLIGHT,
..Default::default()
},
)?;
let default_sampler = resource_manager.get_or_create_sampler(&Default::default())?;
let default_white_texture = Some(Self::create_default_texture(
device.clone(),
resource_manager.clone(),
));
Ok(Self {
device,
graphics_queue,
resource_manager,
egui_renderer,
allocator, // Store the allocator Arc
surface,
swapchain: Some(swapchain),
swapchain_image_views: image_views,
swapchain_format: format,
swapchain_extent: extent,
descriptor_set_layout,
descriptor_pool,
material_descriptor_set_layout,
depth_image_handle,
depth_image_view,
depth_format,
triangle_pipeline_layout,
triangle_pipeline,
model_pipeline_layout,
model_pipeline,
material_descriptor_sets: HashMap::new(),
default_white_texture,
default_sampler,
frames_data,
scene,
current_frame: 0,
window_resized: false,
current_width: initial_width,
current_height: initial_height,
start_time,
})
}
/// Gets or creates/updates a descriptor set for a given material.
fn get_or_create_material_set(
&mut self,
material: &Arc<Material>, // Use Arc<Material> directly if hashable, or use a unique ID
) -> Result<vk::DescriptorSet, RendererError> {
// Return generic error
// Use a unique identifier for the material instance if Arc<Material> isn't directly hashable
// or if pointer comparison isn't reliable across runs/reloads.
// For simplicity here, we use the Arc's pointer address as a key.
// WARNING: This is only safe if the Arc<Material> instances are stable!
// A better key might be derived from material.name or a generated ID.
let material_key = Arc::as_ptr(material) as usize;
if let Some(set) = self.material_descriptor_sets.get(&material_key) {
return Ok(*set);
}
// --- Allocate Descriptor Set ---
let layouts = [self.material_descriptor_set_layout];
let alloc_info = vk::DescriptorSetAllocateInfo::default()
.descriptor_pool(self.descriptor_pool)
.set_layouts(&layouts);
let descriptor_set = unsafe { self.device.raw().allocate_descriptor_sets(&alloc_info)? }[0];
// --- Update Descriptor Set ---
let (image_handle, view_handle, sampler_handle) = match &material.base_color_texture {
Some(texture) => {
// Get the default view handle associated with the image
let img_info = self.resource_manager.get_image_info(texture.handle)?;
let view_h = img_info.default_view_handle.ok_or(RendererError::Other(
"Image missing default view handle".to_string(),
))?;
// Use the sampler specified by the material, or the default
let sampler_h = material.base_color_sampler.unwrap_or(self.default_sampler);
(texture.handle, view_h, sampler_h)
}
None => {
// Use default white texture
let default_tex =
self.default_white_texture
.as_ref()
.ok_or(RendererError::Other(
"Default texture not created".to_string(),
))?;
let img_info = self.resource_manager.get_image_info(default_tex.handle)?;
let view_h = img_info.default_view_handle.ok_or(RendererError::Other(
"Default image missing default view handle".to_string(),
))?;
(default_tex.handle, view_h, self.default_sampler)
}
};
// Get the actual Vulkan handles
let image_view_info = self.resource_manager.get_image_view_info(view_handle)?;
let sampler_info = self.resource_manager.get_sampler_info(sampler_handle)?;
let image_descriptor_info = vk::DescriptorImageInfo::default()
.image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL) // Expected layout for sampling
.image_view(image_view_info.view) // The vk::ImageView
.sampler(sampler_info.sampler); // The vk::Sampler
let writes = [
// Write for binding 0 (baseColorSampler)
vk::WriteDescriptorSet::default()
.dst_set(descriptor_set)
.dst_binding(0)
.dst_array_element(0)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.image_info(std::slice::from_ref(&image_descriptor_info)),
// Add writes for other bindings (normal map, etc.) here
];
unsafe {
self.device.raw().update_descriptor_sets(&writes, &[]); // Update the set
}
// Store in cache
self.material_descriptor_sets
.insert(material_key, descriptor_set);
Ok(descriptor_set)
}
fn create_default_texture(
device: Arc<Device>, // Need device Arc for RM
resource_manager: Arc<ResourceManager>,
) -> Arc<Texture> {
let width = 1;
let height = 1;
let data = [255u8, 255, 255, 255]; // White RGBA
let format = vk::Format::R8G8B8A8_UNORM; // Or SRGB if preferred
let create_info = vk::ImageCreateInfo::default()
.image_type(vk::ImageType::TYPE_2D)
.format(format)
.extent(vk::Extent3D {
width,
height,
depth: 1,
})
.mip_levels(1)
.array_layers(1)
.samples(vk::SampleCountFlags::TYPE_1)
.tiling(vk::ImageTiling::OPTIMAL)
.usage(vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::TRANSFER_DST)
.initial_layout(vk::ImageLayout::UNDEFINED);
let handle = resource_manager
.create_image_init(
&create_info,
gpu_allocator::MemoryLocation::GpuOnly,
vk::ImageAspectFlags::COLOR,
&data,
)
.expect("Failed to create default white texture");
Arc::new(Texture {
handle,
format: vk::Format::R8G8B8A8_UNORM,
extent: vk::Extent3D {
width: 1,
height: 1,
depth: 1,
},
})
}
@ -162,7 +391,33 @@ impl Renderer {
}
}
pub fn render_frame(&mut self) -> Result<(), RendererError> {
pub fn update_textures(&mut self, textures_delta: TexturesDelta) -> Result<(), RendererError> {
tracing::trace!("Updating EGUI textures!");
if !textures_delta.free.is_empty() {
self.frames_data[self.current_frame].textures_to_free =
Some(textures_delta.free.clone());
}
if !textures_delta.set.is_empty() {
self.egui_renderer
.set_textures(
self.device.get_graphics_queue().handle(),
self.frames_data[self.current_frame].command_pool,
textures_delta.set.as_slice(),
)
.expect("Failed to update texture");
}
Ok(())
}
pub fn render_frame(
&mut self,
pixels_per_point: f32,
clipped_primitives: &[ClippedPrimitive],
camera_info: CameraInfo,
) -> Result<(), RendererError> {
// --- Handle Resize ---
if self.window_resized {
self.window_resized = false;
@ -174,7 +429,7 @@ impl Renderer {
// --- Wait for Previous Frame ---
let frame_index = self.current_frame;
let frame_data = &self.frames_data[frame_index];
let frame_data = &mut self.frames_data[frame_index];
frame_data.in_flight_fence.wait(None)?; // Wait indefinitely
@ -183,6 +438,7 @@ impl Renderer {
.swapchain
.as_ref()
.ok_or(RendererError::SwapchainAcquisitionFailed)?;
let (image_index, suboptimal) = unsafe {
// Need unsafe block for acquire_next_image
swapchain_ref.acquire_next_image(
@ -203,6 +459,10 @@ impl Renderer {
// --- Reset Fence (only after successful acquisition) ---
frame_data.in_flight_fence.reset()?;
if let Some(textures) = frame_data.textures_to_free.take() {
self.egui_renderer.free_textures(&textures)?;
}
// --- Record Command Buffer ---
unsafe {
// Need unsafe for Vulkan commands
@ -215,6 +475,15 @@ impl Renderer {
let cmd_begin_info = vk::CommandBufferBeginInfo::default()
.flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT);
// -- Update uniform buffer --
self.update_uniform_buffer(camera_info)?;
let frame_data = &mut self.frames_data[self.current_frame];
let swapchain_ref = self
.swapchain
.as_ref()
.ok_or(RendererError::SwapchainAcquisitionFailed)?;
unsafe {
// Need unsafe for Vulkan commands
self.device
@ -316,18 +585,74 @@ impl Renderer {
.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,
self.model_pipeline,
);
self.device.raw().cmd_bind_descriptor_sets(
command_buffer,
vk::PipelineBindPoint::GRAPHICS,
self.model_pipeline_layout,
0,
&[frame_data.descriptor_set],
&[],
);
// Draw 3 vertices, 1 instance, 0 first vertex, 0 first instance
self.device.raw().cmd_draw(command_buffer, 3, 1, 0, 0);
}
let meshes = self.scene.meshes.clone();
for mesh in meshes {
let material_set = self.get_or_create_material_set(&mesh.material)?;
unsafe {
self.device.raw().cmd_bind_descriptor_sets(
command_buffer,
vk::PipelineBindPoint::GRAPHICS,
self.model_pipeline_layout,
1,
&[material_set],
&[],
);
}
let model_matrix_bytes = unsafe {
std::slice::from_raw_parts(
mesh.transform.as_ref().as_ptr() as *const u8,
std::mem::size_of::<Mat4>(),
)
};
unsafe {
self.device.raw().cmd_push_constants(
command_buffer,
self.model_pipeline_layout,
vk::ShaderStageFlags::VERTEX,
0,
model_matrix_bytes,
);
}
mesh.geometry.draw(self.device.raw(), command_buffer)?;
}
let frame_data = &mut self.frames_data[self.current_frame];
let swapchain_ref = self
.swapchain
.as_ref()
.ok_or(RendererError::SwapchainAcquisitionFailed)?;
tracing::trace!("Rendering EGUI");
self.egui_renderer.cmd_draw(
command_buffer,
self.swapchain_extent,
pixels_per_point,
clipped_primitives,
)?;
tracing::trace!("Rendered EGUI");
// --- End Dynamic Rendering ---
unsafe {
// Need unsafe for Vulkan commands
@ -382,12 +707,6 @@ impl Renderer {
.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(
@ -475,7 +794,7 @@ impl Renderer {
// --- Helper: Cleanup Swapchain Dependent Resources ---
fn cleanup_swapchain_resources(&mut self) {
debug!("Cleaning up swapchain resources...");
// Destroy depth buffer view
unsafe {
self.device
.raw()
@ -484,9 +803,8 @@ impl Renderer {
// 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.");
}
@ -533,38 +851,10 @@ impl Renderer {
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))
}
@ -618,33 +908,12 @@ impl Renderer {
}
// --- Helper: Create Triangle Pipeline ---
fn create_triangle_pipeline(
fn create_model_pipeline(
device: &Arc<Device>,
color_format: vk::Format,
depth_format: vk::Format,
descriptor_set_layouts: &[vk::DescriptorSetLayout],
) -> 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
@ -652,7 +921,7 @@ impl Renderer {
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 main_function_name = c"main";
let vert_stage_info = vk::PipelineShaderStageCreateInfo::default()
.stage(vk::ShaderStageFlags::VERTEX)
@ -666,8 +935,13 @@ impl Renderer {
let shader_stages = [vert_stage_info, frag_stage_info];
let binding_description = shared::Vertex::get_binding_decription();
let attribute_descriptions = shared::Vertex::get_attribute_descriptions();
// --- Fixed Function State ---
let vertex_input_info = vk::PipelineVertexInputStateCreateInfo::default(); // No vertex buffers/attributes
let vertex_input_info = vk::PipelineVertexInputStateCreateInfo::default()
.vertex_binding_descriptions(std::slice::from_ref(&binding_description))
.vertex_attribute_descriptions(&attribute_descriptions);
let input_assembly = vk::PipelineInputAssemblyStateCreateInfo::default()
.topology(vk::PrimitiveTopology::TRIANGLE_LIST)
@ -709,8 +983,15 @@ impl Renderer {
let dynamic_state =
vk::PipelineDynamicStateCreateInfo::default().dynamic_states(&dynamic_states);
let push_constant_range = vk::PushConstantRange::default()
.stage_flags(vk::ShaderStageFlags::VERTEX)
.offset(0)
.size(mem::size_of::<Mat4>() as u32);
// --- Pipeline Layout ---
let layout_info = vk::PipelineLayoutCreateInfo::default(); // No descriptors/push constants
let layout_info = vk::PipelineLayoutCreateInfo::default()
.set_layouts(descriptor_set_layouts)
.push_constant_ranges(std::slice::from_ref(&push_constant_range));
let pipeline_layout = unsafe {
device
.raw()
@ -794,7 +1075,7 @@ impl Renderer {
// --------------------------------------------------------------------
// 3. Create the shader module
let create_info = vk::ShaderModuleCreateInfo::default().code(&code_slice_ref); // Pass the &[u32] slice
let create_info = vk::ShaderModuleCreateInfo::default().code(code_slice_ref); // Pass the &[u32] slice
unsafe {
device
@ -808,7 +1089,13 @@ impl Renderer {
}
// --- Helper: Create Frame Sync Objects & Command Resources ---
fn create_frame_data(device: &Arc<Device>) -> Result<Vec<FrameData>, RendererError> {
fn create_frame_data(
device: &Arc<Device>,
resource_manager: &Arc<ResourceManager>,
descriptor_pool: vk::DescriptorPool,
descriptor_set_layouts: &[vk::DescriptorSetLayout],
swapchain_extent: vk::Extent2D,
) -> Result<Vec<FrameData>, RendererError> {
let mut frames_data = Vec::with_capacity(MAX_FRAMES_IN_FLIGHT);
for _ in 0..MAX_FRAMES_IN_FLIGHT {
let image_available_semaphore = Semaphore::new(device.clone())?;
@ -841,12 +1128,30 @@ impl Renderer {
.map_err(RendererError::CommandBufferAllocation)?[0]
};
tracing::info!("Allocated frame_data command_buffer: {:?}", command_buffer);
let descriptor_set =
Self::create_descriptor_set(device, descriptor_set_layouts, descriptor_pool)?;
let (uniform_buffer, uniform_buffer_allocation, uniform_buffer_mapped_ptr) =
Self::create_uniform_buffer(device, resource_manager)?;
Self::update_descriptor_set(device.clone(), descriptor_set, uniform_buffer);
let uniform_buffer_object = calculate_ubo(CameraInfo::default(), swapchain_extent);
frames_data.push(FrameData {
textures_to_free: None,
command_pool,
command_buffer, // Stays allocated, just reset/rerecorded
image_available_semaphore,
render_finished_semaphore,
in_flight_fence,
descriptor_set,
uniform_buffer,
uniform_buffer_allocation,
uniform_buffer_mapped_ptr,
uniform_buffer_object,
});
}
Ok(frames_data)
@ -871,22 +1176,20 @@ impl Renderer {
}
fn choose_swapchain_format(available_formats: &[vk::SurfaceFormatKHR]) -> vk::SurfaceFormatKHR {
available_formats
*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()
.unwrap_or(&available_formats[0])
}
fn choose_swapchain_present_mode(available_modes: &[vk::PresentModeKHR]) -> vk::PresentModeKHR {
available_modes
*available_modes
.iter()
.find(|&&mode| mode == vk::PresentModeKHR::MAILBOX) // Prefer Mailbox (low latency)
.unwrap_or(&vk::PresentModeKHR::FIFO) // Guaranteed fallback
.clone()
.find(|&&mode| mode == vk::PresentModeKHR::FIFO) // Prefer Mailbox (low latency)
.unwrap_or(&vk::PresentModeKHR::FIFO)
}
fn choose_swapchain_extent(
@ -937,8 +1240,197 @@ impl Renderer {
}
Err(RendererError::Vulkan(
vk::Result::ERROR_FORMAT_NOT_SUPPORTED,
)) // Or custom error
))
}
fn create_material_descriptor_set_layout(
device: &Arc<Device>,
) -> Result<vk::DescriptorSetLayout, RendererError> {
let bindings = [
// Binding 0: Combined Image Sampler (baseColorSampler)
vk::DescriptorSetLayoutBinding::default()
.binding(0)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.descriptor_count(1)
.stage_flags(vk::ShaderStageFlags::FRAGMENT), // Used in fragment shader
// Add more bindings here if needed (e.g., for normal map, metallic/roughness map)
// Binding 1: Uniform Buffer (Optional: for material factors)
// vk::DescriptorSetLayoutBinding::default()
// .binding(1)
// .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
// .descriptor_count(1)
// .stage_flags(vk::ShaderStageFlags::FRAGMENT),
];
let layout_info = vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings);
Ok(unsafe {
device
.raw()
.create_descriptor_set_layout(&layout_info, None)?
})
}
fn create_descriptor_set_layout(
device: &Arc<Device>,
) -> Result<vk::DescriptorSetLayout, RendererError> {
let ubo_layout_binding = vk::DescriptorSetLayoutBinding::default()
.binding(0)
.descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
.descriptor_count(1)
.stage_flags(vk::ShaderStageFlags::VERTEX);
let layout_info = vk::DescriptorSetLayoutCreateInfo::default()
.bindings(std::slice::from_ref(&ubo_layout_binding));
let descriptor_set_layout = unsafe {
device
.raw()
.create_descriptor_set_layout(&layout_info, None)?
};
Ok(descriptor_set_layout)
}
fn create_descriptor_pool(device: &Arc<Device>) -> Result<vk::DescriptorPool, RendererError> {
let pool_sizes = [
vk::DescriptorPoolSize {
ty: vk::DescriptorType::UNIFORM_BUFFER,
descriptor_count: MAX_FRAMES_IN_FLIGHT as u32,
},
vk::DescriptorPoolSize {
ty: vk::DescriptorType::COMBINED_IMAGE_SAMPLER,
descriptor_count: MAX_MATERIALS as u32,
},
];
let pool_info = vk::DescriptorPoolCreateInfo::default()
.pool_sizes(&pool_sizes)
.max_sets(MAX_FRAMES_IN_FLIGHT as u32 + MAX_MATERIALS as u32);
let descriptor_pool = unsafe { device.raw().create_descriptor_pool(&pool_info, None)? };
Ok(descriptor_pool)
}
fn create_descriptor_set(
device: &Arc<Device>,
descriptor_set_layouts: &[vk::DescriptorSetLayout],
descriptor_pool: vk::DescriptorPool,
) -> Result<vk::DescriptorSet, RendererError> {
let alloc_info = vk::DescriptorSetAllocateInfo::default()
.descriptor_pool(descriptor_pool)
.set_layouts(descriptor_set_layouts);
let descriptor_set = unsafe { device.raw().allocate_descriptor_sets(&alloc_info)? }[0];
Ok(descriptor_set)
}
fn create_uniform_buffer(
device: &Arc<Device>,
resource_manager: &Arc<ResourceManager>,
) -> Result<(vk::Buffer, Allocation, *mut std::ffi::c_void), RendererError> {
let buffer_size = mem::size_of::<UniformBufferObject>() as vk::DeviceSize;
let buffer_info = vk::BufferCreateInfo::default()
.size(buffer_size)
.usage(vk::BufferUsageFlags::UNIFORM_BUFFER)
.sharing_mode(vk::SharingMode::EXCLUSIVE);
let allocation = resource_manager
.allocator()
.lock()?
.allocate(&AllocationCreateDesc {
name: "Uniform Buffer",
requirements: unsafe {
{
let temp_buffer = device.raw().create_buffer(&buffer_info, None)?;
let req = device.raw().get_buffer_memory_requirements(temp_buffer);
device.raw().destroy_buffer(temp_buffer, None);
req
}
},
location: MemoryLocation::CpuToGpu,
linear: true,
allocation_scheme: gpu_allocator::vulkan::AllocationScheme::GpuAllocatorManaged,
})?;
let buffer = unsafe { device.raw().create_buffer(&buffer_info, None)? };
tracing::info!("Created uniform buffer {:?}", buffer);
unsafe {
device
.raw()
.bind_buffer_memory(buffer, allocation.memory(), allocation.offset())?;
}
let mapped_ptr = allocation
.mapped_ptr()
.ok_or_else(|| {
error!("Failed to get mapped pointer for CPU->GPU uniform buffer");
ResourceManagerError::Other("Failed to map uniform buffer".to_string())
})?
.as_ptr();
Ok((buffer, allocation, mapped_ptr))
}
fn update_descriptor_set(
device: Arc<Device>,
descriptor_set: vk::DescriptorSet,
buffer: vk::Buffer,
) {
let buffer_info = vk::DescriptorBufferInfo::default()
.buffer(buffer)
.offset(0)
.range(mem::size_of::<UniformBufferObject>() as vk::DeviceSize);
let descriptor_write = vk::WriteDescriptorSet::default()
.dst_set(descriptor_set)
.dst_binding(0)
.dst_array_element(0)
.descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
.buffer_info(std::slice::from_ref(&buffer_info));
unsafe {
device
.raw()
.update_descriptor_sets(std::slice::from_ref(&descriptor_write), &[]);
}
}
fn update_uniform_buffer(&mut self, camera_info: CameraInfo) -> Result<(), RendererError> {
let frame_data = &mut self.frames_data[self.current_frame];
let ubo = calculate_ubo(camera_info, self.swapchain_extent);
if frame_data.uniform_buffer_object != ubo {
let ptr = frame_data.uniform_buffer_mapped_ptr;
unsafe {
let aligned_ptr = ptr as *mut UniformBufferObject;
aligned_ptr.write(ubo);
}
}
Ok(())
}
}
fn calculate_ubo(camera_info: CameraInfo, swapchain_extent: vk::Extent2D) -> UniformBufferObject {
let view = Mat4::look_at_rh(camera_info.camera_pos, camera_info.camera_target, Vec3::Y);
let mut proj = Mat4::perspective_rh(
camera_info.camera_fov.to_radians(),
swapchain_extent.width as f32 / swapchain_extent.height as f32,
0.1,
1000.0,
);
proj.y_axis.y *= -1.0;
UniformBufferObject { view, proj }
}
// --- Drop Implementation ---
@ -962,16 +1454,39 @@ impl Drop for Renderer {
unsafe {
self.device
.raw()
.destroy_pipeline(self.triangle_pipeline, None);
.destroy_pipeline(self.model_pipeline, None);
self.device
.raw()
.destroy_pipeline_layout(self.triangle_pipeline_layout, None);
.destroy_pipeline_layout(self.model_pipeline_layout, None);
}
unsafe {
self.device
.raw()
.destroy_descriptor_pool(self.descriptor_pool, None);
self.device
.raw()
.destroy_descriptor_set_layout(self.descriptor_set_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_buffer(frame_data.uniform_buffer, None);
let mut allocator = self
.allocator
.lock()
.expect("Allocator Mutex to not be poisoned.");
allocator
.free(frame_data.uniform_buffer_allocation)
.expect("Allocator to be able to free an allocation");
}
unsafe {
self.device
.raw()

View file

@ -7,7 +7,8 @@ edition = "2021"
ash.workspace = true
gpu-allocator.workspace = true
thiserror.workspace = true
parking_lot.workspace = true
tracing.workspace = true
gltf.workspace = true
gfx_hal = { path = "../gfx_hal" }
image = { version = "0.25.6", features = ["rayon"] }

View file

@ -34,6 +34,12 @@ pub enum ResourceManagerError {
#[error("Error occurred in GfxHal: {0}")]
GfxHalError(#[from] gfx_hal::error::GfxHalError),
#[error("I/O Error occurred: {0}")]
Io(#[from] std::io::Error),
#[error("Image Error occurred: {0}")]
ImageError(#[from] image::ImageError),
#[error("An unexpected error occurred: {0}")]
Other(String),
}

View file

@ -0,0 +1,175 @@
use std::sync::Arc;
use ash::vk;
use gpu_allocator::MemoryLocation;
use tracing::{debug, trace};
use crate::{BufferHandle, ResourceManager, ResourceManagerError, Result};
// Helper to safely get a byte slice from structured data
unsafe fn as_byte_slice<T: Sized>(data: &[T]) -> &[u8] {
std::slice::from_raw_parts(data.as_ptr() as *const u8, std::mem::size_of_val(data))
}
/// Represents geometry data (verticies and indicies) stored in GPU buffers managed by
/// ResourceManager. Handles automatic cleanup via a `Drop` implementation.
#[derive(Clone)]
pub struct Geometry {
resource_manager: Arc<ResourceManager>,
pub vertex_buffer: BufferHandle,
pub index_buffer: BufferHandle,
pub index_count: u32,
}
impl Geometry {
/// Creates new GPU buffers for the given vetex and index data using `ResourceManager`.
///
/// # Arguments
///
/// * `resource_manager` - An Arc reference to the ResourceManager.
/// * `vertices` - A slice of vertex data.
/// * `indices` - A slice of index data (u32)
///
/// # Errors
///
/// Returns a new `ResourceManagerError` if buffer creation or data upload fails.
pub fn new<V: Sized + Copy>(
resource_manager: Arc<ResourceManager>,
vertices: &[V],
indicies: &[u32],
) -> Result<Self> {
trace!(
"Creating Geometry: {} vertices, {} indicies",
vertices.len(),
indicies.len()
);
if vertices.is_empty() || indicies.is_empty() {
return Err(ResourceManagerError::Other(
"Cannot create Geometry with empty vertices or indicies.".to_string(),
));
}
let vertex_buffer = resource_manager.create_buffer_init(
vk::BufferUsageFlags::VERTEX_BUFFER,
MemoryLocation::GpuOnly,
unsafe { as_byte_slice(vertices) },
)?;
trace!("Vertex buffer created: handle={:?}", vertex_buffer);
let index_buffer = resource_manager.create_buffer_init(
vk::BufferUsageFlags::INDEX_BUFFER,
MemoryLocation::GpuOnly,
unsafe { as_byte_slice(indicies) },
)?;
trace!("Index buffer created: handle={:?}", index_buffer);
let index_count = indicies.len() as u32;
debug!(
"Geometry created successfully: VB={:?}, IB={:?}, Indices={}",
vertex_buffer, index_buffer, index_count
);
Ok(Self {
resource_manager,
vertex_buffer,
index_buffer,
index_count,
// vertex_count,
})
}
/// Binds the vertex and index buffers for drawing.
///
/// # Arguments
///
/// * `device` - Raw `ash::Device` handle.
/// * `command_buffer` - The command buffer to record binding commands into.
///
/// # Errors
///
/// Returns `ResourceManagerError` if buffer info cannot be retrieved.
pub fn bind(&self, device: &ash::Device, command_buffer: vk::CommandBuffer) -> Result<()> {
trace!(
"Binding geometry: VB={:?}, IB={:?}",
self.vertex_buffer,
self.index_buffer
);
// Get buffer info (locks resource manager map briefly)
let vb_info = self.resource_manager.get_buffer_info(self.vertex_buffer)?;
let ib_info = self.resource_manager.get_buffer_info(self.index_buffer)?;
let vk_vertex_buffers = [vb_info.buffer];
let offsets = [0_u64]; // Use vk::DeviceSize (u64)
unsafe {
device.cmd_bind_vertex_buffers(
command_buffer,
0, // binding = 0
&vk_vertex_buffers,
&offsets,
);
device.cmd_bind_index_buffer(
command_buffer,
ib_info.buffer,
0, // offset = 0
vk::IndexType::UINT32,
);
}
Ok(())
}
/// Binds the geometry buffers and issues an indexed draw command.
///
/// # Arguments
///
/// * `device` - Raw `ash::Device` handle.
/// * `command_buffer` - The command buffer to record commands into.
///
/// # Errors
///
/// Returns `ResourceManagerError` if binding fails.
pub fn draw(&self, device: &ash::Device, command_buffer: vk::CommandBuffer) -> Result<()> {
self.bind(device, command_buffer)?; // Bind first
trace!("Drawing geometry: {} indices", self.index_count);
unsafe {
device.cmd_draw_indexed(
command_buffer,
self.index_count, // Use stored index count
1, // instance_count
0, // first_index
0, // vertex_offset
0, // first_instance
);
}
Ok(())
}
}
impl Drop for Geometry {
fn drop(&mut self) {
debug!(
"Dropping Geometry: VB={:?}, IB={:?}",
self.vertex_buffer, self.index_buffer
);
// Request destruction from the resource manager.
// Ignore errors during drop, but log them.
if let Err(e) = self.resource_manager.destroy_buffer(self.vertex_buffer) {
tracing::error!(
"Failed to destroy vertex buffer {:?} during Geometry drop: {}",
self.vertex_buffer,
e
);
}
if let Err(e) = self.resource_manager.destroy_buffer(self.index_buffer) {
tracing::error!(
"Failed to destroy index buffer {:?} during Geometry drop: {}",
self.index_buffer,
e
);
}
// The Arc<ResourceManager> reference count decreases automatically.
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,59 @@
use std::sync::Arc;
use ash::vk;
use crate::{ImageHandle, SamplerHandle};
#[derive(Debug, Clone)]
pub struct Texture {
pub handle: ImageHandle,
pub format: vk::Format,
pub extent: vk::Extent3D,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SamplerDesc {
pub mag_filter: vk::Filter,
pub min_filter: vk::Filter,
pub mipmap_mode: vk::SamplerMipmapMode,
pub address_mode_u: vk::SamplerAddressMode,
pub address_mode_v: vk::SamplerAddressMode,
pub address_mode_w: vk::SamplerAddressMode,
}
impl Default for SamplerDesc {
fn default() -> Self {
Self {
mag_filter: vk::Filter::LINEAR,
min_filter: vk::Filter::LINEAR,
mipmap_mode: vk::SamplerMipmapMode::LINEAR,
address_mode_u: vk::SamplerAddressMode::REPEAT,
address_mode_v: vk::SamplerAddressMode::REPEAT,
address_mode_w: vk::SamplerAddressMode::REPEAT,
}
}
}
#[derive(Debug, Clone)]
pub struct Material {
pub name: String,
pub base_color_texture: Option<Arc<Texture>>,
pub base_color_sampler: Option<SamplerHandle>,
pub base_color_factor: [f32; 4],
pub metallic_factor: f32,
pub roughness_factor: f32,
// TODO: Add other PBR properties:
// pub metallic_roughness_texture: Option<Arc<Texture>>,
// pub metallic_roughness_sampler: Option<SamplerHandle>,
// pub normal_texture: Option<Arc<Texture>>,
// pub normal_sampler: Option<SamplerHandle>,
// pub occlusion_texture: Option<Arc<Texture>>,
// pub occlusion_sampler: Option<SamplerHandle>,
// pub emissive_texture: Option<Arc<Texture>>,
// pub emissive_sampler: Option<SamplerHandle>,
// pub emissive_factor: [f32; 3],
// pub alpha_mode: gltf::material::AlphaMode,
// pub alpha_cutoff: f32,
// pub double_sided: bool,
}

14
crates/scene/Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "scene"
version = "0.1.0"
edition = "2021"
[dependencies]
ash.workspace = true
thiserror.workspace = true
tracing.workspace = true
glam.workspace = true
gltf.workspace = true
shared = { path = "../shared" }
resource_manager = { path = "../resource_manager" }

16
crates/scene/src/error.rs Normal file
View file

@ -0,0 +1,16 @@
use thiserror::Error;
/// Any errors that can be returned from this crate.
#[derive(Error, Debug)]
pub enum SceneError {
#[error("Error from ResourceManager: {0}")]
ResourceManagerError(#[from] resource_manager::ResourceManagerError),
#[error("Error from GLTF: {0}")]
GltfError(#[from] gltf::Error),
#[error("InconsistentData: {0}")]
InconsistentData(String),
}
pub type Result<T> = std::result::Result<T, SceneError>;

411
crates/scene/src/lib.rs Normal file
View file

@ -0,0 +1,411 @@
mod error;
use ash::vk;
pub use error::{Result, SceneError};
use glam::Mat4;
use shared::Vertex;
use std::{collections::HashMap, path::Path, sync::Arc};
use resource_manager::{Geometry, Material, ResourceManager, SamplerDesc, SamplerHandle, Texture};
/// Represents a drawable entity in the scene, storing geometry with its transform.
#[derive(Clone)]
pub struct Mesh {
pub name: String,
pub material: Arc<Material>,
pub geometry: Arc<Geometry>,
pub transform: Mat4,
}
/// Stores all objects to be rendered by the renderer.
pub struct Scene {
pub name: String,
pub meshes: Vec<Mesh>,
}
fn sampler_desc_from_gltf(g_sampler: &gltf::texture::Sampler) -> SamplerDesc {
let wrap_s = g_sampler.wrap_s();
let wrap_t = g_sampler.wrap_t();
SamplerDesc {
mag_filter: g_sampler
.mag_filter()
.map_or(vk::Filter::LINEAR, |mf| match mf {
gltf::texture::MagFilter::Nearest => vk::Filter::NEAREST,
gltf::texture::MagFilter::Linear => vk::Filter::LINEAR,
}),
min_filter: g_sampler
.min_filter()
.map_or(vk::Filter::LINEAR, |mf| match mf {
gltf::texture::MinFilter::Nearest
| gltf::texture::MinFilter::NearestMipmapNearest
| gltf::texture::MinFilter::NearestMipmapLinear => vk::Filter::NEAREST,
gltf::texture::MinFilter::Linear
| gltf::texture::MinFilter::LinearMipmapNearest
| gltf::texture::MinFilter::LinearMipmapLinear => vk::Filter::LINEAR,
}),
mipmap_mode: g_sampler
.min_filter()
.map_or(vk::SamplerMipmapMode::LINEAR, |mf| match mf {
gltf::texture::MinFilter::NearestMipmapNearest
| gltf::texture::MinFilter::LinearMipmapNearest => vk::SamplerMipmapMode::NEAREST,
gltf::texture::MinFilter::NearestMipmapLinear
| gltf::texture::MinFilter::LinearMipmapLinear => vk::SamplerMipmapMode::LINEAR,
_ => vk::SamplerMipmapMode::LINEAR, // Default if no mipmapping
}),
address_mode_u: vk_address_mode(wrap_s),
address_mode_v: vk_address_mode(wrap_t),
address_mode_w: vk::SamplerAddressMode::REPEAT, // glTF doesn't define wrapR
}
}
fn vk_address_mode(g_mode: gltf::texture::WrappingMode) -> vk::SamplerAddressMode {
match g_mode {
gltf::texture::WrappingMode::ClampToEdge => vk::SamplerAddressMode::CLAMP_TO_EDGE,
gltf::texture::WrappingMode::MirroredRepeat => vk::SamplerAddressMode::MIRRORED_REPEAT,
gltf::texture::WrappingMode::Repeat => vk::SamplerAddressMode::REPEAT,
}
}
impl Scene {
/// Takes a glTF file and returns a `Scene`.
pub fn from_gltf<T>(path: T, resource_manager: Arc<ResourceManager>) -> Result<Self>
where
T: AsRef<Path>,
{
let path_ref = path.as_ref();
let base_path = path_ref.parent().unwrap_or_else(|| Path::new(""));
tracing::info!("Loading glTF from: {:?}", path_ref);
tracing::info!("Base path for resources: {:?}", base_path);
// Import images as well
let (doc, buffers, images) = gltf::import(path_ref)?;
tracing::info!(
"glTF Stats: {} scenes, {} nodes, {} meshes, {} materials, {} textures, {} images",
doc.scenes().len(),
doc.nodes().len(),
doc.meshes().len(),
doc.materials().len(),
doc.textures().len(),
doc.images().len()
);
let mut meshes = Vec::new();
// Cache Geometry: Key = (mesh_index, primitive_index)
let mut geometry_cache: HashMap<(usize, usize), Arc<Geometry>> = HashMap::new();
// Cache Materials: Key = glTF material index (usize::MAX for default)
let mut material_cache: HashMap<usize, Arc<Material>> = HashMap::new();
// Cache default sampler handle to avoid repeated lookups
let default_sampler_handle =
resource_manager.get_or_create_sampler(&SamplerDesc::default())?;
let scene_to_load = doc
.default_scene()
.unwrap_or_else(|| doc.scenes().next().expect("No scenes found in glTF"));
let scene_name = scene_to_load
.name()
.unwrap_or("<Default Scene>")
.to_string();
tracing::info!(
"Processing scene '{}' ({})",
scene_name,
scene_to_load.index()
);
// Create a context struct to pass around common data
let mut load_ctx = LoadContext {
doc: &doc,
buffers: &buffers,
images: &images,
base_path,
resource_manager,
geometry_cache: &mut geometry_cache,
material_cache: &mut material_cache,
default_sampler_handle,
meshes: &mut meshes,
};
for node in scene_to_load.nodes() {
Self::process_node(&node, &Mat4::IDENTITY, &mut load_ctx)?;
}
tracing::info!("Successfully loaded {} render meshes.", meshes.len());
Ok(Self {
name: scene_name,
meshes,
})
}
/// Recursively processes a glTF node.
fn process_node(
node: &gltf::Node,
parent_transform: &Mat4,
ctx: &mut LoadContext, // Pass context mutably for caches
) -> Result<()> {
let local_transform = Mat4::from_cols_array_2d(&node.transform().matrix());
let world_transform = *parent_transform * local_transform;
let node_name = node.name().unwrap_or("<Unnamed Node>");
if let Some(mesh) = node.mesh() {
let mesh_index = mesh.index();
let mesh_name = mesh.name().unwrap_or("<Unnamed Mesh>");
tracing::debug!(
"Node '{}' ({}) has Mesh '{}' ({})",
node_name,
node.index(),
mesh_name,
mesh_index
);
// Process mesh primitives
for (primitive_index, primitive) in mesh.primitives().enumerate() {
// Generate a name for the Mesh object
let primitive_name = format!("{}_prim{}", mesh_name, primitive_index);
Self::process_primitive(
&primitive,
mesh_index,
primitive_index,
&primitive_name, // Pass name
world_transform,
ctx, // Pass context
)?;
}
} else {
tracing::trace!("Node '{}' ({}) has no mesh.", node_name, node.index());
}
// Recursively process child nodes
for child_node in node.children() {
Self::process_node(
&child_node,
&world_transform, // Pass current world transform
ctx, // Pass context
)?;
}
Ok(())
}
/// Processes a single glTF primitive, creating Geometry, Material, and Mesh.
fn process_primitive(
primitive: &gltf::Primitive,
mesh_index: usize,
primitive_index: usize,
mesh_name: &str, // Name for the final Mesh object
world_transform: Mat4,
ctx: &mut LoadContext, // Use context
) -> Result<()> {
let geometry_cache_key = (mesh_index, primitive_index);
// --- Get or Create Geometry ---
let geometry = if let Some(cached_geo) = ctx.geometry_cache.get(&geometry_cache_key) {
tracing::trace!("Using cached Geometry for key {:?}", geometry_cache_key);
cached_geo.clone()
} else {
tracing::trace!("Creating new Geometry for key {:?}", geometry_cache_key);
let reader = primitive.reader(|buffer| Some(&ctx.buffers[buffer.index()]));
let Some(pos_iter) = reader.read_positions() else {
tracing::warn!(
"Primitive {:?} missing positions. Skipping.",
geometry_cache_key
);
return Ok(()); // Skip this primitive
};
let positions: Vec<[f32; 3]> = pos_iter.collect();
let vertex_count = positions.len();
if vertex_count == 0 {
tracing::warn!(
"Primitive {:?} has no vertices. Skipping.",
geometry_cache_key
);
return Ok(());
}
let normals: Vec<[f32; 3]> = reader
.read_normals()
.map(|iter| iter.collect())
.unwrap_or_else(|| {
tracing::debug!(
"Primitive {:?} missing normals, using default.",
geometry_cache_key
);
vec![[0.0, 1.0, 0.0]; vertex_count]
});
// Read Texture Coordinates (Set 0) - needed for vertex struct regardless of material
let tex_coords: Vec<[f32; 2]> = reader
.read_tex_coords(0) // Read UV set 0
.map(|iter| iter.into_f32().collect())
.unwrap_or_else(|| {
tracing::trace!(
"Primitive {:?} missing tex_coords (set 0), using default.",
geometry_cache_key
);
vec![[0.0, 0.0]; vertex_count]
});
if normals.len() != vertex_count || tex_coords.len() != vertex_count {
return Err(SceneError::InconsistentData(format!(
"Attribute count mismatch for Primitive {:?} (Pos: {}, Norm: {}, TexCoord0: {}).",
geometry_cache_key, vertex_count, normals.len(), tex_coords.len()
)));
}
let vertices: Vec<Vertex> = positions
.into_iter()
.zip(normals)
.zip(tex_coords)
.map(|((pos, normal), tex_coord)| Vertex {
pos,
normal,
tex_coord,
})
.collect();
let indices: Vec<u32> = reader
.read_indices()
.map(|read_indices| read_indices.into_u32().collect())
.unwrap_or_else(|| (0..vertex_count as u32).collect());
if indices.is_empty() && vertex_count > 0 {
tracing::warn!(
"Primitive {:?} has vertices but no indices. Skipping.",
geometry_cache_key
);
return Ok(());
}
let new_geo = Arc::new(Geometry::new(
ctx.resource_manager.clone(),
&vertices,
&indices,
)?);
ctx.geometry_cache
.insert(geometry_cache_key, new_geo.clone());
new_geo
};
// --- Get or Create Material ---
let g_material = primitive.material();
// Use index usize::MAX as key for default material if index() is None
let material_cache_key = g_material.index().unwrap_or(usize::MAX);
let material_name = g_material.name().unwrap_or("<Default Material>");
let material = if let Some(cached_mat) = ctx.material_cache.get(&material_cache_key) {
tracing::trace!(
"Using cached Material index {} ('{}')",
material_cache_key,
material_name
);
cached_mat.clone()
} else {
tracing::trace!(
"Creating new Material for index {} ('{}')",
material_cache_key,
material_name
);
let pbr = g_material.pbr_metallic_roughness();
let base_color_factor = pbr.base_color_factor();
let metallic_factor = pbr.metallic_factor();
let roughness_factor = pbr.roughness_factor();
let mut loaded_base_color_texture: Option<Arc<Texture>> = None;
let mut loaded_base_color_sampler: Option<SamplerHandle> = None;
// --- Load Base Color Texture (if it exists) ---
if let Some(color_info) = pbr.base_color_texture() {
let tex_coord_set = color_info.tex_coord();
if tex_coord_set != 0 {
tracing::warn!(
"Material '{}' requests tex_coord set {}, but only set 0 is currently loaded. Texture ignored.",
material_name, tex_coord_set
);
// Fall through, texture won't be loaded
} else {
let g_texture = color_info.texture();
let g_sampler = g_texture.sampler();
let g_image = g_texture.source(); // This is the gltf::Image
tracing::debug!(
"Material '{}' uses Texture index {}, Sampler index {:?}, Image index {}",
material_name,
g_texture.index(),
g_sampler.index(),
g_image.index()
);
// Get or create sampler
let sampler_desc = sampler_desc_from_gltf(&g_sampler);
let sampler_handle =
ctx.resource_manager.get_or_create_sampler(&sampler_desc)?;
loaded_base_color_sampler = Some(sampler_handle);
// Load texture image data via ResourceManager
// Pass the correct gltf::Image using its index
let texture = ctx.resource_manager.load_texture(
&ctx.doc
.images()
.nth(g_image.index())
.expect("Image index out of bounds"), // Get gltf::Image
&g_image.source(), // Get gltf::image::Source
ctx.base_path,
ctx.buffers,
vk::ImageUsageFlags::SAMPLED, // Standard usage for textures
)?;
loaded_base_color_texture = Some(texture); // Store Arc<Texture>
}
}
// Assign default sampler if none was loaded via texture info
if loaded_base_color_sampler.is_none() {
loaded_base_color_sampler = Some(ctx.default_sampler_handle);
tracing::trace!("Material '{}' using default sampler.", material_name);
}
// Create the application Material struct
let new_mat = Arc::new(Material {
name: material_name.to_string(),
base_color_texture: loaded_base_color_texture,
base_color_sampler: loaded_base_color_sampler,
base_color_factor,
metallic_factor,
roughness_factor,
// Initialize other material properties here...
});
ctx.material_cache
.insert(material_cache_key, new_mat.clone());
new_mat
};
// Create the final Mesh object
ctx.meshes.push(Mesh {
name: mesh_name.to_string(),
geometry,
material, // Assign the Arc<Material>
transform: world_transform,
});
Ok(())
}
}
// Context struct to avoid passing too many arguments
struct LoadContext<'a> {
doc: &'a gltf::Document,
buffers: &'a [gltf::buffer::Data],
images: &'a [gltf::image::Data], // Keep image data accessible if needed by RM
base_path: &'a Path,
resource_manager: Arc<ResourceManager>,
geometry_cache: &'a mut HashMap<(usize, usize), Arc<Geometry>>,
material_cache: &'a mut HashMap<usize, Arc<Material>>,
default_sampler_handle: SamplerHandle, // Store the default sampler
meshes: &'a mut Vec<Mesh>, // Store results directly
}

11
crates/shared/Cargo.toml Normal file
View file

@ -0,0 +1,11 @@
[package]
name = "shared"
version = "0.1.0"
edition = "2021"
[dependencies]
glam.workspace = true
ash.workspace = true
bytemuck.workspace = true
memoffset = "0.9.1"
derive_builder = "0.20.2"

70
crates/shared/src/lib.rs Normal file
View file

@ -0,0 +1,70 @@
use ash::vk;
use glam::{Mat4, Vec3};
use core::f32;
use std::mem::size_of;
mod material;
#[repr(C)]
#[derive(Clone, Debug, Copy)]
pub struct Vertex {
pub pos: [f32; 3],
pub normal: [f32; 3],
pub tex_coord: [f32; 2],
}
impl Vertex {
pub fn get_binding_decription() -> vk::VertexInputBindingDescription {
vk::VertexInputBindingDescription::default()
.binding(0)
.stride(size_of::<Self>() as u32)
.input_rate(vk::VertexInputRate::VERTEX)
}
pub fn get_attribute_descriptions() -> [vk::VertexInputAttributeDescription; 3] {
[
vk::VertexInputAttributeDescription::default()
.location(0)
.binding(0)
.format(vk::Format::R32G32B32_SFLOAT)
.offset(memoffset::offset_of!(Vertex, pos) as u32),
vk::VertexInputAttributeDescription::default()
.location(1)
.binding(0)
.format(vk::Format::R32G32B32_SFLOAT)
.offset(memoffset::offset_of!(Vertex, normal) as u32),
vk::VertexInputAttributeDescription::default()
.location(2)
.binding(0)
.format(vk::Format::R32G32_SFLOAT)
.offset(memoffset::offset_of!(Vertex, tex_coord) as u32),
]
}
}
#[repr(C)]
#[derive(Clone, Debug, Copy, PartialEq)]
pub struct UniformBufferObject {
pub view: Mat4,
pub proj: Mat4,
}
#[derive(Clone, Debug, Copy, PartialEq)]
pub struct CameraInfo {
pub camera_pos: Vec3,
pub camera_target: Vec3,
pub camera_up: Vec3,
pub camera_fov: f32,
}
impl Default for CameraInfo {
fn default() -> Self {
Self {
camera_pos: Vec3::new(10.0, 10.0, 10.0),
camera_target: Vec3::new(0.0, 0.0, 0.0),
camera_up: Vec3::Y,
camera_fov: 45.0,
}
}
}

View file

45
flake.lock generated
View file

@ -20,11 +20,11 @@
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1733312601,
"narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=",
"lastModified": 1743550720,
"narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9",
"rev": "c621e8422220273271f52058f618c94e405bb0f5",
"type": "github"
},
"original": {
@ -51,23 +51,26 @@
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1733096140,
"narHash": "sha256-1qRH7uAUsyQI7R1Uwl4T+XvdNv778H0Nb5njNrqvylY=",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/5487e69da40cbd611ab2cadee0b4637225f7cfae.tar.gz"
"lastModified": 1743296961,
"narHash": "sha256-b1EdN3cULCqtorQ4QeWgLMrd5ZGOjLSLemfa00heasc=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "e4822aea2a6d1cdd36653c134cacfd64c97ff4fa",
"type": "github"
},
"original": {
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/5487e69da40cbd611ab2cadee0b4637225f7cfae.tar.gz"
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1728538411,
"narHash": "sha256-f0SBJz1eZ2yOuKUr5CA9BHULGXVSn6miBuUWdTyhUhU=",
"lastModified": 1736320768,
"narHash": "sha256-nIYdTAiKIGnFNugbomgBJR+Xv5F1ZQU+HfaBqJKroC0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b69de56fac8c2b6f8fd27f2eca01dcda8e0a4221",
"rev": "4bc9c909d9ac828a039f288cf872d16d38185db8",
"type": "github"
},
"original": {
@ -79,11 +82,11 @@
},
"nixpkgs_3": {
"locked": {
"lastModified": 1733097829,
"narHash": "sha256-9hbb1rqGelllb4kVUCZ307G2k3/UhmA8PPGBoyuWaSw=",
"lastModified": 1735554305,
"narHash": "sha256-zExSA1i/b+1NMRhGGLtNfFGXgLtgo+dcuzHzaWA6w3Q=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "2c15aa59df0017ca140d9ba302412298ab4bf22a",
"rev": "0e82ab234249d8eee3e8c91437802b32c74bb3fd",
"type": "github"
},
"original": {
@ -108,11 +111,11 @@
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1735266518,
"narHash": "sha256-2XkWYGgT+911gOLjgBj+8W8ZJk6P0qHJNz8RfKgT/5o=",
"lastModified": 1743820323,
"narHash": "sha256-UXxJogXhPhBFaX4uxmMudcD/x3sEGFtoSc4busTcftY=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "e0b3654b716098b47f3643c65fbb75ef49c033e1",
"rev": "b4734ce867252f92cdc7d25f8cc3b7cef153e703",
"type": "github"
},
"original": {
@ -141,11 +144,11 @@
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1735135567,
"narHash": "sha256-8T3K5amndEavxnludPyfj3Z1IkcFdRpR23q+T0BVeZE=",
"lastModified": 1743748085,
"narHash": "sha256-uhjnlaVTWo5iD3LXics1rp9gaKgDRQj6660+gbUU3cE=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "9e09d30a644c57257715902efbb3adc56c79cf28",
"rev": "815e4121d6a5d504c0f96e5be2dd7f871e4fd99d",
"type": "github"
},
"original": {

View file

@ -35,11 +35,12 @@
commonArgs,
...
}: {
_module.args = {
_module.args = rec {
pkgs = import nixpkgs {
inherit system;
overlays = [inputs.rust-overlay.overlays.default];
};
craneLib = (inputs.crane.mkLib pkgs).overrideToolchain (
pkgs: pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml
);

View file

@ -1,6 +1,11 @@
[toolchain]
channel = "nightly-2024-11-22"
components = ["rust-src", "rustc-dev", "llvm-tools"]
components = [
"rust-src",
"rustc-dev",
"llvm-tools",
"rustc-codegen-cranelift-preview",
]
# commit_hash = b19329a37cedf2027517ae22c87cf201f93d776e
# Whenever changing the nightly channel, update the commit hash above, and make

View file

@ -1,7 +1,34 @@
#version 450
// Input from vertex shader
layout(location = 0) in vec3 fragNormal; // Receive normal
layout(location = 1) in vec2 fragTexCoord; // Receive texture coordinates
// Output color
layout(location = 0) out vec4 outColor;
// Descriptor set for material properties (Set 1)
layout(set = 1, binding = 0) uniform sampler2D baseColorSampler;
// Optional: Pass material factors via another UBO or Push Constants if needed
// layout(set = 1, binding = 1) uniform MaterialFactors {
// vec4 baseColorFactor;
// } materialFactors;
void main() {
outColor = vec4(1.0, 0.5, 0.0, 1.0); // Orange
// Sample the texture
vec4 texColor = texture(baseColorSampler, fragTexCoord);
// Use the texture color
// You might multiply by baseColorFactor here if you pass it
// outColor = texColor * materialFactors.baseColorFactor;
outColor = texColor;
// Basic fallback if texture alpha is zero (or use baseColorFactor)
if (outColor.a == 0.0) {
outColor = vec4(0.8, 0.8, 0.8, 1.0); // Default grey
}
// You could add basic lighting using fragNormal here later
}

View file

@ -1,10 +1,38 @@
#version 450
vec2 positions[3] = vec2[](
vec2(0.0, -0.5),
vec2(0.5, 0.5),
vec2(-0.5, 0.5)
);
// INPUTS from Vertex Buffer (matching Vertex struct)
layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inNormal;
layout(location = 2) in vec2 inTexCoord; // <<< MUST be vec2
// UNIFORMS (Set 0)
layout(set = 0, binding = 0) uniform UniformBufferObject {
mat4 view;
mat4 proj;
} ubo;
// PUSH CONSTANTS
layout(push_constant) uniform PushConstants {
mat4 model;
} pushConstants;
// OUTPUTS to Fragment Shader
layout(location = 0) out vec3 fragNormal; // Location 0 for Normal
layout(location = 1) out vec2 fragTexCoord; // Location 1 for TexCoord
void main() {
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
vec4 worldPos = pushConstants.model * vec4(inPosition, 1.0);
// Calculate final position
gl_Position = ubo.proj * ubo.view * worldPos;
// --- Pass attributes to Fragment Shader ---
// Pass world-space normal (adjust calculation if needed)
// Ensure fragNormal is assigned a vec3
fragNormal = normalize(mat3(transpose(inverse(pushConstants.model))) * inNormal);
// Pass texture coordinates (ensure inTexCoord is vec2)
// Ensure fragTexCoord is assigned a vec2
fragTexCoord = inTexCoord;
}