Basic mesh rendering
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
Generated
+2930
File diff suppressed because it is too large
Load Diff
+15
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "gaym"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bytemuck = { version = "1.22.0", features = ["derive"] }
|
||||||
|
env_logger = "0.11.8"
|
||||||
|
glam = { version = "0.30.2", features = ["bytemuck"] }
|
||||||
|
log = "0.4.27"
|
||||||
|
thiserror = "2.0.12"
|
||||||
|
vulkano = "0.35.1"
|
||||||
|
vulkano-shaders = "0.35.0"
|
||||||
|
vulkano-win = "0.34.0"
|
||||||
|
winit = "0.30.9"
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
use vulkano::{
|
||||||
|
buffer::AllocateBufferError, command_buffer::CommandBufferExecError, image::AllocateImageError,
|
||||||
|
memory::allocator::MemoryAllocatorError, pipeline::layout::IntoPipelineLayoutCreateInfoError,
|
||||||
|
swapchain::FromWindowError, sync::HostAccessError, LoadingError, Validated, ValidationError,
|
||||||
|
VulkanError,
|
||||||
|
};
|
||||||
|
use winit::raw_window_handle::HandleError;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("Event loop error: {0}")]
|
||||||
|
WinitEventLoop(#[from] winit::error::EventLoopError),
|
||||||
|
#[error("Window creation error: {0}")]
|
||||||
|
WinitOs(#[from] winit::error::OsError),
|
||||||
|
|
||||||
|
#[error("Vulkan loading error: {0}")]
|
||||||
|
Loading(#[from] LoadingError),
|
||||||
|
#[error("Window handle error: {0}")]
|
||||||
|
HandleError(#[from] HandleError),
|
||||||
|
#[error("Vulkan error: {0}")]
|
||||||
|
Vulkan(#[from] VulkanError),
|
||||||
|
#[error("Buffer allocation error: {0}")]
|
||||||
|
BufferAllocation(#[from] AllocateBufferError),
|
||||||
|
#[error("Command buffer execution error: {0}")]
|
||||||
|
CommandBufferExec(#[from] CommandBufferExecError),
|
||||||
|
#[error("Cannot select a physical device")]
|
||||||
|
NoValidDevice,
|
||||||
|
#[error("Vulkan surface creation error: {0}")]
|
||||||
|
FromWindow(#[from] FromWindowError),
|
||||||
|
#[error("Pipeline layout create error: {0}")]
|
||||||
|
IntoPipelineLayoutCreateInfo(#[from] IntoPipelineLayoutCreateInfoError),
|
||||||
|
#[error("Host buffer access error: {0}")]
|
||||||
|
HostAccess(#[from] HostAccessError),
|
||||||
|
#[error("Image allocation error: {0}")]
|
||||||
|
AllocateImage(#[from] AllocateImageError),
|
||||||
|
#[error("GPU memory allocation error: {0}")]
|
||||||
|
MemoryAllocator(#[from] MemoryAllocatorError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Box<ValidationError>> for Error {
|
||||||
|
fn from(value: Box<ValidationError>) -> Self {
|
||||||
|
log::error!("Validation error: {value}");
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<Validated<T>> for Error
|
||||||
|
where
|
||||||
|
Error: From<T>,
|
||||||
|
{
|
||||||
|
fn from(value: Validated<T>) -> Self {
|
||||||
|
Error::from(value.unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
+142
@@ -0,0 +1,142 @@
|
|||||||
|
use std::{process::ExitCode, sync::Arc};
|
||||||
|
|
||||||
|
use error::Error;
|
||||||
|
use render::Renderer;
|
||||||
|
use winit::{
|
||||||
|
application::ApplicationHandler,
|
||||||
|
dpi::PhysicalSize,
|
||||||
|
event::{DeviceEvent, DeviceId, StartCause, WindowEvent},
|
||||||
|
event_loop::{ActiveEventLoop, EventLoop},
|
||||||
|
window::{Window, WindowId},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod error;
|
||||||
|
pub mod render;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Engine {
|
||||||
|
window: Option<Arc<Window>>,
|
||||||
|
renderer: Option<Renderer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Engine {
|
||||||
|
pub fn ensure_renderer(&mut self, event_loop: &ActiveEventLoop) -> Result<(), Error> {
|
||||||
|
if self.window.is_none() {
|
||||||
|
let window_attributes = Window::default_attributes()
|
||||||
|
.with_resizable(false)
|
||||||
|
.with_inner_size(PhysicalSize::new(800, 600));
|
||||||
|
|
||||||
|
let window = Arc::new(event_loop.create_window(window_attributes)?);
|
||||||
|
let render = Renderer::from_window(window.clone())?;
|
||||||
|
|
||||||
|
self.window = Some(window);
|
||||||
|
self.renderer = Some(render);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApplicationHandler for Engine {
|
||||||
|
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
|
if let Err(error) = self.ensure_renderer(event_loop) {
|
||||||
|
log::error!("Renderer setup error: {error}");
|
||||||
|
event_loop.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exiting(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
|
let _ = event_loop;
|
||||||
|
log::info!("Exiting");
|
||||||
|
self.renderer = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn suspended(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
|
let _ = event_loop;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) {
|
||||||
|
let _ = event_loop;
|
||||||
|
let _ = cause;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: ()) {
|
||||||
|
let _ = event_loop;
|
||||||
|
let _ = event;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_event(
|
||||||
|
&mut self,
|
||||||
|
event_loop: &ActiveEventLoop,
|
||||||
|
window_id: WindowId,
|
||||||
|
event: WindowEvent,
|
||||||
|
) {
|
||||||
|
let _ = window_id;
|
||||||
|
|
||||||
|
match event {
|
||||||
|
WindowEvent::CloseRequested => {
|
||||||
|
log::info!("Close requested");
|
||||||
|
event_loop.exit();
|
||||||
|
}
|
||||||
|
WindowEvent::Resized(size) => {
|
||||||
|
log::info!("Window resized: {size:?}");
|
||||||
|
if let Some(render) = self.renderer.as_mut() {
|
||||||
|
render.set_swapchain_dirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WindowEvent::RedrawRequested => {
|
||||||
|
if let Some(render) = self.renderer.as_mut() {
|
||||||
|
if let Err(error) = render.render() {
|
||||||
|
log::error!("Render error: {error}");
|
||||||
|
event_loop.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn device_event(
|
||||||
|
&mut self,
|
||||||
|
event_loop: &ActiveEventLoop,
|
||||||
|
device_id: DeviceId,
|
||||||
|
event: DeviceEvent,
|
||||||
|
) {
|
||||||
|
let _ = event_loop;
|
||||||
|
let _ = device_id;
|
||||||
|
let _ = event;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
|
let _ = event_loop;
|
||||||
|
|
||||||
|
if let Some(window) = self.window.as_ref() {
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn memory_warning(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
|
let _ = event_loop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run() -> Result<(), Error> {
|
||||||
|
let event_loop = EventLoop::new()?;
|
||||||
|
let mut engine = Engine::default();
|
||||||
|
|
||||||
|
event_loop.run_app(&mut engine)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> ExitCode {
|
||||||
|
env_logger::init();
|
||||||
|
match run() {
|
||||||
|
Ok(()) => ExitCode::SUCCESS,
|
||||||
|
Err(error) => {
|
||||||
|
log::error!("Exited with error: {error}");
|
||||||
|
ExitCode::FAILURE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use glam::Vec3;
|
||||||
|
use vulkano::{
|
||||||
|
buffer::{Buffer, BufferCreateInfo, BufferUsage, Subbuffer},
|
||||||
|
memory::allocator::{AllocationCreateInfo, MemoryTypeFilter},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::error::Error;
|
||||||
|
|
||||||
|
use super::{util::Allocators, vertex::InputVertex};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct MeshBuilder {
|
||||||
|
vertices: Vec<InputVertex>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MeshBuilder {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
vertices: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_cube(&mut self, x: i32, y: i32, z: i32) {
|
||||||
|
let position = Vec3::new(x as f32, y as f32, z as f32);
|
||||||
|
// Front
|
||||||
|
self.add_face(
|
||||||
|
position,
|
||||||
|
Vec3::new(0.0, 0.0, 0.0),
|
||||||
|
Vec3::new(1.0, 0.0, 0.0),
|
||||||
|
Vec3::new(1.0, 1.0, 0.0),
|
||||||
|
Vec3::new(0.0, 1.0, 0.0),
|
||||||
|
Vec3::new(1.0, 0.0, 0.0),
|
||||||
|
);
|
||||||
|
// Left
|
||||||
|
self.add_face(
|
||||||
|
position,
|
||||||
|
Vec3::new(0.0, 0.0, 0.0),
|
||||||
|
Vec3::new(0.0, 1.0, 0.0),
|
||||||
|
Vec3::new(0.0, 1.0, 1.0),
|
||||||
|
Vec3::new(0.0, 0.0, 1.0),
|
||||||
|
Vec3::new(0.0, 1.0, 0.0),
|
||||||
|
);
|
||||||
|
// Back
|
||||||
|
self.add_face(
|
||||||
|
position,
|
||||||
|
Vec3::new(0.0, 0.0, 1.0),
|
||||||
|
Vec3::new(1.0, 0.0, 1.0),
|
||||||
|
Vec3::new(1.0, 1.0, 1.0),
|
||||||
|
Vec3::new(0.0, 1.0, 1.0),
|
||||||
|
Vec3::new(0.0, 0.0, 1.0),
|
||||||
|
);
|
||||||
|
// Right
|
||||||
|
self.add_face(
|
||||||
|
position,
|
||||||
|
Vec3::new(1.0, 0.0, 0.0),
|
||||||
|
Vec3::new(1.0, 1.0, 0.0),
|
||||||
|
Vec3::new(1.0, 1.0, 1.0),
|
||||||
|
Vec3::new(1.0, 0.0, 1.0),
|
||||||
|
Vec3::new(1.0, 1.0, 0.0),
|
||||||
|
);
|
||||||
|
// Bottom
|
||||||
|
self.add_face(
|
||||||
|
position,
|
||||||
|
Vec3::new(0.0, 0.0, 0.0),
|
||||||
|
Vec3::new(1.0, 0.0, 0.0),
|
||||||
|
Vec3::new(1.0, 0.0, 1.0),
|
||||||
|
Vec3::new(0.0, 0.0, 1.0),
|
||||||
|
Vec3::new(1.0, 0.0, 1.0),
|
||||||
|
);
|
||||||
|
// Top
|
||||||
|
self.add_face(
|
||||||
|
position,
|
||||||
|
Vec3::new(0.0, 1.0, 0.0),
|
||||||
|
Vec3::new(1.0, 1.0, 0.0),
|
||||||
|
Vec3::new(1.0, 1.0, 1.0),
|
||||||
|
Vec3::new(0.0, 1.0, 1.0),
|
||||||
|
Vec3::new(0.0, 1.0, 1.0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_face(&mut self, d: Vec3, p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, c: Vec3) {
|
||||||
|
self.vertices.push(InputVertex {
|
||||||
|
i_position: p0 + d,
|
||||||
|
i_color: c,
|
||||||
|
});
|
||||||
|
self.vertices.push(InputVertex {
|
||||||
|
i_position: p1 + d,
|
||||||
|
i_color: c,
|
||||||
|
});
|
||||||
|
self.vertices.push(InputVertex {
|
||||||
|
i_position: p2 + d,
|
||||||
|
i_color: c,
|
||||||
|
});
|
||||||
|
self.vertices.push(InputVertex {
|
||||||
|
i_position: p2 + d,
|
||||||
|
i_color: c,
|
||||||
|
});
|
||||||
|
self.vertices.push(InputVertex {
|
||||||
|
i_position: p3 + d,
|
||||||
|
i_color: c,
|
||||||
|
});
|
||||||
|
self.vertices.push(InputVertex {
|
||||||
|
i_position: p0 + d,
|
||||||
|
i_color: c,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn upload(self, allocators: &Arc<Allocators>) -> Result<Subbuffer<[InputVertex]>, Error> {
|
||||||
|
let buffer = Buffer::from_iter(
|
||||||
|
allocators.memory.clone(),
|
||||||
|
BufferCreateInfo {
|
||||||
|
usage: BufferUsage::VERTEX_BUFFER,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
AllocationCreateInfo {
|
||||||
|
memory_type_filter: MemoryTypeFilter::HOST_SEQUENTIAL_WRITE
|
||||||
|
| MemoryTypeFilter::PREFER_DEVICE,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
self.vertices.into_iter(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
use std::{sync::Arc, time::Instant};
|
||||||
|
|
||||||
|
use glam::{Mat4, Vec3};
|
||||||
|
use mesh::MeshBuilder;
|
||||||
|
use util::{Allocators, ViewportExt};
|
||||||
|
use vertex::InputVertex;
|
||||||
|
use vulkano::{
|
||||||
|
buffer::{
|
||||||
|
allocator::{SubbufferAllocator, SubbufferAllocatorCreateInfo},
|
||||||
|
BufferUsage, Subbuffer,
|
||||||
|
},
|
||||||
|
command_buffer::{
|
||||||
|
AutoCommandBufferBuilder, CommandBufferUsage, RenderPassBeginInfo, SubpassBeginInfo,
|
||||||
|
SubpassContents, SubpassEndInfo,
|
||||||
|
},
|
||||||
|
descriptor_set::{DescriptorSet, WriteDescriptorSet},
|
||||||
|
device::{Device, DeviceExtensions, Queue},
|
||||||
|
format::Format,
|
||||||
|
image::Image,
|
||||||
|
instance::{InstanceCreateFlags, InstanceCreateInfo},
|
||||||
|
memory::allocator::MemoryTypeFilter,
|
||||||
|
pipeline::{graphics::viewport::Viewport, GraphicsPipeline, Pipeline, PipelineBindPoint},
|
||||||
|
render_pass::{Framebuffer, RenderPass},
|
||||||
|
shader::EntryPoint,
|
||||||
|
swapchain::{self, Surface, Swapchain, SwapchainCreateInfo, SwapchainPresentInfo},
|
||||||
|
sync::{self, GpuFuture},
|
||||||
|
Validated, VulkanError,
|
||||||
|
};
|
||||||
|
use winit::window::Window;
|
||||||
|
|
||||||
|
use crate::error::Error;
|
||||||
|
|
||||||
|
pub mod mesh;
|
||||||
|
pub mod shaders;
|
||||||
|
pub mod util;
|
||||||
|
pub mod vertex;
|
||||||
|
|
||||||
|
pub const DEPTH_FORMAT: Format = Format::D32_SFLOAT;
|
||||||
|
|
||||||
|
pub struct Renderer {
|
||||||
|
window: Arc<Window>,
|
||||||
|
device: Arc<Device>,
|
||||||
|
queue: Arc<Queue>,
|
||||||
|
|
||||||
|
allocators: Arc<Allocators>,
|
||||||
|
|
||||||
|
swapchain: Arc<Swapchain>,
|
||||||
|
swapchain_images: Vec<Arc<Image>>,
|
||||||
|
swapchain_dirty: bool,
|
||||||
|
|
||||||
|
vertex_buffer: Subbuffer<[InputVertex]>,
|
||||||
|
uniform_allocator: SubbufferAllocator,
|
||||||
|
|
||||||
|
render_pass: Arc<RenderPass>,
|
||||||
|
viewport: Viewport,
|
||||||
|
framebuffers: Vec<Arc<Framebuffer>>,
|
||||||
|
|
||||||
|
vertex_shader: EntryPoint,
|
||||||
|
fragment_shader: EntryPoint,
|
||||||
|
pipeline: Arc<GraphicsPipeline>,
|
||||||
|
|
||||||
|
start: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Renderer {
|
||||||
|
pub fn from_window(window: Arc<Window>) -> Result<Self, Error> {
|
||||||
|
let instance_extensions = Surface::required_extensions(&window)?;
|
||||||
|
let device_extensions = DeviceExtensions {
|
||||||
|
khr_swapchain: true,
|
||||||
|
khr_maintenance1: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let instance = util::create_instance(InstanceCreateInfo {
|
||||||
|
flags: InstanceCreateFlags::ENUMERATE_PORTABILITY,
|
||||||
|
enabled_extensions: instance_extensions,
|
||||||
|
..Default::default()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let surface = Surface::from_window(instance.clone(), window.clone())?;
|
||||||
|
|
||||||
|
let (device, queue) = util::create_device(&instance, device_extensions, &surface)?;
|
||||||
|
let dimensions = window.inner_size();
|
||||||
|
let (swapchain, swapchain_images) = util::create_swapchain(&device, &surface, dimensions)?;
|
||||||
|
|
||||||
|
let allocators = Allocators::new_default(device.clone())?;
|
||||||
|
|
||||||
|
let vertex_buffer = {
|
||||||
|
let mut builder = MeshBuilder::new();
|
||||||
|
builder.add_cube(0, 0, 0);
|
||||||
|
builder.add_cube(-1, 0, 0);
|
||||||
|
builder.add_cube(0, 0, -1);
|
||||||
|
builder.add_cube(-1, 0, -1);
|
||||||
|
builder.add_cube(0, 1, 0);
|
||||||
|
builder.upload(&allocators)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let uniform_allocator = SubbufferAllocator::new(
|
||||||
|
allocators.memory.clone(),
|
||||||
|
SubbufferAllocatorCreateInfo {
|
||||||
|
buffer_usage: BufferUsage::UNIFORM_BUFFER,
|
||||||
|
memory_type_filter: MemoryTypeFilter::PREFER_DEVICE
|
||||||
|
| MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let render_pass = vulkano::single_pass_renderpass!(
|
||||||
|
device.clone(),
|
||||||
|
attachments: {
|
||||||
|
color: {
|
||||||
|
format: swapchain.image_format(),
|
||||||
|
samples: 1,
|
||||||
|
load_op: DontCare,
|
||||||
|
store_op: Store,
|
||||||
|
},
|
||||||
|
msaa: {
|
||||||
|
format: swapchain.image_format(),
|
||||||
|
samples: 4,
|
||||||
|
load_op: Clear,
|
||||||
|
store_op: DontCare,
|
||||||
|
},
|
||||||
|
depth: {
|
||||||
|
format: DEPTH_FORMAT,
|
||||||
|
samples: 4,
|
||||||
|
load_op: Clear,
|
||||||
|
store_op: DontCare,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pass: {
|
||||||
|
color: [msaa],
|
||||||
|
color_resolve: [color],
|
||||||
|
depth_stencil: {depth},
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let framebuffers = util::create_3d_framebuffers(
|
||||||
|
&allocators,
|
||||||
|
&render_pass,
|
||||||
|
&swapchain_images,
|
||||||
|
DEPTH_FORMAT,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let viewport = Viewport::from_window(&window);
|
||||||
|
let vertex_shader = shaders::plain::vs::load(device.clone())?
|
||||||
|
.entry_point("main")
|
||||||
|
.expect("shader entry point");
|
||||||
|
let fragment_shader = shaders::plain::fs::load(device.clone())?
|
||||||
|
.entry_point("main")
|
||||||
|
.expect("shader entry point");
|
||||||
|
let pipeline = shaders::create_two_stage_3d_pipeline::<InputVertex>(
|
||||||
|
&device,
|
||||||
|
&render_pass,
|
||||||
|
viewport.clone(),
|
||||||
|
&vertex_shader,
|
||||||
|
&fragment_shader,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
window,
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
|
||||||
|
allocators,
|
||||||
|
|
||||||
|
swapchain,
|
||||||
|
swapchain_images,
|
||||||
|
swapchain_dirty: false,
|
||||||
|
|
||||||
|
vertex_buffer,
|
||||||
|
uniform_allocator,
|
||||||
|
|
||||||
|
render_pass,
|
||||||
|
viewport,
|
||||||
|
framebuffers,
|
||||||
|
|
||||||
|
vertex_shader,
|
||||||
|
fragment_shader,
|
||||||
|
pipeline,
|
||||||
|
|
||||||
|
start,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&mut self) -> Result<(), Error> {
|
||||||
|
self.refresh_swapchain()?;
|
||||||
|
|
||||||
|
let (image_index, suboptimal, acquire_future) =
|
||||||
|
match swapchain::acquire_next_image(self.swapchain.clone(), None)
|
||||||
|
.map_err(Validated::unwrap)
|
||||||
|
{
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(VulkanError::OutOfDate) => {
|
||||||
|
self.swapchain_dirty = true;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(e) => return Err(e.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if suboptimal {
|
||||||
|
self.swapchain_dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let framebuffer = &self.framebuffers[image_index as usize];
|
||||||
|
|
||||||
|
let dimensions = self.window.inner_size();
|
||||||
|
let aspect = dimensions.width as f32 / dimensions.height as f32;
|
||||||
|
|
||||||
|
let delta = self.start.elapsed();
|
||||||
|
let dt = delta.as_secs_f64() * 1.0;
|
||||||
|
let model = Mat4::IDENTITY;
|
||||||
|
let projection = Mat4::perspective_rh_gl(45.0f32.to_radians(), aspect, 0.01, 100.0);
|
||||||
|
|
||||||
|
fn make_view(camera: Vec3, target: Vec3) -> Mat4 {
|
||||||
|
Mat4::look_at_rh(camera, target, Vec3::Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
let view = make_view(
|
||||||
|
Vec3::new((dt.cos() * 3.0) as f32, 3.0, (dt.sin() * 3.0) as f32),
|
||||||
|
Vec3::new(0.0, 0.0, 0.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
let uniform_buffer = self
|
||||||
|
.uniform_allocator
|
||||||
|
.allocate_sized::<shaders::plain::SceneData>()?;
|
||||||
|
{
|
||||||
|
let mut write = uniform_buffer.write()?;
|
||||||
|
*write = shaders::plain::SceneData {
|
||||||
|
model: model.to_cols_array_2d(),
|
||||||
|
view: view.to_cols_array_2d(),
|
||||||
|
projection: projection.to_cols_array_2d(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let descriptor_layout = &self.pipeline.layout().set_layouts()[0];
|
||||||
|
let set0 = DescriptorSet::new(
|
||||||
|
self.allocators.descriptor_set.clone(),
|
||||||
|
descriptor_layout.clone(),
|
||||||
|
[WriteDescriptorSet::buffer(0, uniform_buffer)],
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut builder = AutoCommandBufferBuilder::primary(
|
||||||
|
self.allocators.command_buffer.clone(),
|
||||||
|
self.queue.queue_family_index(),
|
||||||
|
CommandBufferUsage::OneTimeSubmit,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
builder.begin_render_pass(
|
||||||
|
RenderPassBeginInfo {
|
||||||
|
clear_values: vec![None, Some([0.3, 0.5, 0.9, 1.0].into()), Some(1.0.into())],
|
||||||
|
..RenderPassBeginInfo::framebuffer(framebuffer.clone())
|
||||||
|
},
|
||||||
|
SubpassBeginInfo {
|
||||||
|
contents: SubpassContents::Inline,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
builder
|
||||||
|
.bind_pipeline_graphics(self.pipeline.clone())?
|
||||||
|
.bind_descriptor_sets(
|
||||||
|
PipelineBindPoint::Graphics,
|
||||||
|
self.pipeline.layout().clone(),
|
||||||
|
0,
|
||||||
|
set0.offsets([0]),
|
||||||
|
)?
|
||||||
|
.bind_vertex_buffers(0, self.vertex_buffer.clone())?;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
builder.draw(self.vertex_buffer.len() as _, 1, 0, 0)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.end_render_pass(SubpassEndInfo::default())?;
|
||||||
|
|
||||||
|
let command_buffer = builder.build()?;
|
||||||
|
|
||||||
|
sync::now(self.device.clone())
|
||||||
|
.join(acquire_future)
|
||||||
|
.then_execute(self.queue.clone(), command_buffer)?
|
||||||
|
.then_swapchain_present(
|
||||||
|
self.queue.clone(),
|
||||||
|
SwapchainPresentInfo::swapchain_image_index(self.swapchain.clone(), image_index),
|
||||||
|
)
|
||||||
|
.then_signal_fence_and_flush()?
|
||||||
|
.wait(None)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn refresh_swapchain(&mut self) -> Result<(), Error> {
|
||||||
|
if !self.swapchain_dirty {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
log::info!("Recreate swapchain");
|
||||||
|
|
||||||
|
let dimensions = self.window.inner_size();
|
||||||
|
let create_info = self.swapchain.create_info();
|
||||||
|
let create_info = SwapchainCreateInfo {
|
||||||
|
image_extent: dimensions.into(),
|
||||||
|
..create_info
|
||||||
|
};
|
||||||
|
let (new_swapchain, new_swapchain_images) = self.swapchain.recreate(create_info)?;
|
||||||
|
|
||||||
|
self.viewport = Viewport::from_window(&self.window);
|
||||||
|
self.swapchain = new_swapchain;
|
||||||
|
self.swapchain_images = new_swapchain_images;
|
||||||
|
|
||||||
|
self.rebuild_pipelines()?;
|
||||||
|
|
||||||
|
self.framebuffers = util::create_3d_framebuffers(
|
||||||
|
&self.allocators,
|
||||||
|
&self.render_pass,
|
||||||
|
&self.swapchain_images,
|
||||||
|
DEPTH_FORMAT,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
self.swapchain_dirty = false;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild_pipelines(&mut self) -> Result<(), Error> {
|
||||||
|
self.pipeline = shaders::create_two_stage_3d_pipeline::<InputVertex>(
|
||||||
|
&self.device,
|
||||||
|
&self.render_pass,
|
||||||
|
self.viewport.clone(),
|
||||||
|
&self.vertex_shader,
|
||||||
|
&self.fragment_shader,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_swapchain_dirty(&mut self) {
|
||||||
|
self.swapchain_dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use vulkano::{
|
||||||
|
descriptor_set::layout::DescriptorType,
|
||||||
|
device::Device,
|
||||||
|
pipeline::{
|
||||||
|
graphics::{
|
||||||
|
color_blend::{ColorBlendAttachmentState, ColorBlendState},
|
||||||
|
depth_stencil::{DepthState, DepthStencilState},
|
||||||
|
input_assembly::InputAssemblyState,
|
||||||
|
multisample::MultisampleState,
|
||||||
|
rasterization::RasterizationState,
|
||||||
|
vertex_input::{Vertex, VertexDefinition},
|
||||||
|
viewport::{Viewport, ViewportState},
|
||||||
|
GraphicsPipelineCreateInfo,
|
||||||
|
},
|
||||||
|
layout::PipelineDescriptorSetLayoutCreateInfo,
|
||||||
|
GraphicsPipeline, PipelineLayout, PipelineShaderStageCreateInfo,
|
||||||
|
},
|
||||||
|
render_pass::{RenderPass, Subpass},
|
||||||
|
shader::EntryPoint,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::error::Error;
|
||||||
|
|
||||||
|
pub mod plain {
|
||||||
|
pub mod vs {
|
||||||
|
vulkano_shaders::shader! {
|
||||||
|
ty: "vertex",
|
||||||
|
path: "src/render/shaders/plain.vert",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub mod fs {
|
||||||
|
vulkano_shaders::shader! {
|
||||||
|
ty: "fragment",
|
||||||
|
path: "src/render/shaders/plain.frag",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use vs::SceneData;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_two_stage_3d_pipeline<V: Vertex>(
|
||||||
|
device: &Arc<Device>,
|
||||||
|
render_pass: &Arc<RenderPass>,
|
||||||
|
viewport: Viewport,
|
||||||
|
vertex_shader: &EntryPoint,
|
||||||
|
fragment_shader: &EntryPoint,
|
||||||
|
) -> Result<Arc<GraphicsPipeline>, Error> {
|
||||||
|
let vertex_input_state = V::per_vertex().definition(vertex_shader)?;
|
||||||
|
let stages = [
|
||||||
|
PipelineShaderStageCreateInfo::new(vertex_shader.clone()),
|
||||||
|
PipelineShaderStageCreateInfo::new(fragment_shader.clone()),
|
||||||
|
];
|
||||||
|
|
||||||
|
let layout = {
|
||||||
|
let mut layout_create_info = PipelineDescriptorSetLayoutCreateInfo::from_stages(&stages);
|
||||||
|
layout_create_info.set_layouts[0]
|
||||||
|
.bindings
|
||||||
|
.get_mut(&0)
|
||||||
|
.unwrap()
|
||||||
|
.descriptor_type = DescriptorType::UniformBufferDynamic;
|
||||||
|
|
||||||
|
PipelineLayout::new(
|
||||||
|
device.clone(),
|
||||||
|
layout_create_info.into_pipeline_layout_create_info(device.clone())?,
|
||||||
|
)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let subpass = Subpass::from(render_pass.clone(), 0).unwrap();
|
||||||
|
|
||||||
|
let pipeline = GraphicsPipeline::new(
|
||||||
|
device.clone(),
|
||||||
|
None,
|
||||||
|
GraphicsPipelineCreateInfo {
|
||||||
|
stages: stages.into_iter().collect(),
|
||||||
|
vertex_input_state: Some(vertex_input_state),
|
||||||
|
input_assembly_state: Some(InputAssemblyState::default()),
|
||||||
|
viewport_state: Some(ViewportState {
|
||||||
|
viewports: [viewport].into_iter().collect(),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
rasterization_state: Some(RasterizationState::default()),
|
||||||
|
multisample_state: Some(MultisampleState {
|
||||||
|
rasterization_samples: subpass.num_samples().unwrap(),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
color_blend_state: Some(ColorBlendState::with_attachment_states(
|
||||||
|
subpass.num_color_attachments(),
|
||||||
|
ColorBlendAttachmentState::default(),
|
||||||
|
)),
|
||||||
|
depth_stencil_state: Some(DepthStencilState {
|
||||||
|
depth: Some(DepthState::simple()),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
subpass: Some(subpass.into()),
|
||||||
|
..GraphicsPipelineCreateInfo::layout(layout)
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(pipeline)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#version 460
|
||||||
|
|
||||||
|
layout(location = 0) in vec3 i_color;
|
||||||
|
|
||||||
|
layout(location = 0) out vec4 o_color;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
o_color = vec4(i_color, 1.0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#version 460
|
||||||
|
|
||||||
|
layout(location = 0) in vec3 i_position;
|
||||||
|
layout(location = 1) in vec3 i_color;
|
||||||
|
|
||||||
|
layout(location = 0) out vec3 o_color;
|
||||||
|
|
||||||
|
layout(set = 0, binding = 0) uniform SceneData {
|
||||||
|
mat4 model;
|
||||||
|
mat4 view;
|
||||||
|
mat4 projection;
|
||||||
|
} u_scene;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 m_position = vec4(i_position, 1.0);
|
||||||
|
vec4 s_position = u_scene.projection * u_scene.view * m_position;
|
||||||
|
|
||||||
|
gl_Position = s_position;
|
||||||
|
o_color = i_color;
|
||||||
|
}
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use vulkano::{
|
||||||
|
command_buffer::allocator::{CommandBufferAllocator, StandardCommandBufferAllocator},
|
||||||
|
descriptor_set::allocator::{DescriptorSetAllocator, StandardDescriptorSetAllocator},
|
||||||
|
device::{
|
||||||
|
physical::PhysicalDeviceType, Device, DeviceCreateInfo, DeviceExtensions, Queue,
|
||||||
|
QueueCreateInfo, QueueFlags,
|
||||||
|
},
|
||||||
|
format::Format,
|
||||||
|
image::{view::ImageView, Image, ImageCreateInfo, ImageType, ImageUsage, SampleCount},
|
||||||
|
instance::{Instance, InstanceCreateInfo},
|
||||||
|
memory::allocator::StandardMemoryAllocator,
|
||||||
|
pipeline::graphics::viewport::Viewport,
|
||||||
|
render_pass::{Framebuffer, FramebufferCreateInfo, RenderPass},
|
||||||
|
swapchain::{Surface, Swapchain, SwapchainCreateInfo},
|
||||||
|
VulkanLibrary,
|
||||||
|
};
|
||||||
|
use winit::{dpi::PhysicalSize, window::Window};
|
||||||
|
|
||||||
|
use crate::error::Error;
|
||||||
|
|
||||||
|
pub struct Allocators {
|
||||||
|
pub memory: Arc<StandardMemoryAllocator>,
|
||||||
|
pub command_buffer: Arc<dyn CommandBufferAllocator>,
|
||||||
|
pub descriptor_set: Arc<dyn DescriptorSetAllocator>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ViewportExt {
|
||||||
|
fn from_window(window: &Arc<Window>) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ViewportExt for Viewport {
|
||||||
|
fn from_window(window: &Arc<Window>) -> Self {
|
||||||
|
let dimensions = window.inner_size();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
offset: [0.0, dimensions.height as _],
|
||||||
|
extent: [dimensions.width as _, -(dimensions.height as f32)],
|
||||||
|
depth_range: 0.0..=1.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Allocators {
|
||||||
|
pub fn new_default(device: Arc<Device>) -> Result<Arc<Self>, Error> {
|
||||||
|
let memory = Arc::new(StandardMemoryAllocator::new_default(device.clone()));
|
||||||
|
let command_buffer = Arc::new(StandardCommandBufferAllocator::new(
|
||||||
|
device.clone(),
|
||||||
|
Default::default(),
|
||||||
|
));
|
||||||
|
let descriptor_set = Arc::new(StandardDescriptorSetAllocator::new(
|
||||||
|
device.clone(),
|
||||||
|
Default::default(),
|
||||||
|
));
|
||||||
|
|
||||||
|
Ok(Arc::new(Self {
|
||||||
|
memory,
|
||||||
|
command_buffer,
|
||||||
|
descriptor_set,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_instance(info: InstanceCreateInfo) -> Result<Arc<Instance>, Error> {
|
||||||
|
let library = VulkanLibrary::new()?;
|
||||||
|
let instance = Instance::new(library, info)?;
|
||||||
|
Ok(instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_device(
|
||||||
|
instance: &Arc<Instance>,
|
||||||
|
required_extensions: DeviceExtensions,
|
||||||
|
surface: &Arc<Surface>,
|
||||||
|
) -> Result<(Arc<Device>, Arc<Queue>), Error> {
|
||||||
|
let (physical_device, queue_family_index) = instance
|
||||||
|
.enumerate_physical_devices()?
|
||||||
|
.filter(|device| device.supported_extensions().contains(&required_extensions))
|
||||||
|
.filter_map(|device| {
|
||||||
|
let queue_family_index = device
|
||||||
|
.queue_family_properties()
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.position(|(index, qf)| {
|
||||||
|
qf.queue_flags.contains(QueueFlags::GRAPHICS)
|
||||||
|
&& device
|
||||||
|
.surface_support(index as u32, surface)
|
||||||
|
.unwrap_or(false)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Some((device, queue_family_index as u32))
|
||||||
|
})
|
||||||
|
.min_by_key(|(device, _)| match device.properties().device_type {
|
||||||
|
PhysicalDeviceType::DiscreteGpu => 0,
|
||||||
|
PhysicalDeviceType::IntegratedGpu => 1,
|
||||||
|
PhysicalDeviceType::VirtualGpu => 2,
|
||||||
|
PhysicalDeviceType::Cpu => 3,
|
||||||
|
_ => 3,
|
||||||
|
})
|
||||||
|
.ok_or(Error::NoValidDevice)?;
|
||||||
|
|
||||||
|
let (device, mut queues) = Device::new(
|
||||||
|
physical_device,
|
||||||
|
DeviceCreateInfo {
|
||||||
|
queue_create_infos: vec![QueueCreateInfo {
|
||||||
|
queue_family_index,
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
enabled_extensions: required_extensions,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let queue = queues.next().unwrap();
|
||||||
|
|
||||||
|
Ok((device, queue))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_swapchain(
|
||||||
|
device: &Arc<Device>,
|
||||||
|
surface: &Arc<Surface>,
|
||||||
|
dimensions: PhysicalSize<u32>,
|
||||||
|
) -> Result<(Arc<Swapchain>, Vec<Arc<Image>>), Error> {
|
||||||
|
let physical_device = device.physical_device();
|
||||||
|
let caps = physical_device.surface_capabilities(surface, Default::default())?;
|
||||||
|
|
||||||
|
let composite_alpha = caps
|
||||||
|
.supported_composite_alpha
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.expect("a supported composite alpha");
|
||||||
|
let image_format = physical_device.surface_formats(surface, Default::default())?[0].0;
|
||||||
|
|
||||||
|
let (swapchain, images) = Swapchain::new(
|
||||||
|
device.clone(),
|
||||||
|
surface.clone(),
|
||||||
|
SwapchainCreateInfo {
|
||||||
|
min_image_count: caps.min_image_count + 1,
|
||||||
|
image_format,
|
||||||
|
image_extent: dimensions.into(),
|
||||||
|
image_usage: ImageUsage::COLOR_ATTACHMENT
|
||||||
|
| ImageUsage::TRANSFER_DST
|
||||||
|
| ImageUsage::STORAGE,
|
||||||
|
composite_alpha,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok((swapchain, images))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_3d_framebuffers(
|
||||||
|
allocators: &Allocators,
|
||||||
|
render_pass: &Arc<RenderPass>,
|
||||||
|
swapchain_images: &[Arc<Image>],
|
||||||
|
depth_format: Format,
|
||||||
|
) -> Result<Vec<Arc<Framebuffer>>, Error> {
|
||||||
|
let msaa_image = Image::new(
|
||||||
|
allocators.memory.clone(),
|
||||||
|
ImageCreateInfo {
|
||||||
|
image_type: ImageType::Dim2d,
|
||||||
|
format: swapchain_images[0].format(),
|
||||||
|
extent: swapchain_images[0].extent(),
|
||||||
|
usage: ImageUsage::COLOR_ATTACHMENT | ImageUsage::TRANSIENT_ATTACHMENT,
|
||||||
|
samples: SampleCount::Sample4,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Default::default(),
|
||||||
|
)?;
|
||||||
|
let depth_image = Image::new(
|
||||||
|
allocators.memory.clone(),
|
||||||
|
ImageCreateInfo {
|
||||||
|
format: depth_format,
|
||||||
|
image_type: ImageType::Dim2d,
|
||||||
|
extent: swapchain_images[0].extent(),
|
||||||
|
usage: ImageUsage::DEPTH_STENCIL_ATTACHMENT | ImageUsage::TRANSIENT_ATTACHMENT,
|
||||||
|
samples: SampleCount::Sample4,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Default::default(),
|
||||||
|
)?;
|
||||||
|
let msaa_attachment = ImageView::new_default(msaa_image)?;
|
||||||
|
let depth_attachment = ImageView::new_default(depth_image)?;
|
||||||
|
|
||||||
|
let framebuffers = swapchain_images
|
||||||
|
.iter()
|
||||||
|
.map(|image| {
|
||||||
|
let color_attachment = ImageView::new_default(image.clone())?;
|
||||||
|
let framebuffer = Framebuffer::new(
|
||||||
|
render_pass.clone(),
|
||||||
|
FramebufferCreateInfo {
|
||||||
|
attachments: vec![
|
||||||
|
color_attachment,
|
||||||
|
msaa_attachment.clone(),
|
||||||
|
depth_attachment.clone(),
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(framebuffer)
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, Error>>()?;
|
||||||
|
|
||||||
|
Ok(framebuffers)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
use glam::Vec3;
|
||||||
|
use vulkano::{buffer::BufferContents, pipeline::graphics::vertex_input::Vertex};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, BufferContents, Vertex)]
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct InputVertex {
|
||||||
|
#[format(R32G32B32_SFLOAT)]
|
||||||
|
pub i_position: Vec3,
|
||||||
|
#[format(R32G32B32_SFLOAT)]
|
||||||
|
pub i_color: Vec3,
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user