From db009a9ce59ff731353b09a74e4250f14a7d4dae Mon Sep 17 00:00:00 2001 From: Till Date: Wed, 17 Jul 2019 01:05:44 +0200 Subject: [PATCH] triangle and line --- .gitignore | 79 ++++++++ .idea/misc.xml | 6 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + Cargo.toml | 14 ++ rust-engine.iml | 15 ++ shaders/triangle.frag | 7 + shaders/triangle.vert | 7 + src/main.rs | 5 + src/vulkan.rs | 444 ++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 591 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 Cargo.toml create mode 100644 rust-engine.iml create mode 100644 shaders/triangle.frag create mode 100644 shaders/triangle.vert create mode 100644 src/main.rs create mode 100644 src/vulkan.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e24b57a --- /dev/null +++ b/.gitignore @@ -0,0 +1,79 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..639900d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..c3b3730 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7a894cf --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "rust-engine" +version = "0.1.0" +authors = ["Till "] +edition = "2018" + +[dependencies] +vulkano-shaders = "0.13" +vulkano = "0.13" +vulkano-win = "0.13" +cgmath = "0.17" +image = "0.21" +winit = "0.19" +time = "0.1.37" \ No newline at end of file diff --git a/rust-engine.iml b/rust-engine.iml new file mode 100644 index 0000000..7fe828a --- /dev/null +++ b/rust-engine.iml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/shaders/triangle.frag b/shaders/triangle.frag new file mode 100644 index 0000000..f40b64b --- /dev/null +++ b/shaders/triangle.frag @@ -0,0 +1,7 @@ +#version 450 + +layout(location = 0) out vec4 f_color; + +void main() { + f_color = vec4(1.0, 0.0, 0.0, 1.0); +} \ No newline at end of file diff --git a/shaders/triangle.vert b/shaders/triangle.vert new file mode 100644 index 0000000..eca2400 --- /dev/null +++ b/shaders/triangle.vert @@ -0,0 +1,7 @@ +#version 450 + +layout(location = 0) in vec2 position; + +void main() { + gl_Position = vec4(position, 0.0, 1.0); +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..ef0c8f6 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,5 @@ +mod vulkan; + +fn main() { + vulkan::init(); +} \ No newline at end of file diff --git a/src/vulkan.rs b/src/vulkan.rs new file mode 100644 index 0000000..a59ac5b --- /dev/null +++ b/src/vulkan.rs @@ -0,0 +1,444 @@ +use vulkano::buffer::{BufferUsage, CpuAccessibleBuffer}; +use vulkano::command_buffer::{AutoCommandBufferBuilder, DynamicState}; +use vulkano::device::{Device, DeviceExtensions}; +use vulkano::framebuffer::{Framebuffer, FramebufferAbstract, Subpass, RenderPassAbstract}; +use vulkano::image::SwapchainImage; +use vulkano::instance::{Instance, PhysicalDevice}; +use vulkano::pipeline::GraphicsPipeline; +use vulkano::pipeline::viewport::Viewport; +use vulkano::swapchain::{AcquireError, PresentMode, SurfaceTransform, Swapchain, SwapchainCreationError}; +use vulkano::swapchain; +use vulkano::sync::{GpuFuture, FlushError}; +use vulkano::sync; + +use vulkano_win::VkSurfaceBuild; + +use winit::{EventsLoop, Window, WindowBuilder, Event, WindowEvent}; + +use std::sync::Arc; + +mod vs { + vulkano_shaders::shader!{ + ty: "vertex", + path: "shaders/triangle.vert", + } +} + +mod fs { + vulkano_shaders::shader!{ + ty: "fragment", + path: "shaders/triangle.frag", + } +} + +mod line_vs { + vulkano_shaders::shader!{ + ty: "vertex", + path: "shaders/triangle.vert", + } +} + +mod line_fs { + vulkano_shaders::shader!{ + ty: "fragment", + path: "shaders/triangle.frag", + } +} + +pub fn init() { + let instance = { + let extensions = vulkano_win::required_extensions(); + Instance::new(None, &extensions, None).unwrap() + }; + let physical = PhysicalDevice::enumerate(&instance).next().unwrap(); + println!("Using device: {} (type: {:?})", physical.name(), physical.ty()); + + let mut events_loop = EventsLoop::new(); + let surface = WindowBuilder::new().build_vk_surface(&events_loop, instance.clone()).unwrap(); + let window = surface.window(); + + // The next step is to choose which GPU queue will execute our draw commands. + // + // Devices can provide multiple queues to run commands in parallel (for example a draw queue + // and a compute queue), similar to CPU threads. This is something you have to have to manage + // manually in Vulkan. + // + // In a real-life application, we would probably use at least a graphics queue and a transfers + // queue to handle data transfers in parallel. In this example we only use one queue. + // + // We have to choose which queues to use early on, because we will need this info very soon. + let queue_family = physical.queue_families().find(|&q| { + // We take the first queue that supports drawing to our window. + q.supports_graphics() && surface.is_supported(q).unwrap_or(false) + }).unwrap(); + + // Now initializing the device. This is probably the most important object of Vulkan. + // + // We have to pass five parameters when creating a device: + // + // - Which physical device to connect to. + // + // - A list of optional features and extensions that our program needs to work correctly. + // Some parts of the Vulkan specs are optional and must be enabled manually at device + // creation. In this example the only thing we are going to need is the `khr_swapchain` + // extension that allows us to draw to a window. + // + // - A list of layers to enable. This is very niche, and you will usually pass `None`. + // + // - The list of queues that we are going to use. The exact parameter is an iterator whose + // items are `(Queue, f32)` where the floating-point represents the priority of the queue + // between 0.0 and 1.0. The priority of the queue is a hint to the implementation about how + // much it should prioritize queues between one another. + // + // The list of created queues is returned by the function alongside with the device. + let device_ext = DeviceExtensions { khr_swapchain: true, .. DeviceExtensions::none() }; + let (device, mut queues) = Device::new(physical, physical.supported_features(), &device_ext, + [(queue_family, 0.5)].iter().cloned()).unwrap(); + + // Since we can request multiple queues, the `queues` variable is in fact an iterator. In this + // example we use only one queue, so we just retrieve the first and only element of the + // iterator and throw it away. + let queue = queues.next().unwrap(); + + // Before we can draw on the surface, we have to create what is called a swapchain. Creating + // a swapchain allocates the color buffers that will contain the image that will ultimately + // be visible on the screen. These images are returned alongside with the swapchain. + let (mut swapchain, images) = { + // Querying the capabilities of the surface. When we create the swapchain we can only + // pass values that are allowed by the capabilities. + let caps = surface.capabilities(physical).unwrap(); + + let usage = caps.supported_usage_flags; + + // The alpha mode indicates how the alpha value of the final image will behave. For example + // you can choose whether the window will be opaque or transparent. + let alpha = caps.supported_composite_alpha.iter().next().unwrap(); + + // Choosing the internal format that the images will have. + let format = caps.supported_formats[0].0; + + // The dimensions of the window, only used to initially setup the swapchain. + // NOTE: + // On some drivers the swapchain dimensions are specified by `caps.current_extent` and the + // swapchain size must use these dimensions. + // These dimensions are always the same as the window dimensions + // + // However other drivers dont specify a value i.e. `caps.current_extent` is `None` + // These drivers will allow anything but the only sensible value is the window dimensions. + // + // Because for both of these cases, the swapchain needs to be the window dimensions, we just use that. + let initial_dimensions = if let Some(dimensions) = window.get_inner_size() { + // convert to physical pixels + let dimensions: (u32, u32) = dimensions.to_physical(window.get_hidpi_factor()).into(); + [dimensions.0, dimensions.1] + } else { + // The window no longer exists so exit the application. + return; + }; + + // Please take a look at the docs for the meaning of the parameters we didn't mention. + Swapchain::new(device.clone(), surface.clone(), caps.min_image_count, format, + initial_dimensions, 1, usage, &queue, SurfaceTransform::Identity, alpha, + PresentMode::Fifo, true, None).unwrap() + + }; + + #[derive(Default, Debug, Clone)] + struct Vertex { position: [f32; 2] } + vulkano::impl_vertex!(Vertex, position); + // We now create a buffer that will store the shape of our triangle. + let vertex_buffer = { + CpuAccessibleBuffer::from_iter(device.clone(), BufferUsage::all(), [ + Vertex { position: [-0.5, -0.25] }, + Vertex { position: [0.0, 0.5] }, + Vertex { position: [0.25, -0.1] } + ].iter().cloned()).unwrap() + }; + + let line_vertex_buffer = { + CpuAccessibleBuffer::from_iter(device.clone(), BufferUsage::all(), [ + Vertex { position: [-0.4, -0.3] }, + Vertex { position: [0.01, 0.55] }, + ].iter().cloned()).unwrap() + }; + + // The next step is to create the shaders. + // + // The raw shader creation API provided by the vulkano library is unsafe, for various reasons. + // + // An overview of what the `vulkano_shaders::shader!` macro generates can be found in the + // `vulkano-shaders` crate docs. You can view them at https://docs.rs/vulkano-shaders/ + // + // TODO: explain this in details + + // At this point, OpenGL initialization would be finished. However in Vulkan it is not. OpenGL + // implicitly does a lot of computation whenever you draw. In Vulkan, you have to do all this + // manually. + + // The next step is to create a *render pass*, which is an object that describes where the + // output of the graphics pipeline will go. It describes the layout of the images + // where the colors, depth and/or stencil information will be written. + let render_pass = Arc::new(vulkano::single_pass_renderpass!( + device.clone(), + attachments: { + // `color` is a custom name we give to the first and only attachment. + color: { + // `load: Clear` means that we ask the GPU to clear the content of this + // attachment at the start of the drawing. + load: Clear, + // `store: Store` means that we ask the GPU to store the output of the draw + // in the actual image. We could also ask it to discard the result. + store: Store, + // `format: ` indicates the type of the format of the image. This has to + // be one of the types of the `vulkano::format` module (or alternatively one + // of your structs that implements the `FormatDesc` trait). Here we use the + // same format as the swapchain. + format: swapchain.format(), + // TODO: + samples: 1, + } + }, + pass: { + // We use the attachment named `color` as the one and only color attachment. + color: [color], + // No depth-stencil attachment is indicated with empty brackets. + depth_stencil: {} + } + ).unwrap()); + + let vs = vs::Shader::load(device.clone()).unwrap(); + let fs = fs::Shader::load(device.clone()).unwrap(); + + let line_vs = line_vs::Shader::load(device.clone()).unwrap(); + let line_fs = line_fs::Shader::load(device.clone()).unwrap(); + + let sub_pass = Subpass::from(render_pass.clone(), 0).unwrap(); + + // Before we draw we have to create what is called a pipeline. This is similar to an OpenGL + // program, but much more specific. + let pipeline = Arc::new(GraphicsPipeline::start() + // We need to indicate the layout of the vertices. + // The type `SingleBufferDefinition` actually contains a template parameter corresponding + // to the type of each vertex. But in this code it is automatically inferred. + .vertex_input_single_buffer() + // A Vulkan shader can in theory contain multiple entry points, so we have to specify + // which one. The `main` word of `main_entry_point` actually corresponds to the name of + // the entry point. + .vertex_shader(vs.main_entry_point(), ()) + // The content of the vertex buffer describes a list of triangles. + .triangle_list() + // Use a resizable viewport set to draw over the entire window + .viewports_dynamic_scissors_irrelevant(1) + // See `vertex_shader`. + .fragment_shader(fs.main_entry_point(), ()) + // We have to indicate which subpass of which render pass this pipeline is going to be used + // in. The pipeline will only be usable from this particular subpass. + .render_pass(sub_pass.clone()) + // Now that our builder is filled, we call `build()` to obtain an actual pipeline. + .build(device.clone()) + .unwrap()); + + let line_pipeline = Arc::new(GraphicsPipeline::start() + .vertex_input_single_buffer() + .vertex_shader(line_vs.main_entry_point(), ()) + .line_list() + .viewports_dynamic_scissors_irrelevant(1) + .fragment_shader(line_fs.main_entry_point(), ()) + .render_pass(sub_pass.clone()) + .build(device.clone()) + .unwrap()); + + // Dynamic viewports allow us to recreate just the viewport when the window is resized + // Otherwise we would have to recreate the whole pipeline. + let mut dynamic_state = DynamicState { line_width: None, viewports: None, scissors: None }; + + // The render pass we created above only describes the layout of our framebuffers. Before we + // can draw we also need to create the actual framebuffers. + // + // Since we need to draw to multiple images, we are going to create a different framebuffer for + // each image. + let mut framebuffers = window_size_dependent_setup(&images, render_pass.clone(), &mut dynamic_state); + + // Initialization is finally finished! + + // In some situations, the swapchain will become invalid by itself. This includes for example + // when the window is resized (as the images of the swapchain will no longer match the + // window's) or, on Android, when the application went to the background and goes back to the + // foreground. + // + // In this situation, acquiring a swapchain image or presenting it will return an error. + // Rendering to an image of that swapchain will not produce any error, but may or may not work. + // To continue rendering, we need to recreate the swapchain by creating a new swapchain. + // Here, we remember that we need to do this for the next loop iteration. + let mut recreate_swapchain = false; + + // In the loop below we are going to submit commands to the GPU. Submitting a command produces + // an object that implements the `GpuFuture` trait, which holds the resources for as long as + // they are in use by the GPU. + // + // Destroying the `GpuFuture` blocks until the GPU is finished executing it. In order to avoid + // that, we store the submission of the previous frame here. + let mut previous_frame_end = Box::new(sync::now(device.clone())) as Box; + + loop { + // It is important to call this function from time to time, otherwise resources will keep + // accumulating and you will eventually reach an out of memory error. + // Calling this function polls various fences in order to determine what the GPU has + // already processed, and frees the resources that are no longer needed. + previous_frame_end.cleanup_finished(); + + // Whenever the window resizes we need to recreate everything dependent on the window size. + // In this example that includes the swapchain, the framebuffers and the dynamic state viewport. + if recreate_swapchain { + // Get the new dimensions of the window. + let dimensions = if let Some(dimensions) = window.get_inner_size() { + let dimensions: (u32, u32) = dimensions.to_physical(window.get_hidpi_factor()).into(); + [dimensions.0, dimensions.1] + } else { + return; + }; + + let (new_swapchain, new_images) = match swapchain.recreate_with_dimension(dimensions) { + Ok(r) => r, + // This error tends to happen when the user is manually resizing the window. + // Simply restarting the loop is the easiest way to fix this issue. + Err(SwapchainCreationError::UnsupportedDimensions) => continue, + Err(err) => panic!("{:?}", err) + }; + + swapchain = new_swapchain; + // Because framebuffers contains an Arc on the old swapchain, we need to + // recreate framebuffers as well. + framebuffers = window_size_dependent_setup(&new_images, render_pass.clone(), &mut dynamic_state); + + recreate_swapchain = false; + } + + // Before we can draw on the output, we have to *acquire* an image from the swapchain. If + // no image is available (which happens if you submit draw commands too quickly), then the + // function will block. + // This operation returns the index of the image that we are allowed to draw upon. + // + // This function can block if no image is available. The parameter is an optional timeout + // after which the function call will return an error. + let (image_num, acquire_future) = match swapchain::acquire_next_image(swapchain.clone(), None) { + Ok(r) => r, + Err(AcquireError::OutOfDate) => { + recreate_swapchain = true; + continue; + }, + Err(err) => panic!("{:?}", err) + }; + + // Specify the color to clear the framebuffer with i.e. blue + let clear_values = vec!([0.0, 0.0, 1.0, 1.0].into()); + + // In order to draw, we have to build a *command buffer*. The command buffer object holds + // the list of commands that are going to be executed. + // + // Building a command buffer is an expensive operation (usually a few hundred + // microseconds), but it is known to be a hot path in the driver and is expected to be + // optimized. + // + // Note that we have to pass a queue family when we create the command buffer. The command + // buffer will only be executable on that given queue family. + let command_buffer = AutoCommandBufferBuilder::primary_one_time_submit(device.clone(), queue.family()).unwrap() + // Before we can draw, we have to *enter a render pass*. There are two methods to do + // this: `draw_inline` and `draw_secondary`. The latter is a bit more advanced and is + // not covered here. + // + // The third parameter builds the list of values to clear the attachments with. The API + // is similar to the list of attachments when building the framebuffers, except that + // only the attachments that use `load: Clear` appear in the list. + .begin_render_pass(framebuffers[image_num].clone(), false, clear_values) + .unwrap() + + // We are now inside the first subpass of the render pass. We add a draw command. + // + // The last two parameters contain the list of resources to pass to the shaders. + // Since we used an `EmptyPipeline` object, the objects have to be `()`. + .draw(pipeline.clone(), &dynamic_state, vertex_buffer.clone(), (), ()) + .unwrap() + .draw(line_pipeline.clone(), &dynamic_state, line_vertex_buffer.clone(), (), ()) + .unwrap() + + // We leave the render pass by calling `draw_end`. Note that if we had multiple + // subpasses we could have called `next_inline` (or `next_secondary`) to jump to the + // next subpass. + .end_render_pass() + .unwrap() + + // Finish building the command buffer by calling `build`. + .build().unwrap(); + + let future = previous_frame_end.join(acquire_future) + .then_execute(queue.clone(), command_buffer).unwrap() + + // The color output is now expected to contain our triangle. But in order to show it on + // the screen, we have to *present* the image by calling `present`. + // + // This function does not actually present the image immediately. Instead it submits a + // present command at the end of the queue. This means that it will only be presented once + // the GPU has finished executing the command buffer that draws the triangle. + .then_swapchain_present(queue.clone(), swapchain.clone(), image_num) + .then_signal_fence_and_flush(); + + match future { + Ok(future) => { + previous_frame_end = Box::new(future) as Box<_>; + } + Err(FlushError::OutOfDate) => { + recreate_swapchain = true; + previous_frame_end = Box::new(sync::now(device.clone())) as Box<_>; + } + Err(e) => { + println!("{:?}", e); + previous_frame_end = Box::new(sync::now(device.clone())) as Box<_>; + } + } + + // Note that in more complex programs it is likely that one of `acquire_next_image`, + // `command_buffer::submit`, or `present` will block for some time. This happens when the + // GPU's queue is full and the driver has to wait until the GPU finished some work. + // + // Unfortunately the Vulkan API doesn't provide any way to not wait or to detect when a + // wait would happen. Blocking may be the desired behavior, but if you don't want to + // block you should spawn a separate thread dedicated to submissions. + + // Handling the window events in order to close the program when the user wants to close + // it. + let mut done = false; + events_loop.poll_events(|ev| { + match ev { + Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => done = true, + Event::WindowEvent { event: WindowEvent::Resized(_), .. } => recreate_swapchain = true, + _ => () + } + }); + if done { return; } + } +} + +/// This method is called once during initialization, then again whenever the window is resized +fn window_size_dependent_setup( + images: &[Arc>], + render_pass: Arc, + dynamic_state: &mut DynamicState +) -> Vec> { + let dimensions = images[0].dimensions(); + + let viewport = Viewport { + origin: [0.0, 0.0], + dimensions: [dimensions[0] as f32, dimensions[1] as f32], + depth_range: 0.0 .. 1.0, + }; + dynamic_state.viewports = Some(vec!(viewport)); + + images.iter().map(|image| { + Arc::new( + Framebuffer::start(render_pass.clone()) + .add(image.clone()).unwrap() + .build().unwrap() + ) as Arc + }).collect::>() +}