drawing a red quad

This commit is contained in:
Sven Vogel 2023-04-06 15:40:50 +02:00
parent 8cf63a9348
commit 35083ee148
11 changed files with 630 additions and 20 deletions

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

12
.idea/eruption.iml Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/misc.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/eruption.iml" filepath="$PROJECT_DIR$/.idea/eruption.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

61
Cargo.lock generated
View File

@ -181,6 +181,15 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
[[package]]
name = "cmake"
version = "0.1.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130"
dependencies = [
"cc",
]
[[package]]
name = "core-foundation"
version = "0.9.3"
@ -282,6 +291,7 @@ name = "eruption"
version = "0.1.0"
dependencies = [
"vulkano",
"vulkano-shaders",
"vulkano-win",
"winit",
]
@ -760,6 +770,15 @@ version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "roxmltree"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "921904a62e410e37e215c40381b7117f830d9d89ba60ab5236170541dd25646b"
dependencies = [
"xmlparser",
]
[[package]]
name = "ryu"
version = "1.0.13"
@ -822,6 +841,27 @@ dependencies = [
"serde",
]
[[package]]
name = "shaderc"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31cef52787a0db5108788ea20bed13d6bf4b96287c5c5201e55725f7070f3443"
dependencies = [
"libc",
"shaderc-sys",
]
[[package]]
name = "shaderc-sys"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e8f8439fffcffd6efcd74197204addf935dbab5752696bd990a6cd36d54cf64"
dependencies = [
"cmake",
"libc",
"roxmltree",
]
[[package]]
name = "slotmap"
version = "1.0.6"
@ -1030,6 +1070,21 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "vulkano-shaders"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f8cf18e9becbc6d39f1c39e26bcf69546c93989553eb5748cd734a8a697a6e5"
dependencies = [
"ahash",
"heck",
"proc-macro2",
"quote",
"shaderc",
"syn 1.0.109",
"vulkano",
]
[[package]]
name = "vulkano-win"
version = "0.33.0"
@ -1343,3 +1398,9 @@ name = "xml-rs"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
[[package]]
name = "xmlparser"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d25c75bf9ea12c4040a97f829154768bbbce366287e2dc044af160cd79a13fd"

View File

@ -9,4 +9,5 @@ authors = ["Sven Vogel"]
[dependencies]
vulkano = "0.33.0"
vulkano-win = "0.33.0"
vulkano-shaders = "0.33.0"
winit = "0.28.3"

View File

