Reuse command and uniform buffers

This commit is contained in:
Mark Poliakov 2022-07-05 23:27:46 +03:00
parent f3387f03ae
commit 109ecf929c
7 changed files with 344 additions and 270 deletions

View File

@ -2,7 +2,7 @@ use std::fmt::{Display, Formatter};
use vulkano::{
device::{physical::SurfacePropertiesError, DeviceCreationError},
image::view::ImageViewCreationError,
image::{view::ImageViewCreationError, ImageCreationError},
instance::InstanceCreationError,
render_pass::FramebufferCreationError,
swapchain::SwapchainCreationError,
@ -18,6 +18,7 @@ pub enum EngineError {
VulkanSwapchainError(SwapchainCreationError),
VulkanSurfacePropertiesError(SurfacePropertiesError),
VulkanImageViewError(ImageViewCreationError),
VulkanImageError(ImageCreationError),
VulkanFramebufferError(FramebufferCreationError),
}
@ -52,5 +53,6 @@ wrap!(
FramebufferCreationError,
VulkanFramebufferError
);
wrap!(EngineError, ImageCreationError, VulkanImageError);
impl std::error::Error for EngineError {}

View File

@ -1,25 +1,28 @@
extern crate nalgebra_glm as glm;
use std::time::Instant;
use std::{sync::Arc, time::Instant};
use log::LevelFilter;
use render::event::RenderEvent;
use simplelog::{ColorChoice, CombinedLogger, SharedLogger, TermLogger, TerminalMode};
use vulkano::{
buffer::{BufferUsage, CpuAccessibleBuffer, CpuBufferPool, TypedBufferAccess},
command_buffer::{AutoCommandBufferBuilder, CommandBufferUsage, SubpassContents},
buffer::{BufferUsage, CpuAccessibleBuffer, TypedBufferAccess},
command_buffer::{
AutoCommandBufferBuilder, CommandBufferUsage, PrimaryAutoCommandBuffer, SubpassContents,
},
descriptor_set::{PersistentDescriptorSet, WriteDescriptorSet},
pipeline::{
graphics::{
input_assembly::InputAssemblyState, vertex_input::BuffersDefinition,
viewport::ViewportState,
viewport::ViewportState, depth_stencil::DepthStencilState,
},
GraphicsPipeline, Pipeline, PipelineBindPoint,
},
render_pass::Subpass,
swapchain::{self, AcquireError},
sync::GpuFuture,
};
use winit::{
event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::WindowBuilder,
};
@ -29,10 +32,57 @@ pub mod render;
use crate::render::{
context::VulkanContext,
data::{ModelViewProjection, Vertex},
shaders,
data::{ModelViewProjection, Vertex, CUBE_VERTEX_DATA},
shaders, FrameTimer, FrameTracer,
};
fn create_command_buffers(
vk: &VulkanContext,
pipeline: Arc<GraphicsPipeline>,
uniform_buffers: &[Arc<CpuAccessibleBuffer<shaders::vs::ty::MVP_Data>>],
vertex_buffer: Arc<CpuAccessibleBuffer<[Vertex]>>,
) -> Vec<Arc<PrimaryAutoCommandBuffer>> {
vk.framebuffers()
.iter()
.enumerate()
.map(|(i, framebuffer)| {
let clear_values = vec![[0.0, 0.68, 1.0, 1.0].into(), 1f32.into()];
let layout = pipeline.layout().set_layouts().get(0).unwrap();
let set = PersistentDescriptorSet::new(
layout.clone(),
[WriteDescriptorSet::buffer(0, uniform_buffers[i].clone())],
)
.unwrap();
let mut cmd_buffer_builder = AutoCommandBufferBuilder::primary(
vk.device().clone(),
vk.queue_family(),
CommandBufferUsage::MultipleSubmit,
)
.unwrap();
cmd_buffer_builder
.begin_render_pass(framebuffer.clone(), SubpassContents::Inline, clear_values)
.unwrap()
.set_viewport(0, [vk.viewport().clone()])
.bind_pipeline_graphics(pipeline.clone())
.bind_descriptor_sets(
PipelineBindPoint::Graphics,
pipeline.layout().clone(),
0,
set,
)
.bind_vertex_buffers(0, vertex_buffer.clone())
.draw(vertex_buffer.len() as u32, 1, 0, 0)
.unwrap()
.end_render_pass()
.unwrap();
Arc::new(cmd_buffer_builder.build().unwrap())
})
.collect()
}
fn main() {
let loggers: Vec<Box<dyn SharedLogger>> = vec![TermLogger::new(
LevelFilter::Debug,
@ -51,7 +101,7 @@ fn main() {
};
let event_loop = EventLoop::new();
let vk = VulkanContext::new_windowed(
let mut vk = VulkanContext::new_windowed(
&event_loop,
WindowBuilder::new()
.with_title("yray engine")
@ -64,15 +114,13 @@ fn main() {
let vs = shaders::vs::load(vk.device().clone()).unwrap();
let fs = shaders::fs::load(vk.device().clone()).unwrap();
let uniform_buffer: CpuBufferPool<shaders::vs::ty::MVP_Data> =
CpuBufferPool::uniform_buffer(vk.device().clone());
let pipeline = GraphicsPipeline::start()
.vertex_input_state(BuffersDefinition::new().vertex::<Vertex>())
.vertex_shader(vs.entry_point("main").unwrap(), ())
.input_assembly_state(InputAssemblyState::new())
.viewport_state(ViewportState::viewport_dynamic_scissor_irrelevant())
.fragment_shader(fs.entry_point("main").unwrap(), ())
.depth_stencil_state(DepthStencilState::simple_depth_test())
.render_pass(Subpass::from(vk.render_pass().clone(), 0).unwrap())
.build(vk.device().clone())
.unwrap();
@ -81,134 +129,123 @@ fn main() {
vk.device().clone(),
BufferUsage::vertex_buffer(),
false,
[
// Front
Vertex {
position: [-0.5, -0.5, -0.5],
},
Vertex {
position: [ 0.5, -0.5, -0.5],
},
Vertex {
position: [ 0.5, 0.5, -0.5],
},
Vertex {
position: [ 0.5, 0.5, -0.5],
},
Vertex {
position: [-0.5, 0.5, -0.5],
},
Vertex {
position: [-0.5, -0.5, -0.5],
},
// Right
Vertex {
position: [ 0.5, -0.5, -0.5],
},
Vertex {
position: [ 0.5, -0.5, 0.5],
},
Vertex {
position: [ 0.5, 0.5, 0.5],
},
Vertex {
position: [ 0.5, 0.5, 0.5],
},
Vertex {
position: [ 0.5, 0.5, -0.5],
},
Vertex {
position: [ 0.5, -0.5, -0.5],
},
]
.iter()
.cloned(),
CUBE_VERTEX_DATA.iter().cloned(),
)
.unwrap();
let rotation_start = Instant::now();
let viewport = vk.viewport();
mvp.projection = glm::perspective(
viewport.dimensions[0] / viewport.dimensions[1],
45.0,
0.01,
100.0,
let dim = vk.inner_size();
mvp.projection = glm::perspective(dim.0 / dim.1, 45.0, 0.01, 100.0);
let uniform_buffers: Vec<_> = (0..vk.swapchain().image_count())
.map(|_| {
CpuAccessibleBuffer::from_data(
vk.device().clone(),
BufferUsage::uniform_buffer_transfer_destination(),
false,
shaders::vs::ty::MVP_Data::from(mvp),
)
.unwrap()
})
.collect();
let mut command_buffers = create_command_buffers(
&vk,
pipeline.clone(),
&uniform_buffers,
vertex_buffer.clone(),
);
log::info!("Entering event loop");
vk.run(
event_loop,
move |event, ctl| match event {
RenderEvent::CloseRequested => {
*ctl = ControlFlow::Exit;
}
RenderEvent::ViewportResized(size) => {
log::debug!("Window resized, new size: {}x{}", size.0, size.1);
let mut recreate_swapchain = false;
let mut frame_timer = FrameTimer::new();
let mut frame_tracer = FrameTracer::new();
event_loop.run(move |event, _, ctl| match event {
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => {
*ctl = ControlFlow::Exit;
}
Event::WindowEvent {
event: WindowEvent::Resized(_),
..
} => {
recreate_swapchain = true;
}
Event::RedrawEventsCleared => {
frame_tracer.reset();
frame_tracer.trace("Frame begin");
if recreate_swapchain {
let size = vk.recreate_swapchain().unwrap();
mvp.projection = glm::perspective(size.0 / size.1, 45.0, 0.01, 100.0);
}
},
move |vk, future, framebuffer| {
let clear_values = vec![[0.0, 0.68, 1.0, 1.0].into()];
recreate_swapchain = false;
let uniform_buffer_subbuffer = {
let elapsed = rotation_start.elapsed().as_secs() as f64
+ rotation_start.elapsed().subsec_nanos() as f64 / 1_000_000_000.0;
mvp.view = glm::look_at(
&glm::vec3(elapsed.cos() as f32 * 5.0, 5.0, elapsed.sin() as f32 * 5.0),
&glm::vec3(0.0, 0.0, 0.0),
&glm::vec3(0.0, 1.0, 0.0),
command_buffers = create_command_buffers(
&vk,
pipeline.clone(),
&uniform_buffers,
vertex_buffer.clone(),
);
frame_tracer.trace("Recreate swapchain/command buffers");
}
let uniform_data = shaders::vs::ty::MVP_Data {
model: mvp.model.into(),
view: mvp.view.into(),
projection: mvp.projection.into(),
let (image_index, suboptimal, acquire_future) =
match swapchain::acquire_next_image(vk.swapchain().clone(), None) {
Ok(r) => r,
Err(AcquireError::OutOfDate) => {
recreate_swapchain = true;
return;
}
Err(e) => panic!("Failed to acquire next image: {:?}", e),
};
uniform_buffer.next(uniform_data).unwrap()
};
if suboptimal {
recreate_swapchain = true;
}
let layout = pipeline.layout().set_layouts().get(0).unwrap();
let set = PersistentDescriptorSet::new(
layout.clone(),
[WriteDescriptorSet::buffer(0, uniform_buffer_subbuffer)],
)
.unwrap();
frame_tracer.trace("Acquire next image");
let mut cmd_buffer_builder = AutoCommandBufferBuilder::primary(
vk.device().clone(),
vk.queue_family(),
CommandBufferUsage::OneTimeSubmit,
)
.unwrap();
cmd_buffer_builder
.begin_render_pass(framebuffer, SubpassContents::Inline, clear_values)
.unwrap()
.set_viewport(0, [vk.viewport().clone()])
.bind_pipeline_graphics(pipeline.clone())
.bind_descriptor_sets(
PipelineBindPoint::Graphics,
pipeline.layout().clone(),
0,
set,
)
.bind_vertex_buffers(0, vertex_buffer.clone())
.draw(vertex_buffer.len() as u32, 1, 0, 0)
.unwrap()
.end_render_pass()
.unwrap();
let command_buffer = cmd_buffer_builder.build().unwrap();
let elapsed = rotation_start.elapsed().as_secs() as f64
+ rotation_start.elapsed().subsec_nanos() as f64 / 1_000_000_000.0;
let res = future
mvp.view = glm::look_at(
&glm::vec3(elapsed.cos() as f32 * 5.0, 5.0, elapsed.sin() as f32 * 5.0),
&glm::vec3(0.0, 0.0, 0.0),
&glm::vec3(0.0, 1.0, 0.0),
);
{
let buffer = uniform_buffers[image_index].clone();
let mut data = buffer.write().unwrap();
*data = mvp.into();
}
frame_tracer.trace("Submit new uniforms");
let command_buffer = command_buffers[image_index].clone();
let future = acquire_future
.then_execute(vk.queue().clone(), command_buffer)
.unwrap()
.then_swapchain_present(vk.queue().clone(), vk.swapchain().clone(), image_index)
.then_signal_fence_and_flush()
.unwrap();
vk.frame_tracer().trace("Render pass 1 command send");
future.wait(None).unwrap();
res
},
);
frame_tracer.trace("Submit commands and wait");
frame_timer.frame_done();
if frame_timer.update() {
log::debug!("{} frames/s", frame_timer.fps());
log::debug!("{:?}", frame_tracer.timings());
}
}
_ => (),
});
}

View File

@ -5,29 +5,22 @@ use vulkano::{
physical::{PhysicalDevice, PhysicalDeviceType, QueueFamily},
Device, DeviceCreateInfo, DeviceExtensions, Queue, QueueCreateInfo,
},
image::{view::ImageView, SwapchainImage},
format::Format,
image::{view::ImageView, AttachmentImage, ImageAccess, SwapchainImage},
instance::{Instance, InstanceCreateInfo},
pipeline::graphics::viewport::Viewport,
render_pass::{Framebuffer, FramebufferCreateInfo, RenderPass},
swapchain::{
self, AcquireError, Surface, Swapchain, SwapchainAcquireFuture, SwapchainCreateInfo,
SwapchainCreationError,
},
sync::{self, FlushError},
sync::{GpuFuture, JoinFuture},
swapchain::{Surface, Swapchain, SwapchainCreateInfo, SwapchainCreationError},
Version,
};
use vulkano_win::VkSurfaceBuild;
use winit::{
event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoop},
event_loop::EventLoop,
window::{Window, WindowBuilder},
};
use crate::error::EngineError;
use super::{event::RenderEvent, FrameTimer, FrameTracer};
pub struct VulkanContext {
#[allow(dead_code)]
instance: Arc<Instance>,
@ -41,8 +34,6 @@ pub struct VulkanContext {
surface: Arc<Surface<Window>>,
viewport: Viewport,
frame_tracer: FrameTracer,
}
impl VulkanContext {
@ -53,7 +44,7 @@ impl VulkanContext {
let required_extensions = vulkano_win::required_extensions();
let device_extensions = DeviceExtensions {
khr_swapchain: true,
khr_maintenance1: true, // Required for flipping Y-axis on viewport
khr_maintenance1: true, // Required for flipping Y-axis on viewport
..DeviceExtensions::none()
};
@ -110,16 +101,23 @@ impl VulkanContext {
store: Store,
format: swapchain.image_format(),
samples: 1,
},
depth: {
load: Clear,
store: DontCare,
format: Format::D16_UNORM,
samples: 1,
}
},
pass: {
color: [color],
depth_stencil: {}
depth_stencil: {depth}
}
)
.unwrap();
let framebuffers = Self::create_framebuffers(&swapchain_images, render_pass.clone())?;
let framebuffers =
Self::create_framebuffers(device.clone(), &swapchain_images, render_pass.clone())?;
let viewport = Viewport {
origin: [0.0, dim.height as f32],
@ -137,120 +135,10 @@ impl VulkanContext {
render_pass,
framebuffers,
viewport,
frame_tracer: FrameTracer::new()
})
}
pub fn frame_tracer(&mut self) -> &mut FrameTracer {
&mut self.frame_tracer
}
pub fn run<
T: GpuFuture + 'static,
EventHandler: 'static + FnMut(RenderEvent, &mut ControlFlow),
RenderHandler: 'static
+ FnMut(
&mut VulkanContext,
JoinFuture<Box<dyn GpuFuture>, SwapchainAcquireFuture<Window>>,
Arc<Framebuffer>,
) -> T,
>(
mut self,
event_loop: EventLoop<()>,
mut event_handler: EventHandler,
mut render_handler: RenderHandler,
) -> ! {
let mut recreate_swapchain = false;
let mut previous_frame_end =
Some(Box::new(sync::now(self.device().clone())) as Box<dyn GpuFuture>);
let mut frame_timer = FrameTimer::new();
event_loop.run(move |event, _, ctl| {
match event {
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => {
event_handler(RenderEvent::CloseRequested, ctl);
}
Event::WindowEvent {
event: WindowEvent::Resized(_),
..
} => {
recreate_swapchain = true;
}
Event::RedrawEventsCleared => {
self.frame_tracer.reset();
previous_frame_end
.as_mut()
.take()
.unwrap()
.cleanup_finished();
self.frame_tracer.trace("Previous frame cleanup");
if recreate_swapchain {
let size = self.recreate_swapchain().unwrap();
event_handler(RenderEvent::ViewportResized(size), ctl);
recreate_swapchain = false;
}
let (image_num, suboptimal, acquire_future) =
match swapchain::acquire_next_image(self.swapchain.clone(), None) {
Ok(r) => r,
Err(AcquireError::OutOfDate) => {
recreate_swapchain = true;
return;
}
Err(e) => panic!("Failed to acquire next image: {:?}", e),
};
if suboptimal {
recreate_swapchain = true;
}
self.frame_tracer.trace("Acquire next frame");
let future = previous_frame_end.take().unwrap().join(acquire_future);
let framebuffer = self.framebuffers[image_num].clone();
let future = render_handler(&mut self, future, framebuffer)
.then_swapchain_present(
self.queue.clone(),
self.swapchain.clone(),
image_num,
)
.then_signal_semaphore_and_flush();
match future {
Ok(future) => {
previous_frame_end = Some(Box::new(future) as Box<_>);
}
Err(FlushError::OutOfDate) => {
recreate_swapchain = true;
previous_frame_end =
Some(Box::new(sync::now(self.device.clone())) as Box<_>);
}
Err(e) => {
log::error!("Failed to flush future: {:?}", e);
previous_frame_end =
Some(Box::new(sync::now(self.device.clone())) as Box<_>);
}
}
self.frame_tracer.trace("Frame finish");
frame_timer.frame_done();
if frame_timer.update() {
log::debug!("{} frames/s", frame_timer.fps());
log::debug!("{:?}", self.frame_tracer.timings());
}
}
_ => (),
};
});
}
fn recreate_swapchain(&mut self) -> Result<(f32, f32), EngineError> {
pub fn recreate_swapchain(&mut self) -> Result<(f32, f32), EngineError> {
let dim = self.surface.window().inner_size();
self.viewport.origin[1] = dim.height as f32;
@ -269,12 +157,17 @@ impl VulkanContext {
};
self.swapchain = new_swapchain;
self.framebuffers = Self::create_framebuffers(&new_images, self.render_pass.clone())?;
self.framebuffers =
Self::create_framebuffers(self.device.clone(), &new_images, self.render_pass.clone())?;
self.swapchain_images = new_images;
Ok(dim.into())
}
pub fn inner_size(&self) -> (f32, f32) {
self.surface.window().inner_size().into()
}
pub fn device(&self) -> &Arc<Device> {
&self.device
}
@ -327,14 +220,22 @@ impl VulkanContext {
}
fn create_single_framebuffer(
device: Arc<Device>,
image: Arc<SwapchainImage<Window>>,
render_pass: Arc<RenderPass>,
) -> Result<Arc<Framebuffer>, EngineError> {
let dimensions = image.dimensions().width_height();
let view = ImageView::new_default(image)?;
let depth_buffer = ImageView::new_default(AttachmentImage::transient(
device,
dimensions,
Format::D16_UNORM,
)?)?;
let res = Framebuffer::new(
render_pass,
FramebufferCreateInfo {
attachments: vec![view],
attachments: vec![view, depth_buffer],
..Default::default()
},
)?;
@ -342,12 +243,15 @@ impl VulkanContext {
}
fn create_framebuffers(
device: Arc<Device>,
images: &[Arc<SwapchainImage<Window>>],
render_pass: Arc<RenderPass>,
) -> Result<Vec<Arc<Framebuffer>>, EngineError> {
images
.iter()
.map(|image| Self::create_single_framebuffer(image.clone(), render_pass.clone()))
.map(|image| {
Self::create_single_framebuffer(device.clone(), image.clone(), render_pass.clone())
})
.collect()
}
}

View File

@ -1,6 +1,8 @@
use bytemuck_derive::{Pod, Zeroable};
use glm::TMat4;
use super::shaders;
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, Zeroable, Pod)]
pub struct Vertex {
@ -15,3 +17,130 @@ pub struct ModelViewProjection {
pub view: TMat4<f32>,
pub projection: TMat4<f32>,
}
pub const CUBE_VERTEX_DATA: &[Vertex] = &[
// Front
Vertex {
position: [-0.5, -0.5, -0.5],
},
Vertex {
position: [0.5, -0.5, -0.5],
},
Vertex {
position: [0.5, 0.5, -0.5],
},
Vertex {
position: [0.5, 0.5, -0.5],
},
Vertex {
position: [-0.5, 0.5, -0.5],
},
Vertex {
position: [-0.5, -0.5, -0.5],
},
// Right
Vertex {
position: [0.5, -0.5, -0.5],
},
Vertex {
position: [0.5, -0.5, 0.5],
},
Vertex {
position: [0.5, 0.5, 0.5],
},
Vertex {
position: [0.5, 0.5, 0.5],
},
Vertex {
position: [0.5, 0.5, -0.5],
},
Vertex {
position: [0.5, -0.5, -0.5],
},
// Back
Vertex {
position: [-0.5, -0.5, 0.5],
},
Vertex {
position: [0.5, -0.5, 0.5],
},
Vertex {
position: [0.5, 0.5, 0.5],
},
Vertex {
position: [0.5, 0.5, 0.5],
},
Vertex {
position: [-0.5, 0.5, 0.5],
},
Vertex {
position: [-0.5, -0.5, 0.5],
},
// Left
Vertex {
position: [-0.5, -0.5, -0.5],
},
Vertex {
position: [-0.5, -0.5, 0.5],
},
Vertex {
position: [-0.5, 0.5, 0.5],
},
Vertex {
position: [-0.5, 0.5, 0.5],
},
Vertex {
position: [-0.5, 0.5, -0.5],
},
Vertex {
position: [-0.5, -0.5, -0.5],
},
// Top
Vertex {
position: [-0.5, 0.5, -0.5],
},
Vertex {
position: [0.5, 0.5, -0.5],
},
Vertex {
position: [0.5, 0.5, 0.5],
},
Vertex {
position: [0.5, 0.5, 0.5],
},
Vertex {
position: [-0.5, 0.5, 0.5],
},
Vertex {
position: [-0.5, 0.5, -0.5],
},
// Bottom
Vertex {
position: [-0.5, -0.5, -0.5],
},
Vertex {
position: [0.5, -0.5, -0.5],
},
Vertex {
position: [0.5, -0.5, 0.5],
},
Vertex {
position: [0.5, -0.5, 0.5],
},
Vertex {
position: [-0.5, -0.5, 0.5],
},
Vertex {
position: [-0.5, -0.5, -0.5],
},
];
impl From<ModelViewProjection> for shaders::vs::ty::MVP_Data {
fn from(mvp: ModelViewProjection) -> Self {
Self {
model: mvp.model.into(),
view: mvp.view.into(),
projection: mvp.projection.into()
}
}
}

View File

@ -1,5 +0,0 @@
pub enum RenderEvent {
ViewportResized((f32, f32)),
CloseRequested,
}

View File

@ -1,18 +1,16 @@
use std::time::{Instant, Duration};
use std::time::{Duration, Instant};
pub mod context;
pub mod shaders;
pub mod event;
pub mod data;
pub mod shaders;
pub struct FrameTimer {
last_time: Instant,
frames_per_second: u64,
counter: u64
counter: u64,
}
pub struct FrameTracer {
frame_start: Option<Instant>,
trace_points: Vec<(String, Instant)>,
}
@ -21,7 +19,7 @@ impl FrameTimer {
Self {
last_time: Instant::now(),
frames_per_second: 0,
counter: 0
counter: 0,
}
}
@ -49,16 +47,20 @@ impl FrameTimer {
}
}
impl Default for FrameTimer {
fn default() -> Self {
Self::new()
}
}
impl FrameTracer {
pub const fn new() -> Self {
Self {
frame_start: None,
trace_points: vec![]
trace_points: vec![],
}
}
pub fn reset(&mut self) {
self.frame_start = Some(Instant::now());
self.trace_points.clear();
}
@ -67,10 +69,9 @@ impl FrameTracer {
}
pub fn timings(&self) -> Vec<(String, Duration)> {
if let Some(start) = self.frame_start {
self.trace_points.iter().map(|point| (point.0.clone(), point.1 - start)).collect()
} else {
vec![]
}
self.trace_points
.windows(2)
.map(|pair| (pair[1].0.to_owned(), pair[1].1 - pair[0].1))
.collect()
}
}

View File

@ -14,9 +14,13 @@ layout(set = 0, binding = 0) uniform MVP_Data {
mat4 projection;
} uniforms;
layout(location = 0) out vec3 m_position;
void main() {
mat4 worldview = uniforms.view * uniforms.model;
gl_Position = uniforms.projection * worldview * vec4(position, 1.0);
m_position = position;
}",
types_meta: {
use bytemuck::{Pod, Zeroable};
@ -32,10 +36,12 @@ pub mod fs {
src: "
#version 450
layout(location = 0) in vec3 m_position;
layout(location = 0) out vec4 f_color;
void main() {
f_color = vec4(1.0, 0.0, 0.0, 1.0);
f_color = vec4(m_position, 1.0);
}"
}
}