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

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.