impl: 3D Rendering

This commit is contained in:
zack 2025-04-01 21:41:24 -04:00
parent dbf9544e80
commit 70176bb86a
No known key found for this signature in database
GPG key ID: EE8A2B709E2401D1
18 changed files with 862 additions and 185 deletions

102
Cargo.lock generated
View file

@ -549,6 +549,72 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991"
[[package]]
name = "darling"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]]
name = "derive_builder"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "derive_builder_macro"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
dependencies = [
"derive_builder_core",
"syn",
]
[[package]]
name = "dispatch"
version = "0.2.0"
@ -664,9 +730,11 @@ dependencies = [
"egui",
"egui-winit",
"gfx_hal",
"glam",
"raw-window-handle",
"renderer",
"resource_manager",
"shared",
"thiserror 2.0.12",
"tracing",
"tracing-subscriber",
@ -748,6 +816,12 @@ dependencies = [
"miniz_oxide 0.8.5",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.5.0"
@ -811,6 +885,7 @@ version = "0.1.0"
dependencies = [
"ash",
"ash-window",
"gpu-allocator",
"thiserror 2.0.12",
"tracing",
"winit",
@ -990,6 +1065,12 @@ dependencies = [
"syn",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "1.0.3"
@ -1199,6 +1280,15 @@ dependencies = [
"libc",
]
[[package]]
name = "memoffset"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
dependencies = [
"autocfg",
]
[[package]]
name = "miniz_oxide"
version = "0.7.4"
@ -1758,6 +1848,7 @@ dependencies = [
"gpu-allocator",
"resource_manager",
"shaderc",
"shared",
"thiserror 2.0.12",
"tracing",
"walkdir",
@ -1910,6 +2001,17 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "shared"
version = "0.1.0"
dependencies = [
"ash",
"bytemuck",
"derive_builder",
"glam",
"memoffset",
]
[[package]]
name = "shlex"
version = "1.3.0"

View file

@ -5,7 +5,7 @@ members = [
"crates/engine",
"crates/gfx_hal",
"crates/renderer",
"crates/resource_manager",
"crates/resource_manager", "crates/shared",
]
[workspace.dependencies]

View file

@ -13,9 +13,12 @@ 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" }
clap = { version = "4.5.34", features = ["derive"] }
egui-winit = "0.31.1"

View file

@ -8,15 +8,17 @@ use std::{
use ash::vk;
use clap::Parser;
use egui::{Context, ViewportId};
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 resource_manager::{Geometry, ResourceManager, ResourceManagerError};
use shared::{CameraInfo, Vertex};
use tracing::{debug, error, info, warn};
use tracing_subscriber::{filter, layer::SubscriberExt, util::SubscriberInitExt, Layer};
use winit::{
@ -25,6 +27,7 @@ use winit::{
event_loop::{ActiveEventLoop, EventLoop},
window::Window,
};
// --- Configuration ---
const APP_NAME: &str = "BeginDisregard";
const ENGINE_NAME: &str = "Engine";
@ -69,18 +72,33 @@ struct Application {
frame_count: u32,
last_fps_update_time: Instant,
last_frame_time: Instant,
current_fps: f64,
}
#[derive(Default)]
struct EditorUI {}
struct EditorUI {
camera_info: CameraInfo,
}
impl EditorUI {
fn title() -> String {
"engine".to_string()
}
fn build_ui(&mut self, ctx: &egui::Context) {
egui::Window::new(Self::title()).show(ctx, |ui| ui.label(Self::title()));
fn build_ui(&mut self, ctx: &egui::Context, current_fps: f64) {
egui::Window::new(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");
ui.add(Slider::new(&mut self.camera_info.camera_fov, 10.0..=120.0));
});
});
}
}
@ -241,14 +259,61 @@ 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();
// --- 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();
let vertices = vec![
// Define 8 vertices for a cube with positions and colors
// Make sure winding order is correct (e.g., counter-clockwise for front faces)
Vertex {
pos: Vec3::new(-0.5, -0.5, 0.5).into(),
color: Vec3::new(1.0, 0.0, 0.0).into(),
}, // Front bottom left 0
Vertex {
pos: Vec3::new(0.5, -0.5, 0.5).into(),
color: Vec3::new(0.0, 1.0, 0.0).into(),
}, // Front bottom right 1
Vertex {
pos: Vec3::new(0.5, 0.5, 0.5).into(),
color: Vec3::new(0.0, 0.0, 1.0).into(),
}, // Front top right 2
Vertex {
pos: Vec3::new(-0.5, 0.5, 0.5).into(),
color: Vec3::new(1.0, 1.0, 0.0).into(),
}, // Front top left 3
// ... add back face vertices (4-7) ...
Vertex {
pos: Vec3::new(-0.5, -0.5, -0.5).into(),
color: Vec3::new(1.0, 0.0, 1.0).into(),
}, // Back bottom left 4
Vertex {
pos: Vec3::new(0.5, -0.5, -0.5).into(),
color: Vec3::new(0.0, 1.0, 1.0).into(),
}, // Back bottom right 5
Vertex {
pos: Vec3::new(0.5, 0.5, -0.5).into(),
color: Vec3::new(0.5, 0.5, 0.5).into(),
}, // Back top right 6
Vertex {
pos: Vec3::new(-0.5, 0.5, -0.5).into(),
color: Vec3::new(1.0, 1.0, 1.0).into(),
}, // Back top left 7
];
let indices = vec![
// Define 12 triangles (36 indices) for the cube faces
// Front face
0, 1, 2, 2, 3, 0, // Right face
1, 5, 6, 6, 2, 1, // Back face
5, 4, 7, 7, 6, 5, // Left face
4, 0, 3, 3, 7, 4, // Top face
3, 2, 6, 6, 7, 3, // Bottom face
4, 5, 1, 1, 0, 4,
];
let cube_geometry = Geometry::new(resource_manager.clone(), &vertices, &indices)?;
// --- 5. Renderer ---
let initial_size = window.inner_size();
@ -258,6 +323,7 @@ impl Application {
graphics_queue.clone(),
surface.clone(),
resource_manager.clone(),
vec![cube_geometry],
initial_size.width,
initial_size.height,
)?;
@ -288,6 +354,7 @@ impl Application {
egui_ctx,
egui_app,
frame_count: 0,
current_fps: 0.,
last_fps_update_time: Instant::now(),
last_frame_time: Instant::now(),
})
@ -332,6 +399,7 @@ impl Application {
if elapsed_sice_last_update >= Duration::from_secs(1) {
let fps = self.frame_count as f64 / elapsed_sice_last_update.as_secs_f64();
self.current_fps = fps;
let new_title = format!("{} - {} - {:.0} FPS", ENGINE_NAME, APP_NAME, fps);
self.window.set_title(&new_title);
@ -349,7 +417,7 @@ impl Application {
pixels_per_point,
..
} = self.egui_ctx.run(raw_input, |ctx| {
self.egui_app.build_ui(ctx);
self.egui_app.build_ui(ctx, self.current_fps);
});
self.renderer.update_textures(textures_delta).unwrap();
@ -360,10 +428,11 @@ impl Application {
let clipped_primitives = self.egui_ctx.tessellate(shapes, pixels_per_point);
// --- Render Frame ---
match self
.renderer
.render_frame(pixels_per_point, &clipped_primitives)
{
match self.renderer.render_frame(
pixels_per_point,
&clipped_primitives,
self.egui_app.camera_info,
) {
Ok(_) => {
self.window.request_redraw();
}
@ -499,7 +568,7 @@ fn main() -> Result<(), Box<dyn Error>> {
.with_ansi(true)
.with_file(false)
.with_line_number(false)
.with_filter(filter::LevelFilter::DEBUG);
.with_filter(filter::LevelFilter::INFO);
let registry = tracing_subscriber::registry().with(fmt_layer);

View file

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

View file

@ -63,6 +63,10 @@ pub enum GfxHalError {
/// 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>;

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

@ -65,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

@ -16,6 +16,7 @@ winit.workspace = true
gfx_hal = { path = "../gfx_hal" }
resource_manager = { path = "../resource_manager" }
shared = { path = "../shared" }
[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,6 +1,8 @@
use std::{
ffi::CStr,
ffi::{c_void, CStr},
mem,
sync::{Arc, Mutex},
time::Instant,
};
use ash::vk;
@ -10,14 +12,15 @@ 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 resource_manager::{ImageHandle, ResourceManager, ResourceManagerError};
use glam::{Mat4, Vec3};
use gpu_allocator::{
vulkan::{Allocation, AllocationCreateDesc, Allocator},
MemoryLocation,
};
use resource_manager::{Geometry, ImageHandle, ResourceManager, ResourceManagerError};
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;
@ -55,6 +58,14 @@ 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),
}
impl<T> From<std::sync::PoisonError<T>> for RendererError {
fn from(_: std::sync::PoisonError<T>) -> Self {
Self::AllocatorUnavailable
}
}
struct FrameData {
@ -64,6 +75,12 @@ struct FrameData {
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 {
@ -84,14 +101,19 @@ pub struct Renderer {
swapchain_format: vk::SurfaceFormatKHR,
swapchain_extent: vk::Extent2D,
scene: Vec<Geometry>,
descriptor_set_layout: vk::DescriptorSetLayout,
descriptor_pool: vk::DescriptorPool,
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,
frames_data: Vec<FrameData>,
current_frame: usize,
@ -100,6 +122,8 @@ pub struct Renderer {
window_resized: bool,
current_width: u32,
current_height: u32,
start_time: Instant,
}
impl Renderer {
@ -109,6 +133,7 @@ impl Renderer {
graphics_queue: Arc<Queue>,
surface: Arc<Surface>,
resource_manager: Arc<ResourceManager>,
scene: Vec<Geometry>,
initial_width: u32,
initial_height: u32,
) -> Result<Self, RendererError> {
@ -128,10 +153,26 @@ 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, descriptor_pool) =
Self::create_descriptor_sets_resources(&device)?;
let frames_data = Self::create_frame_data(&device)?;
let (model_pipeline_layout, model_pipeline) = Self::create_model_pipeline(
&device,
format.format,
depth_format,
descriptor_set_layout,
)?;
let start_time = Instant::now();
let frames_data = Self::create_frame_data(
&device,
&resource_manager,
descriptor_pool,
descriptor_set_layout,
swapchain.extent(),
start_time,
)?;
info!("Renderer initialized successfully.");
@ -144,6 +185,7 @@ impl Renderer {
},
Options {
srgb_framebuffer: true,
in_flight_frames: MAX_FRAMES_IN_FLIGHT,
..Default::default()
},
)?;
@ -159,16 +201,21 @@ impl Renderer {
swapchain_image_views: image_views,
swapchain_format: format,
swapchain_extent: extent,
descriptor_set_layout,
descriptor_pool,
depth_image_handle,
depth_image_view,
depth_format,
triangle_pipeline_layout,
triangle_pipeline,
model_pipeline_layout,
model_pipeline,
frames_data,
scene,
current_frame: 0,
window_resized: false,
current_width: initial_width,
current_height: initial_height,
start_time,
})
}
@ -208,6 +255,7 @@ impl Renderer {
&mut self,
pixels_per_point: f32,
clipped_primitives: &[ClippedPrimitive],
camera_info: CameraInfo,
) -> Result<(), RendererError> {
// --- Handle Resize ---
if self.window_resized {
@ -229,6 +277,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(
@ -250,7 +299,6 @@ impl Renderer {
frame_data.in_flight_fence.reset()?;
if let Some(textures) = frame_data.textures_to_free.take() {
tracing::debug!("Freeing EGUI Textures");
self.egui_renderer.free_textures(&textures)?;
}
@ -266,6 +314,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
@ -373,10 +430,21 @@ impl Renderer {
self.device.raw().cmd_bind_pipeline(
command_buffer,
vk::PipelineBindPoint::GRAPHICS,
self.triangle_pipeline,
self.model_pipeline,
);
// Draw 3 vertices, 1 instance, 0 first vertex, 0 first instance
self.device.raw().cmd_draw(command_buffer, 3, 1, 0, 0);
self.device.raw().cmd_bind_descriptor_sets(
command_buffer,
vk::PipelineBindPoint::GRAPHICS,
self.model_pipeline_layout,
0,
&[frame_data.descriptor_set],
&[],
);
}
for g in &self.scene {
g.draw(self.device.raw(), command_buffer)?;
}
tracing::trace!("Rendering EGUI");
@ -682,33 +750,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_layout: 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
@ -730,8 +777,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)
@ -774,7 +826,8 @@ impl Renderer {
vk::PipelineDynamicStateCreateInfo::default().dynamic_states(&dynamic_states);
// --- Pipeline Layout ---
let layout_info = vk::PipelineLayoutCreateInfo::default(); // No descriptors/push constants
let layout_info = vk::PipelineLayoutCreateInfo::default()
.set_layouts(std::slice::from_ref(&descriptor_set_layout)); // No descriptors/push constants
let pipeline_layout = unsafe {
device
.raw()
@ -872,7 +925,14 @@ 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_layout: vk::DescriptorSetLayout,
swapchain_extent: vk::Extent2D,
instant: Instant,
) -> 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())?;
@ -905,6 +965,19 @@ 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_layout, 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, instant);
frames_data.push(FrameData {
textures_to_free: None,
command_pool,
@ -912,6 +985,11 @@ impl Renderer {
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)
@ -936,22 +1014,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()
.unwrap_or(&vk::PresentModeKHR::FIFO)
}
fn choose_swapchain_extent(
@ -1004,6 +1080,166 @@ impl Renderer {
vk::Result::ERROR_FORMAT_NOT_SUPPORTED,
)) // Or custom error
}
fn create_descriptor_sets_resources(
device: &Arc<Device>,
) -> Result<(vk::DescriptorSetLayout, vk::DescriptorPool), 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)?
};
let pool_size = vk::DescriptorPoolSize {
ty: vk::DescriptorType::UNIFORM_BUFFER,
descriptor_count: MAX_FRAMES_IN_FLIGHT as u32,
};
let pool_info = vk::DescriptorPoolCreateInfo::default()
.pool_sizes(std::slice::from_ref(&pool_size))
.max_sets(MAX_FRAMES_IN_FLIGHT as u32);
let descriptor_pool = unsafe { device.raw().create_descriptor_pool(&pool_info, None)? };
Ok((descriptor_set_layout, descriptor_pool))
}
fn create_descriptor_set(
device: &Arc<Device>,
descriptor_set_layout: vk::DescriptorSetLayout,
descriptor_pool: vk::DescriptorPool,
) -> Result<vk::DescriptorSet, RendererError> {
let layouts = vec![descriptor_set_layout; 1];
let alloc_info = vk::DescriptorSetAllocateInfo::default()
.descriptor_pool(descriptor_pool)
.set_layouts(&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, self.start_time);
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,
start: Instant,
) -> UniformBufferObject {
let time = start.elapsed();
let model = Mat4::from_rotation_y(time.as_secs_f32());
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 { model, view, proj }
}
// --- Drop Implementation ---
@ -1027,16 +1263,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

@ -0,0 +1,174 @@
use std::sync::Arc;
use ash::vk::{self, IndexType};
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.
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.
}
}

View file

@ -1,4 +1,5 @@
mod error;
mod geo;
use std::{
collections::HashMap,
@ -10,10 +11,12 @@ use std::{
};
use ash::vk;
use gfx_hal::{device::Device, instance::Instance, queue::Queue};
use gfx_hal::{device::Device, instance::Instance, queue::Queue, Fence};
use tracing::{debug, error, trace, warn};
pub use error::{ResourceManagerError, Result};
pub use geo::Geometry;
use gpu_allocator::{
vulkan::{Allocation, AllocationCreateDesc, Allocator, AllocatorCreateDesc},
MemoryLocation,
@ -125,7 +128,7 @@ impl Drop for InternalImageInfo {
struct TransferSetup {
command_pool: vk::CommandPool,
queue: Arc<Queue>,
fence: vk::Fence,
fence: Fence,
}
pub struct ResourceManager {
@ -135,7 +138,7 @@ pub struct ResourceManager {
buffers: Mutex<HashMap<u64, InternalBufferInfo>>,
images: Mutex<HashMap<u64, InternalImageInfo>>,
next_id: AtomicU64,
transfer_setup: Mutex<Option<TransferSetup>>,
transfer_setup: Arc<Mutex<TransferSetup>>,
}
impl ResourceManager {
@ -152,6 +155,28 @@ impl ResourceManager {
})?;
debug!("GPU Allocator created.");
let queue_family_index = device
.transfer_queue_family_index()
.or(device.compute_queue_family_index()) // Try compute as fallback
.unwrap_or(device.graphics_queue_family_index()); // Graphics as last resort
let queue = device.get_queue(queue_family_index, 0)?;
// Create command pool for transfer commands
let pool_info = vk::CommandPoolCreateInfo::default()
.flags(vk::CommandPoolCreateFlags::TRANSIENT) // Hint that buffers are short-lived
.queue_family_index(queue_family_index);
let command_pool = unsafe { device.raw().create_command_pool(&pool_info, None)? };
// Create a fence for waiting
let fence = Fence::new(device.clone(), false)?;
let new_setup = TransferSetup {
command_pool,
queue,
fence,
};
Ok(Self {
_instance: instance,
device,
@ -159,7 +184,7 @@ impl ResourceManager {
buffers: Mutex::new(HashMap::new()),
images: Mutex::new(HashMap::new()),
next_id: AtomicU64::new(1),
transfer_setup: Mutex::new(None),
transfer_setup: Arc::new(Mutex::new(new_setup)),
})
}
@ -168,114 +193,59 @@ impl ResourceManager {
self.allocator.clone()
}
/// Gets or initializes the TransferSetup resources.
fn get_transfer_setup(&self) -> Result<TransferSetup> {
let mut setup_guard = self.transfer_setup.lock()?;
if let Some(setup) = setup_guard.as_ref() {
// Simple check: Reset fence before reusing
unsafe { self.device.raw().reset_fences(&[setup.fence])? };
return Ok(TransferSetup {
// Return a copy/clone
command_pool: setup.command_pool,
queue: setup.queue.clone(),
fence: setup.fence,
});
}
debug!("Initializing TransferSetup...");
// Find a queue that supports transfer (prefer dedicated, fallback to graphics)
let queue_family_index = self
.device
.transfer_queue_family_index()
.or(self.device.compute_queue_family_index()) // Try compute as fallback
.unwrap_or(self.device.graphics_queue_family_index()); // Graphics as last resort
let queue = self.device.get_queue(queue_family_index, 0)?;
// Create command pool for transfer commands
let pool_info = vk::CommandPoolCreateInfo::default()
.flags(vk::CommandPoolCreateFlags::TRANSIENT) // Hint that buffers are short-lived
.queue_family_index(queue_family_index);
let command_pool = unsafe { self.device.raw().create_command_pool(&pool_info, None)? };
// Create a fence for waiting
let fence_info = vk::FenceCreateInfo::default();
let fence = unsafe { self.device.raw().create_fence(&fence_info, None)? };
let new_setup = TransferSetup {
command_pool,
queue,
fence,
};
*setup_guard = Some(new_setup); // Store it
debug!("TransferSetup initialized.");
// Return a new copy for use
Ok(TransferSetup {
command_pool: setup_guard.as_ref().unwrap().command_pool,
queue: setup_guard.as_ref().unwrap().queue.clone(),
fence: setup_guard.as_ref().unwrap().fence,
})
}
/// Helper to allocate, begin, end, submit, and wait for a single command buffer.
/// Helper to allocate, begin, end, submit, and wait for a single command buffer
/// using the provided TransferSetup.
unsafe fn submit_commands_and_wait<F>(
&self,
transfer_setup: &TransferSetup,
transfer_setup: &TransferSetup, // Use the cloned setup
record_fn: F,
) -> Result<()>
where
F: FnOnce(vk::CommandBuffer) -> Result<()>,
F: FnOnce(vk::CommandBuffer) -> Result<()>, // Closure records commands
{
let device = self.device.raw();
let device_raw = self.device.raw(); // Get raw ash::Device
// Allocate command buffer
let alloc_info = vk::CommandBufferAllocateInfo::default()
.command_pool(transfer_setup.command_pool)
.level(vk::CommandBufferLevel::PRIMARY)
.command_buffer_count(1);
let command_buffer = device.allocate_command_buffers(&alloc_info)?[0];
let command_buffer = device_raw.allocate_command_buffers(&alloc_info)?[0];
tracing::info!("Allocated command_buffer: {:?}", command_buffer);
trace!("Allocated temporary command buffer for transfer.");
// Begin recording
let begin_info = vk::CommandBufferBeginInfo::default()
.flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT);
device.begin_command_buffer(command_buffer, &begin_info)?;
device_raw.begin_command_buffer(command_buffer, &begin_info)?;
// Record user commands
// --- Record user commands ---
let record_result = record_fn(command_buffer);
// End recording (even if user function failed, to allow cleanup)
device.end_command_buffer(command_buffer)?;
// --- End Recording ---
// Always end buffer, even if recording failed, to allow cleanup
device_raw.end_command_buffer(command_buffer)?;
// Check user function result *after* ending buffer
record_result?;
trace!("Transfer commands recorded.");
let binding = [command_buffer];
// Submit
let submits = [vk::SubmitInfo::default().command_buffers(&binding)];
// Use the transfer queue and fence
// Submit to the transfer queue
let submits =
[vk::SubmitInfo::default().command_buffers(std::slice::from_ref(&command_buffer))];
// Use the queue from the TransferSetup. Assuming Queue::submit handles locking.
transfer_setup
.queue
.submit(self.device.raw(), &submits, None)?; // Submit without fence initially
.submit(device_raw, &submits, Some(&transfer_setup.fence))?; // Submit WITH fence
trace!("Transfer command buffer submitted.");
// Wait for completion using a separate wait call
// This avoids holding the queue's internal submit lock during the wait.
let fences = [transfer_setup.fence];
match device.wait_for_fences(&fences, true, u64::MAX) {
Ok(_) => {}
Err(vk::Result::TIMEOUT) => {
// Should not happen with u64::MAX
warn!("Transfer fence wait timed out unexpectedly.");
return Err(ResourceManagerError::TransferFailed(
"Fence wait timeout".to_string(),
));
}
Err(e) => return Err(e.into()),
}
// Wait for completion using the fence
transfer_setup.fence.wait(None)?;
// Free command buffer
device.free_command_buffers(transfer_setup.command_pool, &[command_buffer]);
// Free command buffer *after* successful wait
device_raw.free_command_buffers(transfer_setup.command_pool, &[command_buffer]);
trace!("Temporary command buffer freed.");
transfer_setup.fence.reset()?;
Ok(())
}
@ -387,7 +357,7 @@ impl ResourceManager {
let dest_handle = self.create_buffer(size, final_usage, location)?;
// 4. Record and submit transfer command
let transfer_setup = self.get_transfer_setup()?;
let transfer_setup = self.transfer_setup.lock()?;
let dest_info = self.get_buffer_info(dest_handle)?; // Get info for vk::Buffer handle
let staging_info_for_copy = self.get_buffer_info(staging_handle)?; // Get info again
@ -589,22 +559,18 @@ impl Drop for ResourceManager {
debug!("Clearing {} image entries...", images_map.len());
images_map.clear();
// Destroy transfer setup resources
let mut setup_guard = self
let setup = self
.transfer_setup
.lock()
.expect("mutex to not be poisoned");
if let Some(setup) = setup_guard.take() {
// take() removes it from the Option
debug!("Destroying TransferSetup resources...");
unsafe {
self.device.raw().destroy_fence(setup.fence, None);
self.device
.raw()
.destroy_command_pool(setup.command_pool, None);
}
debug!("TransferSetup resources destroyed.");
debug!("Destroying TransferSetup resources...");
unsafe {
self.device
.raw()
.destroy_command_pool(setup.command_pool, None);
}
debug!("TransferSetup resources destroyed.");
// The Allocator is wrapped in an Arc<Mutex<>>, so its Drop will be handled
// when the last Arc reference (including those held by Internal*Info) is dropped.

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"

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

@ -0,0 +1,61 @@
use ash::vk;
use glam::{Mat4, Vec3};
use core::f32;
use std::mem::size_of;
#[repr(C)]
#[derive(Clone, Debug, Copy)]
pub struct Vertex {
pub pos: [f32; 3],
pub color: [f32; 3],
}
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; 2] {
[
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, color) as u32),
]
}
}
#[repr(C)]
#[derive(Clone, Debug, Copy, PartialEq)]
pub struct UniformBufferObject {
pub model: Mat4,
pub view: Mat4,
pub proj: Mat4,
}
#[derive(Clone, Debug, Copy, PartialEq)]
pub struct CameraInfo {
pub camera_pos: Vec3,
pub camera_target: 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_fov: 45.0,
}
}
}

View file

@ -1,7 +1,16 @@
#version 450
// Input from vertex shader
layout(location = 0) in vec3 fragColor;
// layout(location = 1) in vec2 fragTexCoord; // If using textures
// Output color
layout(location = 0) out vec4 outColor;
// layout(binding = 1) uniform sampler2D texSampler; // If using textures
void main() {
outColor = vec4(1.0, 0.5, 0.0, 1.0); // Orange
// Use interpolated color
outColor = vec4(fragColor, 1.0);
// outColor = texture(texSampler, fragTexCoord); // If using textures
}

View file

@ -1,10 +1,26 @@
#version 450
vec2 positions[3] = vec2[](
vec2(0.0, -0.5),
vec2(0.5, 0.5),
vec2(-0.5, 0.5)
);
// Matches Vertex struct attribute descriptions
layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inColor;
// layout(location = 2) in vec2 inTexCoord; // If you add texture coords
// Matches UniformBufferObject struct and descriptor set layout binding
layout(binding = 0) uniform UniformBufferObject {
mat4 model;
mat4 view;
mat4 proj;
} ubo;
// Output to fragment shader
layout(location = 0) out vec3 fragColor;
// layout(location = 1) out vec2 fragTexCoord; // If you add texture coords
void main() {
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
// Transform position: Model -> World -> View -> Clip space
gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 1.0);
// Pass color (and other attributes) through
fragColor = inColor;
// fragTexCoord = inTexCoord;
}