Added vert and shader, created Vulkan chain, started work on async timestep

This commit is contained in:
Jake Jensen 2025-07-07 19:05:53 -04:00
parent 5f84d78add
commit 591f81b514
5 changed files with 988 additions and 4 deletions

272
Cargo.lock generated
View File

@ -18,6 +18,15 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2187590a23ab1e3df8681afdf0987c48504d80291f002fcdb651f0ef5e25169"
[[package]]
name = "addr2line"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
dependencies = [
"gimli",
]
[[package]]
name = "adler2"
version = "2.0.1"
@ -91,6 +100,21 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04"
[[package]]
name = "anyhow"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "approx"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
dependencies = [
"num-traits",
]
[[package]]
name = "arrayref"
version = "0.3.9"
@ -139,6 +163,21 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "backtrace"
version = "0.3.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
dependencies = [
"addr2line",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
"windows-targets 0.52.6",
]
[[package]]
name = "bitflags"
version = "1.3.2"
@ -545,6 +584,12 @@ dependencies = [
"wasi 0.14.2+wasi-0.2.4",
]
[[package]]
name = "gimli"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "half"
version = "2.6.0"
@ -596,6 +641,17 @@ dependencies = [
"web-sys",
]
[[package]]
name = "io-uring"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013"
dependencies = [
"bitflags 2.9.1",
"cfg-if",
"libc",
]
[[package]]
name = "itoa"
version = "1.0.15"
@ -724,6 +780,16 @@ dependencies = [
"libc",
]
[[package]]
name = "matrixmultiply"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08"
dependencies = [
"autocfg",
"rawpointer",
]
[[package]]
name = "memchr"
version = "2.7.5"
@ -785,6 +851,44 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "mio"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
dependencies = [
"libc",
"wasi 0.11.1+wasi-snapshot-preview1",
"windows-sys 0.59.0",
]
[[package]]
name = "nalgebra"
version = "0.33.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26aecdf64b707efd1310e3544d709c5c0ac61c13756046aaaba41be5c4f66a3b"
dependencies = [
"approx",
"matrixmultiply",
"nalgebra-macros",
"num-complex",
"num-rational",
"num-traits",
"simba",
"typenum",
]
[[package]]
name = "nalgebra-macros"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "254a5372af8fc138e36684761d3c0cdb758a4410e938babcff1c860ce14ddbfc"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
]
[[package]]
name = "ndk"
version = "0.7.0"
@ -873,6 +977,54 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-complex"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
"num-traits",
]
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
dependencies = [
"num-bigint",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "num_enum"
version = "0.5.11"
@ -1230,6 +1382,15 @@ dependencies = [
"objc2-foundation 0.2.2",
]
[[package]]
name = "object"
version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.21.3"
@ -1277,6 +1438,12 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "percent-encoding"
version = "2.3.1"
@ -1419,6 +1586,12 @@ dependencies = [
"objc2-quartz-core 0.3.1",
]
[[package]]
name = "rawpointer"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
[[package]]
name = "redox_syscall"
version = "0.3.5"
@ -1484,6 +1657,12 @@ dependencies = [
"xmlparser",
]
[[package]]
name = "rustc-demangle"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f"
[[package]]
name = "rustix"
version = "0.38.44"
@ -1522,6 +1701,15 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "safe_arch"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323"
dependencies = [
"bytemuck",
]
[[package]]
name = "same-file"
version = "1.0.6"
@ -1535,7 +1723,10 @@ dependencies = [
name = "sapphirePhysSim"
version = "0.1.0"
dependencies = [
"anyhow",
"bytemuck",
"nalgebra",
"tokio",
"vulkano 0.35.1",
"vulkano-shaders",
"vulkano-win",
@ -1639,6 +1830,28 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
dependencies = [
"libc",
]
[[package]]
name = "simba"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3a386a501cd104797982c15ae17aafe8b9261315b5d07e3ec803f2ea26be0fa"
dependencies = [
"approx",
"num-complex",
"num-traits",
"paste",
"wide",
]
[[package]]
name = "simd-adler32"
version = "0.3.7"
@ -1725,6 +1938,16 @@ dependencies = [
"serde",
]
[[package]]
name = "socket2"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "strict-num"
version = "0.1.1"
@ -1832,6 +2055,37 @@ dependencies = [
"strict-num",
]
[[package]]
name = "tokio"
version = "1.46.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17"
dependencies = [
"backtrace",
"bytes",
"io-uring",
"libc",
"mio 1.0.4",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"slab",
"socket2",
"tokio-macros",
"windows-sys 0.52.0",
]
[[package]]
name = "tokio-macros"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
]
[[package]]
name = "toml_datetime"
version = "0.6.11"
@ -1882,6 +2136,12 @@ version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
[[package]]
name = "typenum"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
[[package]]
name = "unicode-ident"
version = "1.0.18"
@ -2323,6 +2583,16 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "wide"
version = "0.7.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03"
dependencies = [
"bytemuck",
"safe_arch",
]
[[package]]
name = "winapi"
version = "0.3.9"
@ -2656,7 +2926,7 @@ dependencies = [
"instant",
"libc",
"log",
"mio",
"mio 0.8.11",
"ndk 0.7.0",
"objc2 0.3.0-beta.3.patch-leaks.3",
"once_cell",

View File

@ -4,8 +4,13 @@ version = "0.1.0"
edition = "2024"
[dependencies]
bytemuck = "1.23.1"
bytemuck = { version = "1.13", features = ["derive"] }
# Error handling
anyhow = "1.0"
# For math operations (e.g., vectors, matrices)
nalgebra = "0.33.2"
vulkano = "0.35.1"
vulkano-shaders = "0.35.0"
vulkano-win = "0.34.0"
winit = "0.30.11"
tokio = { version = "1", features = ["full"] }

View File

@ -1,6 +1,605 @@
// Jake Jensen, 2025
// Rust
fn main() {
println!("Hello, world!");
use anyhow::{Result, Context};
use std::sync::Arc;
use tokio::sync::watch;
use vulkano::{
buffer::{Buffer, BufferCreateInfo, BufferUsage, CpuAccessibleBuffer, Subbuffer},
command_buffer::{
allocator::{StandardCommandBufferAllocator, StandardCommandBufferAllocatorCreateInfo},
AutoCommandBufferBuilder, CommandBufferUsage, RenderPassBeginInfo, SubpassContents,
},
device::{
physical::{PhysicalDevice, PhysicalDeviceType},
Device, DeviceCreateInfo, DeviceExtensions, QueueCreateInfo, QueueFlags,
},
format::Format,
image::{view::ImageView, Image, ImageCreateInfo, ImageUsage, SwapchainImage},
instance::{Instance, InstanceCreateInfo, InstanceExtensions, Version},
memory::allocator::{AllocationCreateInfo, MemoryTypeFilter, StandardMemoryAllocator},
pipeline::{
graphics::{
input_assembly::InputAssemblyState,
vertex_input::{Vertex, VertexInputState},
viewport::{Viewport, ViewportState},
GraphicsPipeline,
},
Pipeline, PipelineBindPoint,
},
render_pass::{Framebuffer, FramebufferCreateInfo, RenderPass, Subpass},
swapchain::{
acquire_next_image, Surface, Swapchain, SwapchainCreateInfo, SwapchainPresentInfo,
},
sync::{self, GpuFuture},
VulkanLibrary,
};
use winit::{
event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::{Window, WindowBuilder},
};
use nalgebra::{Vector2, Vector4};
// Include the shader code
mod vs {
vulkano_shaders::shader! {
ty: "vertex",
path: "src/shaders/vert.glsl",
}
}
mod fs {
vulkano_shaders::shader! {
ty: "fragment",
path: "src/shaders/frag.glsl",
}
}
/// Represents a single circle in the simulation.
#[derive(Debug, Clone, Copy)]
pub struct Circle {
pub position: Vector2<f32>,
pub radius: f32,
pub color: Vector4<f32>,
}
/// The entire physics state to be sent to the renderer.
#[derive(Debug, Clone)]
pub struct PhysicsState {
pub circles: Vec<Circle>,
}
// Implement the `Vertex` trait for our quad vertices.
// These are the vertices that make up a unit square, which will be scaled and positioned by the shader.
#[derive(BufferContents, Vertex)]
#[repr(C)]
struct QuadVertex {
#[format(R32G32_SFLOAT)]
position: [f32; 2],
}
// --- Physics Simulation Task ---
async fn physics_task(
sender: watch::Sender<PhysicsState>,
initial_state: PhysicsState,
) -> Result<()> {
let mut physics_state = initial_state;
let mut last_update_time = tokio::time::Instant::now();
let mut accumulator = tokio::time::Duration::ZERO;
// Fixed timestep for physics updates (e.g., 60 updates per second)
const PHYSICS_TIMESTEP: tokio::time::Duration = tokio::time::Duration::from_nanos(16_666_667); // ~1/60th second
log::info!("Physics task started.");
loop {
let now = tokio::time::Instant::now();
accumulator += now.duration_since(last_update_time);
last_update_time = now;
// Prevent "spiral of death" if rendering is too slow or system is overloaded
// By clamping the accumulator, we ensure physics doesn't try to catch up infinitely.
// It will just slow down the simulation if the system can't keep up.
if accumulator > PHYSICS_TIMESTEP * 5 { // Cap at 5 physics steps behind
accumulator = PHYSICS_TIMESTEP * 5;
}
// Perform physics updates in fixed steps
while accumulator >= PHYSICS_TIMESTEP {
// --- YOUR PHYSICS LOGIC GOES HERE ---
// For now, let's just make circles bounce off walls and move randomly
for circle in &mut physics_state.circles {
// Simple movement
circle.position.x += 0.001; // Move right
circle.position.y += 0.0005; // Move up
// Simple wall bounce (normalized device coordinates -1 to 1)
if circle.position.x + circle.radius > 1.0 {
circle.position.x = 1.0 - circle.radius;
}
if circle.position.x - circle.radius < -1.0 {
circle.position.x = -1.0 + circle.radius;
}
if circle.position.y + circle.radius > 1.0 {
circle.position.y = 1.0 - circle.radius;
}
if circle.position.y - circle.radius < -1.0 {
circle.position.y = -1.0 + circle.radius;
}
}
// --- END PHYSICS LOGIC ---
accumulator -= PHYSICS_TIMESTEP;
}
// Send the updated physics state to the renderer.
// `send_replace` ensures only the latest value is available, dropping older ones.
sender.send_replace(physics_state.clone());
// Yield control back to the Tokio runtime.
// This is important to allow other tasks (like the winit event loop if it's also async)
// to run, and to prevent busy-waiting.
tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
}
}
// --- Vulkan Renderer ---
struct Renderer {
_library: Arc<VulkanLibrary>,
instance: Arc<Instance>,
surface: Arc<Surface>,
device: Arc<Device>,
queue: Arc<vulkano::device::Queue>,
memory_allocator: Arc<StandardMemoryAllocator>,
command_buffer_allocator: Arc<StandardCommandBufferAllocator>,
render_pass: Arc<RenderPass>,
pipeline: Arc<GraphicsPipeline>,
swapchain: Arc<Swapchain>,
framebuffers: Vec<Arc<Framebuffer>>,
recreate_swapchain: bool,
previous_frame_end: Option<Box<dyn GpuFuture>>,
quad_vertex_buffer: Subbuffer<[QuadVertex]>, // Static buffer for the quad
}
impl Renderer {
fn new(event_loop: &EventLoop<()>) -> Result<Self> {
let library = VulkanLibrary::new().context("No Vulkan library found")?;
log::info!("Vulkan library loaded: {:?}", library.name());
let required_extensions = vulkano_win::required_extensions(&library);
let instance = Instance::new(
library.clone(),
InstanceCreateInfo {
enabled_extensions: required_extensions,
// Enable validation layers for debugging
enabled_layers: if cfg!(debug_assertions) {
vec!["VK_LAYER_KHRONOS_validation".to_string()]
} else {
vec![]
},
max_api_version: Some(Version::V1_1), // Specify minimum Vulkan API version
..Default::default()
},
)
.context("Failed to create Vulkan instance")?;
log::info!("Vulkan instance created.");
let window = Arc::new(WindowBuilder::new().build(event_loop).context("Failed to create window")?);
let surface = vulkano_win::create_surface_from_winit(instance.clone(), window.clone())
.context("Failed to create Vulkan surface")?;
log::info!("Vulkan surface created.");
let (physical_device, queue_family_index) = Self::select_physical_device(&instance, &surface)?;
log::info!("Selected physical device: {}", physical_device.properties().device_name);
let (device, mut queues) = Device::new(
physical_device.clone(),
DeviceCreateInfo {
enabled_extensions: DeviceExtensions {
khr_swapchain: true, // Required for rendering to a window
..DeviceExtensions::empty()
},
queue_create_infos: vec![QueueCreateInfo {
queue_family_index,
// Prioritize this queue for scheduling
queue_priorities: vec![1.0],
..Default::default()
}],
..Default::default()
},
)
.context("Failed to create Vulkan device")?;
let queue = queues.next().context("No queues found")?;
log::info!("Vulkan device and queue created.");
let memory_allocator = Arc::new(StandardMemoryAllocator::new_default(device.clone()));
let command_buffer_allocator = Arc::new(StandardCommandBufferAllocator::new(
device.clone(),
StandardCommandBufferAllocatorCreateInfo::default(),
));
log::info!("Memory and command buffer allocators created.");
let (mut swapchain, images) = Self::create_swapchain(
device.clone(),
surface.clone(),
physical_device.clone(),
window.inner_size(),
)?;
log::info!("Swapchain created.");
let render_pass = Self::create_render_pass(device.clone(), swapchain.image_format())?;
log::info!("Render pass created.");
let pipeline = Self::create_pipeline(
device.clone(),
render_pass.clone(),
swapchain.image_extent(),
)?;
log::info!("Graphics pipeline created.");
let framebuffers = Self::create_framebuffers(&images, render_pass.clone())?;
log::info!("Framebuffers created.");
// Create a static vertex buffer for a unit quad
let quad_vertices = [
QuadVertex { position: [-1.0, -1.0] },
QuadVertex { position: [ 1.0, -1.0] },
QuadVertex { position: [-1.0, 1.0] },
QuadVertex { position: [-1.0, 1.0] },
QuadVertex { position: [ 1.0, -1.0] },
QuadVertex { position: [ 1.0, 1.0] },
];
let quad_vertex_buffer = Buffer::from_iter(
&memory_allocator,
BufferCreateInfo {
usage: BufferUsage::VERTEX_BUFFER,
..Default::default()
},
AllocationCreateInfo {
memory_type_filter: MemoryTypeFilter::PREFER_DEVICE | MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
..Default::default()
},
quad_vertices,
).context("Failed to create quad vertex buffer")?;
log::info!("Quad vertex buffer created.");
let previous_frame_end = Some(sync::now(device.clone()).boxed());
Ok(Self {
_library: library,
instance,
surface,
device,
queue,
memory_allocator,
command_buffer_allocator,
render_pass,
pipeline,
swapchain,
framebuffers,
recreate_swapchain: false,
previous_frame_end,
quad_vertex_buffer,
})
}
fn select_physical_device(
instance: &Arc<Instance>,
surface: &Arc<Surface>,
) -> Result<(Arc<PhysicalDevice>, u32)> {
instance
.enumerate_physical_devices()
.context("Failed to enumerate physical devices")?
.filter(|p| p.supported_extensions().contains(&DeviceExtensions { khr_swapchain: true, ..DeviceExtensions::empty() }))
.filter_map(|p| {
p.queue_family_properties()
.iter()
.enumerate()
.position(|(i, q)| {
q.queue_flags.contains(QueueFlags::GRAPHICS)
&& p.surface_support(i as u32, surface).unwrap_or(false)
})
.map(|i| (p, i as u32))
})
.min_by_key(|(p, _)| match p.properties().device_type {
PhysicalDeviceType::DiscreteGpu => 0,
PhysicalDeviceType::IntegratedGpu => 1,
PhysicalDeviceType::VirtualGpu => 2,
PhysicalDeviceType::Cpu => 3,
_ => 4,
})
.context("No suitable physical device found")
}
fn create_swapchain(
device: Arc<Device>,
surface: Arc<Surface>,
physical_device: Arc<PhysicalDevice>,
window_size: winit::dpi::PhysicalSize<u32>,
) -> Result<(Arc<Swapchain>, Vec<Arc<Image>>)> {
let capabilities = physical_device
.surface_capabilities(&surface, Default::default())
.context("Failed to get surface capabilities")?;
let image_format = physical_device
.surface_formats(&surface, Default::default())
.context("Failed to get surface formats")?
.iter()
.min_by_key(|(f, _)| match f {
// Prefer sRGB if available, otherwise just pick the first one
Format::R8G8B8A8_SRGB => 0,
Format::B8G8R8A8_SRGB => 0,
_ => 1,
})
.map(|(f, _)| *f)
.unwrap_or(Format::B8G8R8A8_SRGB); // Fallback
let present_mode = capabilities.present_modes.iter().next().unwrap(); // Just pick the first available
let image_extent = capabilities.current_extent.unwrap_or([
window_size.width,
window_size.height,
]);
Swapchain::new(
device.clone(),
surface.clone(),
SwapchainCreateInfo {
min_image_count: capabilities.min_image_count,
image_format,
image_extent,
image_array_layers: 1,
image_usage: ImageUsage::COLOR_ATTACHMENT,
composite_alpha: capabilities.supported_composite_alpha.iter().next().unwrap(),
present_mode,
clipped: true,
..Default::default()
},
)
.context("Failed to create swapchain")
}
fn create_render_pass(device: Arc<Device>, format: Format) -> Result<Arc<RenderPass>> {
vulkano::single_pass_renderpass! {
device.clone(),
attachments: {
color: {
load: Clear,
store: Store,
format: format,
samples: 1,
}
},
pass: {
color: [color],
depth_stencil: {}
}
}
.context("Failed to create render pass")
}
fn create_pipeline(
device: Arc<Device>,
render_pass: Arc<RenderPass>,
image_extent: [u32; 2],
) -> Result<Arc<GraphicsPipeline>> {
let vs = vs::load(device.clone()).context("Failed to load vertex shader")?;
let fs = fs::load(device.clone()).context("Failed to load fragment shader")?;
GraphicsPipeline::new(
device.clone(),
Some(vs.entry_point("main").unwrap()),
Some(fs.entry_point("main").unwrap()),
InputAssemblyState::new(),
ViewportState::viewport_fixed_scissor_irrelevant([Viewport {
offset: [0.0, 0.0],
extent: [image_extent[0] as f32, image_extent[1] as f32],
depth_range: 0.0..=1.0,
}]),
render_pass.clone().into(),
None,
)
.context("Failed to create graphics pipeline")
}
fn create_framebuffers(
images: &[Arc<SwapchainImage>],
render_pass: Arc<RenderPass>,
) -> Result<Vec<Arc<Framebuffer>>> {
images
.iter()
.map(|image| {
let view = ImageView::new_default(image.clone()).context("Failed to create image view")?;
Framebuffer::new(
render_pass.clone(),
FramebufferCreateInfo {
attachments: vec![view],
..Default::default()
},
)
.context("Failed to create framebuffer")
})
.collect::<Result<Vec<_>>>()
}
fn recreate_swapchain_if_needed(&mut self, new_dimensions: [u32; 2]) -> Result<()> {
if self.recreate_swapchain {
self.recreate_swapchain = false;
let (new_swapchain, new_images) = Self::create_swapchain(
self.device.clone(),
self.surface.clone(),
self.device.physical_device().clone(),
winit::dpi::PhysicalSize::new(new_dimensions[0], new_dimensions[1]),
)?;
self.swapchain = new_swapchain;
self.framebuffers = Self::create_framebuffers(&new_images, self.render_pass.clone())?;
self.pipeline = Self::create_pipeline(
self.device.clone(),
self.render_pass.clone(),
new_dimensions,
)?;
log::info!("Swapchain recreated with new dimensions: {:?}", new_dimensions);
}
Ok(())
}
fn render(&mut self, physics_state: &PhysicsState) -> Result<()> {
self.previous_frame_end.as_mut().unwrap().cleanup_finished();
let image_extent = self.swapchain.image_extent();
if image_extent[0] == 0 || image_extent[1] == 0 {
// Window is minimized, skip rendering
return Ok(());
}
self.recreate_swapchain_if_needed(image_extent)?;
let (image_index, suboptimal, acquire_future) =
match acquire_next_image(self.swapchain.clone(), None) {
Ok(r) => r,
Err(vulkano::sync::AcquireError::OutOfDate) => {
self.recreate_swapchain = true;
return Ok(());
}
Err(e) => return Err(anyhow::Error::from(e).context("Failed to acquire next swapchain image")),
};
if suboptimal {
self.recreate_swapchain = true;
}
let mut builder = AutoCommandBufferBuilder::primary(
&self.command_buffer_allocator,
self.queue.queue_family_index(),
CommandBufferUsage::OneTimeSubmit, // Command buffer will be submitted once
).context("Failed to create command buffer builder")?;
builder
.begin_render_pass(
RenderPassBeginInfo {
clear_values: vec![Some([0.0, 0.0, 0.1, 1.0].into())], // Clear to dark blue
..RenderPassBeginInfo::framebuffer(self.framebuffers[image_index as usize].clone())
},
SubpassContents::Inline,
)?
.bind_pipeline_graphics(self.pipeline.clone())?;
// Draw each circle
for circle in &physics_state.circles {
let push_constants = fs::PushConstants {
center: circle.position.into(),
radius: circle.radius,
color: circle.color.into(),
};
builder
.push_constants(self.pipeline.layout().clone(), 0, push_constants)?
.bind_vertex_buffers(0, self.quad_vertex_buffer.clone())?
.draw(self.quad_vertex_buffer.len() as u32, 1, 0, 0)?;
}
builder.end_render_pass()?;
let command_buffer = builder.build().context("Failed to build command buffer")?;
let future = self
.previous_frame_end
.take()
.unwrap()
.join(acquire_future)
.then_execute(self.queue.clone(), command_buffer)
.context("Failed to execute command buffer")?
.then_swapchain_present(self.queue.clone(), self.swapchain.clone(), image_index)
.then_signal_fence_and_flush();
match future {
Ok(future) => {
self.previous_frame_end = Some(future.boxed());
}
Err(vulkano::sync::FlushError::OutOfDate) => {
self.recreate_swapchain = true;
self.previous_frame_end = Some(sync::now(self.device.clone()).boxed());
}
Err(e) => {
log::error!("Failed to flush future: {:?}", e);
self.previous_frame_end = Some(sync::now(self.device.clone()).boxed());
}
}
Ok(())
}
}
#[tokio::main]
async fn main() -> Result<()> {
// Initialize logging (useful for Vulkano debug messages)
env_logger::init();
// --- Setup communication channels ---
// `watch` channel is great for sending the latest state, dropping older ones if not read.
let initial_physics_state = PhysicsState {
circles: vec![
Circle {
position: Vector2::new(0.0, 0.0),
radius: 0.1,
color: Vector4::new(1.0, 0.0, 0.0, 1.0), // Red
},
Circle {
position: Vector2::new(0.5, 0.5),
radius: 0.05,
color: Vector4::new(0.0, 1.0, 0.0, 1.0), // Green
},
Circle {
position: Vector2::new(-0.5, -0.5),
radius: 0.08,
color: Vector4::new(0.0, 0.0, 1.0, 1.0), // Blue
},
],
};
let (physics_sender, physics_receiver) = watch::channel(initial_physics_state.clone());
// --- Spawn Physics Task ---
tokio::spawn(physics_task(physics_sender, initial_physics_state));
// --- Setup Winit Event Loop and Renderer ---
let event_loop = EventLoop::new().context("Failed to create event loop")?;
let mut renderer = Renderer::new(&event_loop)?;
// --- Main Rendering Loop ---
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Poll; // Continuously poll for events and redraw
match event {
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => {
*control_flow = ControlFlow::Exit;
}
Event::WindowEvent {
event: WindowEvent::Resized(_),
..
} => {
renderer.recreate_swapchain = true;
}
Event::MainEventsCleared => {
// This event fires when all other events have been processed.
// It's a good place to trigger a redraw.
// Get the latest physics state from the watch channel
let current_physics_state = physics_receiver.borrow().clone();
// Render the current state
if let Err(e) = renderer.render(&current_physics_state) {
log::error!("Rendering error: {:?}", e);
// Handle critical rendering errors, e.g., exit application
*control_flow = ControlFlow::Exit;
}
}
_ => (),
}
})?;
Ok(())
}

85
src/shaders/frag.glsl Normal file
View File

@ -0,0 +1,85 @@
#version 450
// Output color
layout(location = 0) out vec4 f_color;
// Push constants for circle data (must match vertex shader)
layout(push_constant) uniform PushConstants {
vec2 center;
float radius;
vec4 color;
} push_constants;
void main() {
// Get the fragment's position in normalized device coordinates (NDC)
// which is the same space as our circle's center and radius
vec2 frag_pos_ndc = gl_FragCoord.xy / vec2(gl_FragCoord.w) * 0.5 + 0.5; // Convert to [0,1] range, then adjust to [-1,1]
// Convert gl_FragCoord from window coordinates to NDC
// gl_FragCoord.xy are pixel coordinates, gl_FragCoord.w is 1.0 for non-perspective.
// We need to transform these to the [-1, 1] NDC space that our vertex shader outputs.
// This assumes a viewport from (0,0) to (width, height)
// A more robust solution would involve passing screen dimensions as a uniform.
// For now, let's assume gl_FragCoord.xy is already in a space where distance makes sense relative to NDC.
// A simpler approach for this example is to use the `position` from the vertex shader
// which is implicitly interpolated, but that's not how fragment shaders work directly.
// Let's simplify and assume a full-screen quad is drawn, and we're calculating distance from the center.
// This is a common pattern for drawing circles or other 2D shapes in a fragment shader.
// The `position` from the vertex shader is interpolated across the quad.
// We need to pass the *vertex* position to the fragment shader to calculate distance.
// Let's modify the vertex shader to pass the transformed position.
// Corrected approach: Pass the transformed vertex position to the fragment shader
// The vertex shader will output `gl_Position` which is the clip-space coordinate.
// The fragment shader receives `gl_FragCoord` which is window-space.
// To correctly calculate the distance, we need the fragment's position *relative to the circle's center in NDC*.
// This means we need to transform gl_FragCoord.xy back to NDC, or pass the vertex position.
// Let's adjust the vertex shader to pass the local quad coordinates to the fragment shader
// and the fragment shader will use that to determine distance.
// For a simple circle drawn within a quad, the most common approach is to pass
// the *normalized* coordinates of the quad to the fragment shader.
// The vertex shader would output `vec2 v_uv;` and the fragment shader would take `layout(location = 0) in vec2 v_uv;`
// Then `distance(v_uv, vec2(0.0, 0.0))` would be used.
// Let's refine the shaders for this common pattern:
// Vert shader passes a UV coordinate (0 to 1 or -1 to 1) for the quad.
// Frag shader uses this UV to calculate distance from the center of its *local* quad.
// The push constants will then position and scale this quad.
// REVISED SHADER PLAN:
// vert.glsl:
// - input: vec2 in_pos (for a unit quad, e.g., (-1,-1) to (1,1))
// - push_constants: center (vec2), radius (float), color (vec4)
// - output: vec2 v_uv (pass in_pos as v_uv)
// - gl_Position = vec4(center + in_pos * radius, 0.0, 1.0);
// frag.glsl:
// - input: vec2 v_uv
// - push_constants: color (vec4) (center/radius not needed here if v_uv is relative)
// - calculate distance from v_uv to (0,0)
// - if distance > 1.0, discard
// - else, output push_constants.color
// Let's go with this revised plan for the shaders.
// This is the fragment shader for the *revised* plan.
layout(location = 0) in vec2 v_uv; // Interpolated UV coordinate from vertex shader
layout(push_constant) uniform PushConstants {
vec2 center; // Not directly used for distance calculation here, but for context
float radius; // Not directly used for distance calculation here
vec4 color;
} push_constants;
void main() {
// Calculate distance from the center of the quad (which is (0,0) in v_uv space)
float dist = length(v_uv);
// Discard fragments outside the unit circle
if (dist > 1.0) {
discard;
}
f_color = push_constants.color;
}

25
src/shaders/vert.glsl Normal file
View File

@ -0,0 +1,25 @@
#version 450
// Vertex input: position for a unit quad (-1, -1) to (1, 1)
layout(location = 0) in vec2 in_pos;
// Output to fragment shader: interpolated UV for the quad
layout(location = 0) out vec2 v_uv;
// Push constants for circle data
layout(push_constant) uniform PushConstants {
vec2 center;
float radius;
vec4 color;
} push_constants;
void main() {
// Transform the unit quad vertex to be centered at `push_constants.center`
// and scaled by `push_constants.radius`.
// This effectively creates a square around the circle's center with side length 2*radius.
gl_Position = vec4(push_constants.center + in_pos * push_constants.radius, 0.0, 1.0);
// Pass the original unit quad position (UV) to the fragment shader.
// The fragment shader will use this to determine if the fragment is inside the circle.
v_uv = in_pos;
}