From 591f81b5149ee487fb7a0d64cd700c867b5f72bb Mon Sep 17 00:00:00 2001 From: Jake Jensen Date: Mon, 7 Jul 2025 19:05:53 -0400 Subject: [PATCH] Added vert and shader, created Vulkan chain, started work on async timestep --- Cargo.lock | 272 ++++++++++++++++++- Cargo.toml | 7 +- src/main.rs | 603 +++++++++++++++++++++++++++++++++++++++++- src/shaders/frag.glsl | 85 ++++++ src/shaders/vert.glsl | 25 ++ 5 files changed, 988 insertions(+), 4 deletions(-) create mode 100644 src/shaders/frag.glsl create mode 100644 src/shaders/vert.glsl diff --git a/Cargo.lock b/Cargo.lock index e287db3..e01608f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 9679888..64aae45 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } diff --git a/src/main.rs b/src/main.rs index cc51e81..89d9107 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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, + pub radius: f32, + pub color: Vector4, +} + +/// The entire physics state to be sent to the renderer. +#[derive(Debug, Clone)] +pub struct PhysicsState { + pub circles: Vec, +} + +// 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, + 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, + instance: Arc, + surface: Arc, + device: Arc, + queue: Arc, + memory_allocator: Arc, + command_buffer_allocator: Arc, + render_pass: Arc, + pipeline: Arc, + swapchain: Arc, + framebuffers: Vec>, + recreate_swapchain: bool, + previous_frame_end: Option>, + quad_vertex_buffer: Subbuffer<[QuadVertex]>, // Static buffer for the quad +} + +impl Renderer { + fn new(event_loop: &EventLoop<()>) -> Result { + 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, + surface: &Arc, + ) -> Result<(Arc, 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, + surface: Arc, + physical_device: Arc, + window_size: winit::dpi::PhysicalSize, + ) -> Result<(Arc, Vec>)> { + 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, format: Format) -> Result> { + 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, + render_pass: Arc, + image_extent: [u32; 2], + ) -> Result> { + 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], + render_pass: Arc, + ) -> Result>> { + 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::>>() + } + + 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(¤t_physics_state) { + log::error!("Rendering error: {:?}", e); + // Handle critical rendering errors, e.g., exit application + *control_flow = ControlFlow::Exit; + } + } + _ => (), + } + })?; + + Ok(()) +} \ No newline at end of file diff --git a/src/shaders/frag.glsl b/src/shaders/frag.glsl new file mode 100644 index 0000000..8928ca5 --- /dev/null +++ b/src/shaders/frag.glsl @@ -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; + } \ No newline at end of file diff --git a/src/shaders/vert.glsl b/src/shaders/vert.glsl new file mode 100644 index 0000000..dbb6702 --- /dev/null +++ b/src/shaders/vert.glsl @@ -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; +} \ No newline at end of file