@ -1,10 +1,57 @@
use vulkano::device::{DeviceExtensions, Properties, QueueFlags};
use vulkano::device::physical::PhysicalDeviceType;
mod shader;
use std::sync::Arc;
use vulkano::device::{Device, DeviceCreateInfo, DeviceExtensions, Properties, Queue, QueueCreateInfo, QueueFlags};
use vulkano::device::physical::{PhysicalDevice, PhysicalDeviceType};
use vulkano::image::{ImageAccess, ImageUsage, SwapchainImage};
use vulkano::instance::{Instance, InstanceCreateInfo};
use vulkano::VulkanLibrary;
use vulkano::memory::allocator::{AllocationCreateInfo, MemoryUsage, StandardMemoryAllocator};
use vulkano::swapchain::{acquire_next_image, AcquireError, Surface, Swapchain, SwapchainCreateInfo, SwapchainCreationError, SwapchainPresentInfo};
use vulkano::{sync, VulkanLibrary};
use vulkano_win::VkSurfaceBuild;
use winit::event_loop::EventLoop;
use winit::window::WindowBuilder;
use winit::event_loop::{ControlFlow, EventLoop};
use winit::window::{Window, WindowBuilder};
use vulkano::buffer::{Buffer, BufferContents, BufferCreateInfo, BufferUsage};
use vulkano::command_buffer::allocator::StandardCommandBufferAllocator;
use vulkano::command_buffer::{AutoCommandBufferBuilder, CommandBufferUsage, RenderPassBeginInfo, SubpassContents};
use vulkano::image::view::ImageView;
use vulkano::pipeline::graphics::vertex_input::Vertex;
use vulkano::pipeline::graphics::viewport::Viewport;
use vulkano::render_pass::{Framebuffer, FramebufferCreateInfo, RenderPass};
use vulkano::sync::{FlushError, GpuFuture};
use winit::event::{Event, WindowEvent};
// We now create a buffer that will store the shape of our triangle. We use `#[repr(C)]` here
// to force rustc to use a defined layout for our data, as the default representation has *no
// guarantees*.
#[derive(BufferContents, Vertex)]
#[repr(C)]
struct Vertex2d {
#[format(R32G32_SFLOAT)]
position: [f32; 2],
}
const QUAD_VERTICES: [Vertex2d; 6] = [
Vertex2d {
position: [-1.0, -1.0]
},
Vertex2d {
position: [1.0, -1.0]
},
Vertex2d {
position: [1.0, 1.0]
},
Vertex2d {
position: [-1.0, -1.0]
},
Vertex2d {
position: [1.0, 1.0]
},
Vertex2d {
position: [-1.0, 1.0]
}
];
pub fn init() {
let lib = VulkanLibrary::new().unwrap();
@ -38,6 +85,284 @@ pub fn init() {
.build_vk_surface(&event_loop, instance.clone())
.unwrap();
let (device, mut queues) = get_device(&instance, &surface);
// Since we can request multiple queues, the `queues` variable is in fact an iterator. We only
// use one queue in this example, so we just retrieve the first and only element of the
// iterator.
let queue = queues.next().unwrap();
let (mut swapchain, images) = create_swapchain(&device, &surface);
let memory_allocator = StandardMemoryAllocator::new_default(device.clone());
let vertex_buffer = Buffer::from_iter(
&memory_allocator,
BufferCreateInfo {
usage: BufferUsage::VERTEX_BUFFER,
..Default::default()
},
AllocationCreateInfo {
usage: MemoryUsage::Upload,
..Default::default()
},
QUAD_VERTICES,
).unwrap();
// 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.
let render_pass = create_render_pass(&device, &swapchain);
// 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 viewport = Viewport {
origin: [0.0, 0.0],
dimensions: [0.0, 0.0],
depth_range: 0.0..1.0,
};
// 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 viewport);
// Before we can start creating and recording command buffers, we need a way of allocating
// them. Vulkano provides a command buffer allocator, which manages raw Vulkan command pools
// underneath and provides a safe interface for them.
let command_buffer_allocator =
StandardCommandBufferAllocator::new(device.clone(), Default::default());
// 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 = Some(sync::now(device.clone()).boxed());
let pipeline = shader::create_program(&render_pass, &device);
event_loop.run(move |event, _, control_flow| {
match event {
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => {
*control_flow = ControlFlow::Exit;
}
Event::WindowEvent {
event: WindowEvent::Resized(_),
..
} => {
recreate_swapchain = true;
}
Event::RedrawEventsCleared => {
// Do not draw the frame when the screen dimensions are zero. On Windows, this can
// occur when minimizing the application.
let window = surface.object().unwrap().downcast_ref::<Window>().unwrap();
let dimensions = window.inner_size();
if dimensions.width == 0 || dimensions.height == 0 {
return;
}
// 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.as_mut().unwrap().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 {
// Use the new dimensions of the window.
let (new_swapchain, new_images) =
match swapchain.recreate(SwapchainCreateInfo {
image_extent: dimensions.into(),
..swapchain.create_info()
}) {
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::ImageExtentNotSupported { .. }) => return,
Err(e) => panic!("failed to recreate swapchain: {e}"),
};
swapchain = new_swapchain;
// Because framebuffers contains a reference to the old swapchain, we need to
// recreate framebuffers as well.
framebuffers = window_size_dependent_setup(
&new_images,
render_pass.clone(),
&mut viewport,
);
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_index, suboptimal, acquire_future) =
match acquire_next_image(swapchain.clone(), None) {
Ok(r) => r,
Err(AcquireError::OutOfDate) => {
recreate_swapchain = true;
return;
}
Err(e) => panic!("failed to acquire next image: {e}"),
};
// `acquire_next_image` can be successful, but suboptimal. This means that the
// swapchain image will still work, but it may not display correctly. With some
// drivers this can be when the window resizes, but it may not cause the swapchain
// to become out of date.
if suboptimal {
recreate_swapchain = true;
}
// 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 mut builder = AutoCommandBufferBuilder::primary(
&command_buffer_allocator,
queue.queue_family_index(),
CommandBufferUsage::OneTimeSubmit,
)
.unwrap();
builder
// Before we can draw, we have to *enter a render pass*.
.begin_render_pass(
RenderPassBeginInfo {
// A list of values to clear the attachments with. This list contains
// one item for each attachment in the render pass. In this case, there
// is only one attachment, and we clear it with a blue color.
//
// Only attachments that have `LoadOp::Clear` are provided with clear
// values, any others should use `ClearValue::None` as the clear value.
clear_values: vec![Some([0.0, 0.0, 1.0, 1.0].into())],
..RenderPassBeginInfo::framebuffer(
framebuffers[image_index as usize].clone(),
)
},
// The contents of the first (and only) subpass. This can be either
// `Inline` or `SecondaryCommandBuffers`. The latter is a bit more advanced
// and is not covered here.
SubpassContents::Inline,
)
.unwrap()
// We are now inside the first subpass of the render pass.
.set_viewport(0, [viewport.clone()])
.bind_pipeline_graphics(pipeline.clone())
.bind_vertex_buffers(0, vertex_buffer.clone())
// We add a draw command.
.draw(vertex_buffer.len() as u32, 1, 0, 0)
.unwrap()
// We leave the render pass. Note that if we had multiple subpasses we could
// have called `next_subpass` to jump to the next subpass.
.end_render_pass()
.unwrap();
// Finish building the command buffer by calling `build`.
let command_buffer = builder.build().unwrap();
let future = previous_frame_end
.take()
.unwrap()
.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
// `then_swapchain_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(),
SwapchainPresentInfo::swapchain_image_index(swapchain.clone(), image_index),
)
.then_signal_fence_and_flush();
match future {
Ok(future) => {
previous_frame_end = Some(future.boxed());
}
Err(FlushError::OutOfDate) => {
recreate_swapchain = true;
previous_frame_end = Some(sync::now(device.clone()).boxed());
}
Err(e) => {
panic!("failed to flush future: {e}");
// previous_frame_end = Some(sync::now(device.clone()).boxed());
}
}
}
_ => (),
}
});
}
/// This function is called once during initialization, then again whenever the window is resized.
fn window_size_dependent_setup(
images: &[Arc<SwapchainImage>],
render_pass: Arc<RenderPass>,
viewport: &mut Viewport,
) -> Vec<Arc<Framebuffer>> {
let dimensions = images[0].dimensions().width_height();
viewport.dimensions = [dimensions[0] as f32, dimensions[1] as f32];
images
.iter()
.map(|image| {
let view = ImageView::new_default(image.clone()).unwrap();
Framebuffer::new(
render_pass.clone(),
FramebufferCreateInfo {
attachments: vec![view],
..Default::default()
},
)
.unwrap()
})
.collect::<Vec<_>>()
}
fn get_device(instance: &Arc<Instance>, surface: &Arc<Surface>) -> (Arc<Device>, impl ExactSizeIterator<Item=Arc<Queue>> + Sized) {
// Choose device extensions that we're going to use. In order to present images to a surface,
// we need a `Swapchain`, which is provided by the `khr_swapchain` extension.
let device_extensions = DeviceExtensions {
@ -45,9 +370,75 @@ pub fn init() {
..DeviceExtensions::empty()
};
let (physical_device, queue_family_index) = choose_physical_device(&instance, &surface, device_extensions);
print_physical_device_info(physical_device.properties());
// Now initializing the device. This is probably the most important object of Vulkan.
//
// An iterator of created queues is returned by the function alongside the device.
Device::new(
// Which physical device to connect to.
physical_device,
DeviceCreateInfo {
// 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.
enabled_extensions: device_extensions,
// The list of queues that we are going to use. Here we only use one queue, from the
// previously chosen queue family.
queue_create_infos: vec![QueueCreateInfo {
queue_family_index,
..Default::default()
}],
..Default::default()
},
).unwrap()
}
fn create_render_pass(device: &Arc<Device>, swapchain: &Arc<Swapchain>) -> Arc<RenderPass> {
// 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.
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: <ty>` 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.image_format(),
// `samples: 1` means that we ask the GPU to use one sample to determine the value
// of each pixel in the color attachment. We could use a larger value
// (multisampling) for antialiasing. An example of this can be found in
// msaa-renderpass.rs.
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()
}
fn choose_physical_device(instance: &Arc<Instance>, surface: &Arc<Surface>, device_extensions: DeviceExtensions) -> (Arc<PhysicalDevice>, u32) {
// We then choose which physical device to use. First, we enumerate all the available physical
// devices, then apply filters to narrow them down to those that can support our needs.
let (physical_device, queue_family_index) = instance
instance
.enumerate_physical_devices()
.unwrap()
.filter(|p| {
@ -102,9 +493,67 @@ pub fn init() {
_ => 5,
}
})
.expect("no suitable physical device found");
.expect("no suitable physical device found")
}
print_physical_device_info(physical_device.properties());
fn create_swapchain(device: &Arc<Device>, surface: &Arc<Surface>) -> (Arc<Swapchain>, Vec<Arc<SwapchainImage>>) {
// 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 the swapchain.
// Querying the capabilities of the surface. When we create the swapchain we can only pass
// values that are allowed by the capabilities.
let surface_capabilities = device
.physical_device()
.surface_capabilities(&surface, Default::default())
.unwrap();
// Choosing the internal format that the images will have.
let image_format = Some(
device
.physical_device()
.surface_formats(&surface, Default::default())
.unwrap()[0]
.0,
);
let window = surface.object().unwrap().downcast_ref::<Window>().unwrap();
// Please take a look at the docs for the meaning of the parameters we didn't mention.
Swapchain::new(
device.clone(),
surface.clone(),
SwapchainCreateInfo {
min_image_count: surface_capabilities.min_image_count,
image_format,
// The dimensions of the window, only used to initially setup the swapchain.
//
// NOTE:
// On some drivers the swapchain dimensions are specified by
// `surface_capabilities.current_extent` and the swapchain size must use these
// dimensions. These dimensions are always the same as the window dimensions.
//
// However, other drivers don't specify a value, i.e.
// `surface_capabilities.current_extent` is `None`. These drivers will allow
// anything, but the only sensible value is the window dimensions.
//
// Both of these cases need the swapchain to use the window dimensions, so we just
// use that.
image_extent: window.inner_size().into(),
image_usage: ImageUsage::COLOR_ATTACHMENT,
// 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.
composite_alpha: surface_capabilities
.supported_composite_alpha
.into_iter()
.next()
.unwrap(),
..Default::default()
},
).unwrap()
}
fn print_physical_device_info(device_properties: &Properties) {
@ -120,15 +569,5 @@ fn print_device_driver_info(device_properties: &Properties) {
let default_info = String::from("<none>");
let info = device_properties.driver_info.as_ref().unwrap_or(&default_info);
format!("driver:\n\tname: {}\n\tversion: {}\n\t info: {}\n", name, device_properties.driver_version, info);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
init();
}
}
println!("driver:\n\tname: {}\n\tversion: {}\n\tinfo: {}\n", name, device_properties.driver_version, info);
}

7
src/main.rs Normal file
View File

@ -0,0 +1,7 @@
use eruption::init;
fn main() {
init();
}

25
src/shader/composit.rs Normal file
View File

@ -0,0 +1,25 @@
pub(crate) mod vs {
vulkano_shaders::shader! {
ty: "vertex",
src: r"
#version 450
layout(location = 0) in vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
",
}
}
pub(crate) mod fs {
vulkano_shaders::shader! {
ty: "fragment",
src: r"
#version 450
layout(location = 0) out vec4 f_color;
void main() {
f_color = vec4(1.0, 0.0, 0.0, 1.0);
}
",
}
}

37
src/shader/mod.rs Normal file
View File

@ -0,0 +1,37 @@
mod composit;
use std::sync::Arc;
use vulkano::device::Device;
use vulkano::pipeline::graphics::input_assembly::InputAssemblyState;
use vulkano::pipeline::graphics::vertex_input::Vertex;
use vulkano::pipeline::graphics::viewport::ViewportState;
use vulkano::pipeline::GraphicsPipeline;
use vulkano::render_pass::{RenderPass, Subpass};
use crate::Vertex2d;
pub fn create_program(render_pass: &Arc<RenderPass>, device: &Arc<Device>) -> Arc<GraphicsPipeline> {
let vs = composit::vs::load(device.clone()).unwrap();
let fs = composit::fs::load(device.clone()).unwrap();
// Before we draw we have to create what is called a pipeline. This is similar to an OpenGL
// program, but much more specific.
GraphicsPipeline::start()
// 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(Subpass::from(render_pass.clone(), 0).unwrap())
// We need to indicate the layout of the vertices.
.vertex_input_state(Vertex2d::per_vertex())
// The content of the vertex buffer describes a list of triangles.
.input_assembly_state(InputAssemblyState::new())
// A Vulkan shader can in theory contain multiple entry points, so we have to specify
// which one.
.vertex_shader(vs.entry_point("main").unwrap(), ())
// Use a resizable viewport set to draw over the entire window
.viewport_state(ViewportState::viewport_dynamic_scissor_irrelevant())
// See `vertex_shader`.
.fragment_shader(fs.entry_point("main").unwrap(), ())
// Now that our builder is filled, we call `build()` to obtain an actual pipeline.
.build(device.clone())
.unwrap()
}