pub mod camera;
pub mod compute_registry;
pub mod debug_overlay;
pub mod frame_pacer;
pub mod pipeline;
pub mod readback;
pub mod render;
pub mod scene_renderer;
pub mod shader_registry;
pub mod swapchain;
use std::sync::Arc;
use vulkano::device::physical::{PhysicalDevice, PhysicalDeviceType};
use vulkano::device::{
Device, DeviceCreateInfo, DeviceExtensions, Queue, QueueCreateInfo,
QueueFlags,
};
use vulkano::instance::debug::ValidationFeatureEnable;
use vulkano::instance::debug::{
DebugUtilsMessageSeverity, DebugUtilsMessageType, DebugUtilsMessenger,
DebugUtilsMessengerCallback, DebugUtilsMessengerCreateInfo,
};
use vulkano::instance::{Instance, InstanceCreateInfo};
use vulkano::swapchain::Surface;
use vulkano::VulkanLibrary;
use winit::event_loop::EventLoop;
use winit::window::Window;
#[derive(Debug)]
pub struct DeviceSelectionError {
pub selector: String,
pub available: Vec<String>,
}
impl std::fmt::Display for DeviceSelectionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"RUSTING_VULKAN_DEVICE={:?} matches no available Vulkan device; available devices: [{}]",
self.selector,
self.available.join(", ")
)
}
}
impl std::error::Error for DeviceSelectionError {}
pub fn select_device_index(
names: &[String],
selector: &str,
) -> Result<usize, DeviceSelectionError> {
if let Ok(index) = selector.parse::<usize>() {
if index < names.len() {
return Ok(index);
}
}
let needle = selector.to_lowercase();
names
.iter()
.position(|name| name.to_lowercase().contains(&needle))
.ok_or_else(|| DeviceSelectionError {
selector: selector.to_string(),
available: names.to_vec(),
})
}
fn select_physical_device(
candidates: Vec<(Arc<PhysicalDevice>, u32)>,
) -> (Arc<PhysicalDevice>, u32) {
match std::env::var("RUSTING_VULKAN_DEVICE") {
Ok(selector) => {
let names: Vec<String> = candidates
.iter()
.map(|(p, _)| p.properties().device_name.clone())
.collect();
let index = select_device_index(&names, &selector)
.unwrap_or_else(|err| panic!("{err}"));
candidates.into_iter().nth(index).unwrap()
}
Err(_) => candidates
.into_iter()
.min_by_key(|(p, _)| match p.properties().device_type {
PhysicalDeviceType::DiscreteGpu => 0,
PhysicalDeviceType::IntegratedGpu => 1,
_ => 2,
})
.unwrap(),
}
}
pub struct HeadlessVulkanBase {
pub instance: Arc<Instance>,
pub device: Arc<Device>,
pub queue: Arc<Queue>,
}
pub fn init_vulkan_headless() -> HeadlessVulkanBase {
let library = VulkanLibrary::new().expect("No Vulkan driver found.");
let instance = Instance::new(library, InstanceCreateInfo::default())
.expect("Failed to create headless Vulkan instance");
let candidates: Vec<_> = instance
.enumerate_physical_devices()
.unwrap()
.filter_map(|p| {
p.queue_family_properties()
.iter()
.enumerate()
.position(|(_, q)| {
q.queue_flags
.intersects(QueueFlags::GRAPHICS | QueueFlags::COMPUTE)
})
.map(|i| (p, i as u32))
})
.collect();
let (physical_device, queue_family_index) =
select_physical_device(candidates);
println!(
"Selected headless Vulkan device: {} (vendor 0x{:x}, driver {}, api {})",
physical_device.properties().device_name,
physical_device.properties().vendor_id,
physical_device.properties().driver_version,
physical_device.properties().api_version,
);
let (device, mut queues) = Device::new(
physical_device,
DeviceCreateInfo {
queue_create_infos: vec![QueueCreateInfo {
queue_family_index,
..Default::default()
}],
..Default::default()
},
)
.expect("Failed to create headless Vulkan device");
let queue = queues.next().unwrap();
HeadlessVulkanBase {
instance,
device,
queue,
}
}
#[derive(Clone)]
pub struct VulkanBase {
pub device: Arc<Device>,
pub queue: Arc<Queue>,
pub surface: Arc<Surface>,
pub window: Arc<Window>,
pub instance: Arc<Instance>,
pub debug_messenger: Option<Arc<DebugUtilsMessenger>>,
}
pub fn init_vulkan(event_loop: &EventLoop<()>, title: &str) -> VulkanBase {
let library = VulkanLibrary::new().expect("No Vulkan driver found.");
let mut required_extensions = Surface::required_extensions(event_loop)
.expect("Failed to determine Vulkan surface extensions");
let validation_enabled = (cfg!(debug_assertions)
|| cfg!(feature = "validation"))
&& library
.layer_properties()
.expect("Failed to enumerate Vulkan layers")
.any(|layer| layer.name() == "VK_LAYER_KHRONOS_validation");
let validation_callback = unsafe {
DebugUtilsMessengerCallback::new(|severity, message_type, data| {
eprintln!(
"[Vulkan {severity:?} {message_type:?}] {}: {}",
data.message_id_name.unwrap_or("unknown"),
data.message
);
})
};
let debug_create_info = DebugUtilsMessengerCreateInfo {
message_severity: DebugUtilsMessageSeverity::ERROR
| DebugUtilsMessageSeverity::WARNING
| DebugUtilsMessageSeverity::INFO,
message_type: DebugUtilsMessageType::GENERAL
| DebugUtilsMessageType::VALIDATION
| DebugUtilsMessageType::PERFORMANCE,
..DebugUtilsMessengerCreateInfo::user_callback(validation_callback)
};
let mut enabled_validation_features = Vec::new();
if validation_enabled
&& library.supported_extensions().ext_validation_features
{
required_extensions.ext_validation_features = true;
enabled_validation_features
.push(ValidationFeatureEnable::SynchronizationValidation);
let gpu_validation_enabled = std::env::var("RUSTING_GPU_VALIDATION")
.is_ok_and(|value| matches!(value.as_str(), "1" | "true" | "TRUE"));
if gpu_validation_enabled {
enabled_validation_features
.push(ValidationFeatureEnable::GpuAssisted);
enabled_validation_features
.push(ValidationFeatureEnable::GpuAssistedReserveBindingSlot);
}
}
let debug_utils_enabled =
validation_enabled && library.supported_extensions().ext_debug_utils;
if debug_utils_enabled {
required_extensions.ext_debug_utils = true;
}
let instance = Instance::new(
library,
InstanceCreateInfo {
enabled_extensions: required_extensions,
enabled_layers: validation_enabled
.then(|| "VK_LAYER_KHRONOS_validation".to_owned())
.into_iter()
.collect(),
debug_utils_messengers: debug_utils_enabled
.then(|| debug_create_info.clone())
.into_iter()
.collect(),
enabled_validation_features,
..Default::default()
},
)
.unwrap();
#[allow(deprecated)]
let window = Arc::new(
event_loop
.create_window(Window::default_attributes().with_title(title))
.expect("Failed to create window"),
);
let surface = Surface::from_window(instance.clone(), window.clone())
.expect("Failed to create Vulkan surface");
let debug_messenger = debug_utils_enabled
.then(|| {
DebugUtilsMessenger::new(instance.clone(), debug_create_info).ok()
})
.flatten()
.map(Arc::new);
let device_extensions = DeviceExtensions {
khr_swapchain: true,
..DeviceExtensions::empty()
};
let candidates: Vec<_> = instance
.enumerate_physical_devices()
.unwrap()
.filter(|p| p.supported_extensions().contains(&device_extensions))
.filter_map(|p| {
p.queue_family_properties()
.iter()
.enumerate()
.position(|(i, q)| {
q.queue_flags.intersects(QueueFlags::GRAPHICS)
&& p.surface_support(i as u32, &surface)
.unwrap_or(false)
})
.map(|i| (p, i as u32))
})
.collect();
let (physical_device, queue_family_index) =
select_physical_device(candidates);
println!(
"Selected Vulkan device: {} (vendor 0x{:x}, driver {}, api {})",
physical_device.properties().device_name,
physical_device.properties().vendor_id,
physical_device.properties().driver_version,
physical_device.properties().api_version,
);
let (device, mut queues) = Device::new(
physical_device,
DeviceCreateInfo {
enabled_extensions: device_extensions,
queue_create_infos: vec![QueueCreateInfo {
queue_family_index,
..Default::default()
}],
..Default::default()
},
)
.unwrap();
let queue = queues.next().unwrap();
if debug_utils_enabled {
let _ = device.set_debug_utils_object_name(
&queue,
Some("RustingEngine Main Queue"),
);
}
VulkanBase {
device,
queue,
surface,
window,
instance,
debug_messenger,
}
}
#[cfg(test)]
pub(crate) mod test_support {
use super::{init_vulkan_headless, HeadlessVulkanBase};
use crate::rendering::readback::read_back_buffer;
use std::sync::{Arc, OnceLock};
use vulkano::buffer::{
Buffer, BufferContents, BufferCreateInfo, BufferUsage,
};
use vulkano::command_buffer::allocator::StandardCommandBufferAllocator;
use vulkano::command_buffer::{
AutoCommandBufferBuilder, CommandBufferUsage,
};
use vulkano::descriptor_set::allocator::StandardDescriptorSetAllocator;
use vulkano::descriptor_set::{DescriptorSet, WriteDescriptorSet};
use vulkano::memory::allocator::{
AllocationCreateInfo, MemoryTypeFilter, StandardMemoryAllocator,
};
use vulkano::pipeline::compute::ComputePipelineCreateInfo;
use vulkano::pipeline::layout::PipelineDescriptorSetLayoutCreateInfo;
use vulkano::pipeline::{
ComputePipeline, Pipeline, PipelineBindPoint, PipelineLayout,
PipelineShaderStageCreateInfo,
};
use vulkano::shader::EntryPoint;
use vulkano::sync::GpuFuture;
static HEADLESS_DEVICE: OnceLock<HeadlessVulkanBase> = OnceLock::new();
pub(crate) fn headless_device() -> &'static HeadlessVulkanBase {
HEADLESS_DEVICE.get_or_init(init_vulkan_headless)
}
pub(crate) fn dispatch_and_read_back<T>(
base: &HeadlessVulkanBase,
entry_point: EntryPoint,
input: Vec<T>,
workgroups: [u32; 3],
) -> Vec<T>
where
T: BufferContents + Clone,
{
let memory_allocator =
Arc::new(StandardMemoryAllocator::new_default(base.device.clone()));
let command_buffer_allocator =
Arc::new(StandardCommandBufferAllocator::new(
base.device.clone(),
Default::default(),
));
let descriptor_set_allocator =
Arc::new(StandardDescriptorSetAllocator::new(
base.device.clone(),
Default::default(),
));
let buffer = Buffer::from_iter(
memory_allocator.clone(),
BufferCreateInfo {
usage: BufferUsage::STORAGE_BUFFER | BufferUsage::TRANSFER_SRC,
..Default::default()
},
AllocationCreateInfo {
memory_type_filter: MemoryTypeFilter::PREFER_DEVICE
| MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
..Default::default()
},
input,
)
.unwrap();
let stage = PipelineShaderStageCreateInfo::new(entry_point);
let layout = PipelineLayout::new(
base.device.clone(),
PipelineDescriptorSetLayoutCreateInfo::from_stages([&stage])
.into_pipeline_layout_create_info(base.device.clone())
.unwrap(),
)
.unwrap();
let pipeline = ComputePipeline::new(
base.device.clone(),
None,
ComputePipelineCreateInfo::stage_layout(stage, layout),
)
.unwrap();
let set = DescriptorSet::new(
descriptor_set_allocator,
pipeline.layout().set_layouts()[0].clone(),
[WriteDescriptorSet::buffer(0, buffer.clone())],
[],
)
.unwrap();
let mut builder = AutoCommandBufferBuilder::primary(
command_buffer_allocator.clone(),
base.queue.queue_family_index(),
CommandBufferUsage::OneTimeSubmit,
)
.unwrap();
builder
.bind_pipeline_compute(pipeline.clone())
.unwrap()
.bind_descriptor_sets(
PipelineBindPoint::Compute,
pipeline.layout().clone(),
0,
set,
)
.unwrap();
unsafe { builder.dispatch(workgroups) }.unwrap();
let command_buffer = builder.build().unwrap();
let future = vulkano::sync::now(base.device.clone())
.then_execute(base.queue.clone(), command_buffer)
.unwrap()
.then_signal_fence_and_flush()
.unwrap();
future.wait(None).unwrap();
read_back_buffer(
&base.device,
&base.queue,
&memory_allocator,
&command_buffer_allocator,
buffer,
)
}
pub(crate) fn assert_matches_golden_image(
actual: &image::RgbaImage,
golden_path: &std::path::Path,
artifact_dir: &std::path::Path,
tolerance: u8,
) {
let expected = image::open(golden_path)
.unwrap_or_else(|error| {
panic!("golden image {golden_path:?} missing or unreadable: {error}")
})
.to_rgba8();
if actual.dimensions() != expected.dimensions() {
std::fs::create_dir_all(artifact_dir).unwrap();
actual.save(artifact_dir.join("actual.png")).unwrap();
expected.save(artifact_dir.join("expected.png")).unwrap();
panic!(
"golden image mismatch: actual size {:?} != expected size {:?}; artifacts written to {artifact_dir:?}",
actual.dimensions(),
expected.dimensions(),
);
}
let mut diff = image::RgbaImage::new(actual.width(), actual.height());
let mut max_diff = 0u8;
for ((actual_pixel, expected_pixel), diff_pixel) in actual
.pixels()
.zip(expected.pixels())
.zip(diff.pixels_mut())
{
for channel in 0..4 {
let delta =
actual_pixel[channel].abs_diff(expected_pixel[channel]);
max_diff = max_diff.max(delta);
diff_pixel[channel] = delta;
}
diff_pixel[3] = 255;
}
if max_diff > tolerance {
std::fs::create_dir_all(artifact_dir).unwrap();
actual.save(artifact_dir.join("actual.png")).unwrap();
expected.save(artifact_dir.join("expected.png")).unwrap();
diff.save(artifact_dir.join("diff.png")).unwrap();
panic!(
"golden image mismatch: max per-channel difference {max_diff} exceeds tolerance {tolerance}; artifacts written to {artifact_dir:?}"
);
}
}
#[test]
fn identical_image_matches_its_own_golden_with_zero_tolerance() {
let scratch =
std::env::temp_dir().join("rusting_engine_golden_test_identical");
std::fs::create_dir_all(&scratch).unwrap();
let golden_path = scratch.join("golden.png");
let artifact_dir = scratch.join("artifacts");
let image = image::RgbaImage::from_fn(4, 4, |x, y| {
image::Rgba([(x * 16) as u8, (y * 16) as u8, 0, 255])
});
image.save(&golden_path).unwrap();
assert_matches_golden_image(&image, &golden_path, &artifact_dir, 0);
assert!(!artifact_dir.exists());
std::fs::remove_dir_all(&scratch).unwrap();
}
#[test]
fn a_small_difference_within_tolerance_passes() {
let scratch =
std::env::temp_dir().join("rusting_engine_golden_test_tolerance");
std::fs::create_dir_all(&scratch).unwrap();
let golden_path = scratch.join("golden.png");
let artifact_dir = scratch.join("artifacts");
let golden = image::RgbaImage::from_pixel(
4,
4,
image::Rgba([100, 100, 100, 255]),
);
golden.save(&golden_path).unwrap();
let actual = image::RgbaImage::from_pixel(
4,
4,
image::Rgba([102, 100, 100, 255]),
);
assert_matches_golden_image(&actual, &golden_path, &artifact_dir, 5);
assert!(!artifact_dir.exists());
std::fs::remove_dir_all(&scratch).unwrap();
}
#[test]
fn a_mismatch_beyond_tolerance_panics_and_writes_artifact_images() {
let scratch =
std::env::temp_dir().join("rusting_engine_golden_test_mismatch");
std::fs::create_dir_all(&scratch).unwrap();
let golden_path = scratch.join("golden.png");
let artifact_dir = scratch.join("artifacts");
let golden =
image::RgbaImage::from_pixel(4, 4, image::Rgba([0, 0, 0, 255]));
golden.save(&golden_path).unwrap();
let actual =
image::RgbaImage::from_pixel(4, 4, image::Rgba([255, 0, 0, 255]));
let result = std::panic::catch_unwind(|| {
assert_matches_golden_image(
&actual,
&golden_path,
&artifact_dir,
5,
);
});
assert!(
result.is_err(),
"expected a mismatch beyond tolerance to panic"
);
assert!(artifact_dir.join("actual.png").exists());
assert!(artifact_dir.join("expected.png").exists());
assert!(artifact_dir.join("diff.png").exists());
std::fs::remove_dir_all(&scratch).unwrap();
}
}
#[cfg(test)]
mod headless_device_tests {
use super::*;
#[test]
#[cfg_attr(
not(feature = "gpu-tests"),
ignore = "run with `--features gpu-tests` on a machine with a Vulkan driver"
)]
fn headless_device_creation_succeeds_with_a_vulkan_driver_present() {
if VulkanLibrary::new().is_err() {
eprintln!("skipping: no Vulkan driver present");
return;
}
let base = init_vulkan_headless();
assert!(!base
.device
.physical_device()
.properties()
.device_name
.is_empty());
}
}
#[cfg(test)]
mod debug_utils_tests {
use super::*;
use vulkano::command_buffer::allocator::StandardCommandBufferAllocator;
use vulkano::command_buffer::{
AutoCommandBufferBuilder, CommandBufferUsage,
};
use vulkano::instance::debug::DebugUtilsLabel;
use vulkano::instance::InstanceExtensions;
#[test]
#[cfg_attr(
not(feature = "gpu-tests"),
ignore = "run with `--features gpu-tests` on a machine with a Vulkan driver"
)]
fn object_names_and_command_buffer_labels_are_accepted_by_the_driver() {
let Ok(library) = VulkanLibrary::new() else {
eprintln!("skipping: no Vulkan driver present");
return;
};
if !library.supported_extensions().ext_debug_utils {
eprintln!("skipping: driver has no ext_debug_utils");
return;
}
let instance = Instance::new(
library,
InstanceCreateInfo {
enabled_extensions: vulkano::instance::InstanceExtensions {
ext_debug_utils: true,
..InstanceExtensions::empty()
},
..Default::default()
},
)
.expect("Failed to create instance with ext_debug_utils");
let candidates: Vec<_> = instance
.enumerate_physical_devices()
.unwrap()
.filter_map(|p| {
p.queue_family_properties()
.iter()
.enumerate()
.position(|(_, q)| {
q.queue_flags.intersects(
QueueFlags::GRAPHICS | QueueFlags::COMPUTE,
)
})
.map(|i| (p, i as u32))
})
.collect();
let (physical_device, queue_family_index) =
select_physical_device(candidates);
let (device, mut queues) = Device::new(
physical_device,
DeviceCreateInfo {
queue_create_infos: vec![QueueCreateInfo {
queue_family_index,
..Default::default()
}],
..Default::default()
},
)
.expect("Failed to create device");
let queue = queues.next().unwrap();
device
.set_debug_utils_object_name(&queue, Some("test queue"))
.expect("naming the queue must succeed");
let command_buffer_allocator = StandardCommandBufferAllocator::new(
device.clone(),
Default::default(),
);
let mut builder = AutoCommandBufferBuilder::primary(
Arc::new(command_buffer_allocator),
queue.queue_family_index(),
CommandBufferUsage::OneTimeSubmit,
)
.unwrap();
builder
.begin_debug_utils_label(DebugUtilsLabel {
label_name: "test scope".to_string(),
..Default::default()
})
.expect("beginning a label must succeed");
unsafe {
builder
.end_debug_utils_label()
.expect("ending a label after a matching begin must succeed");
}
let command_buffer = builder.build().unwrap();
use vulkano::sync::GpuFuture;
vulkano::sync::now(device.clone())
.then_execute(queue, command_buffer)
.unwrap()
.then_signal_fence_and_flush()
.unwrap()
.wait(None)
.unwrap();
}
}
#[cfg(test)]
mod device_selection_tests {
use super::*;
fn names() -> Vec<String> {
vec![
"llvmpipe (LLVM 18.0.0, 256 bits)".to_string(),
"NVIDIA GeForce RTX 3080".to_string(),
"Intel(R) UHD Graphics 630".to_string(),
]
}
#[test]
fn selects_by_exact_index() {
assert_eq!(select_device_index(&names(), "1").unwrap(), 1);
}
#[test]
fn selects_by_case_insensitive_name_substring() {
assert_eq!(select_device_index(&names(), "nvidia").unwrap(), 1);
assert_eq!(select_device_index(&names(), "llvmpipe").unwrap(), 0);
}
#[test]
fn out_of_range_index_falls_back_to_name_match_and_then_fails() {
let err = select_device_index(&names(), "99").unwrap_err();
assert_eq!(err.selector, "99");
assert_eq!(err.available, names());
}
#[test]
fn no_match_lists_every_available_device_in_the_error() {
let err = select_device_index(&names(), "does-not-exist").unwrap_err();
assert_eq!(err.available, names());
assert!(err.to_string().contains("llvmpipe"));
assert!(err.to_string().contains("NVIDIA GeForce RTX 3080"));
assert!(err.to_string().contains("Intel(R) UHD Graphics 630"));
}
}