Add an interactive fractal example (#1707)

* Add interactive fractal example

* Rename folder

* Update guide
This commit is contained in:
Okko Hakola 2021-09-13 18:14:54 +03:00 committed by GitHub
parent 54cb28d4e5
commit e0839af295
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 1489 additions and 0 deletions

View File

@ -22,3 +22,5 @@ png = "0.17"
time = "0.3"
serde = { version = "1.0", features = ["derive"] }
ron = "0.6"
rand = "0.8.4"

View File

@ -0,0 +1,318 @@
// Copyright (c) 2021 The vulkano developers
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE or
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT
// license <LICENSE-MIT or https://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
use crate::fractal_compute_pipeline::FractalComputePipeline;
use crate::renderer::{InterimImageView, RenderOptions, Renderer};
use cgmath::Vector2;
use time::Instant;
use vulkano::sync::GpuFuture;
use winit::dpi::PhysicalPosition;
use winit::event::{
ElementState, Event, KeyboardInput, MouseButton, MouseScrollDelta, VirtualKeyCode, WindowEvent,
};
const MAX_ITERS_INIT: u32 = 200;
const MOVE_SPEED: f32 = 0.5;
/// App for exploring Julia and Mandelbrot fractals
pub struct FractalApp {
/// Pipeline that computes Mandelbrot & Julia fractals and writes them to an image
fractal_pipeline: FractalComputePipeline,
/// Toggle that flips between julia and mandelbrot
pub is_julia: bool,
/// Togglet thats stops the movement on Julia
is_c_paused: bool,
/// C is a constant input to Julia escape time algorithm (mouse position).
c: Vector2<f32>,
/// Our zoom level
scale: Vector2<f32>,
/// Our translation on the complex plane
translation: Vector2<f32>,
/// How far should the escape time algorithm run (higher = less performance, more accurate image)
pub max_iters: u32,
/// Time tracking, useful for frame independent movement
time: Instant,
dt: f32,
dt_sum: f32,
frame_count: f32,
avg_fps: f32,
/// Input state to handle mouse positions, continuous movement etc.
input_state: InputState,
}
impl FractalApp {
pub fn new(renderer: &Renderer) -> FractalApp {
FractalApp {
fractal_pipeline: FractalComputePipeline::new(renderer.queue()),
is_julia: false,
is_c_paused: false,
c: Vector2::new(0.0, 0.0),
scale: Vector2::new(4.0, 4.0),
translation: Vector2::new(0.0, 0.0),
max_iters: MAX_ITERS_INIT,
time: Instant::now(),
dt: 0.0,
dt_sum: 0.0,
frame_count: 0.0,
avg_fps: 0.0,
input_state: InputState::new(),
}
}
pub fn print_guide(&self) {
println!(
"\
Usage:
WASD: Pan view
Scroll: Zoom in/out
Space: Toggle between Mandelbrot and Julia
Enter: Randomize color palette
Equals/Minus: Increase/Decrease max iterations
F: Toggle fullscreen
Right mouse: Stop movement in Julia (mouse position determines c)
Esc: Quit\
"
);
}
/// Run our compute pipeline and return a future of when the compute is finished
pub fn compute(&mut self, image_target: InterimImageView) -> Box<dyn GpuFuture> {
self.fractal_pipeline.compute(
image_target,
self.c,
self.scale,
self.translation,
self.max_iters,
self.is_julia,
)
}
/// Should the app quit? (on esc)
pub fn is_running(&self) -> bool {
!self.input_state.should_quit
}
/// Return average fps
pub fn avg_fps(&self) -> f32 {
self.avg_fps
}
/// Delta time in milliseconds
pub fn dt(&self) -> f32 {
self.dt * 1000.0
}
/// Update times and dt at the end of each frame
pub fn update_time(&mut self) {
// Each second, update average fps & reset frame count & dt sum
if self.dt_sum > 1.0 {
self.avg_fps = self.frame_count / self.dt_sum;
self.frame_count = 0.0;
self.dt_sum = 0.0;
}
self.dt = self.time.elapsed().as_seconds_f32();
self.dt_sum += self.dt;
self.frame_count += 1.0;
self.time = Instant::now();
}
/// Updates app state based on input state
pub fn update_state_after_inputs(&mut self, renderer: &mut Renderer) {
// Zoom in or out
if self.input_state.scroll_delta > 0. {
self.scale /= 1.05;
} else if self.input_state.scroll_delta < 0. {
self.scale *= 1.05;
}
// Move speed scaled by zoom level
let move_speed = MOVE_SPEED * self.dt * self.scale.x;
// Panning
if self.input_state.pan_up {
self.translation += Vector2::new(0.0, move_speed);
}
if self.input_state.pan_down {
self.translation += Vector2::new(0.0, -move_speed);
}
if self.input_state.pan_right {
self.translation += Vector2::new(move_speed, 0.0);
}
if self.input_state.pan_left {
self.translation += Vector2::new(-move_speed, 0.0);
}
// Toggle between julia and mandelbrot
if self.input_state.toggle_julia {
self.is_julia = !self.is_julia;
}
// Toggle c
if self.input_state.toggle_c {
self.is_c_paused = !self.is_c_paused;
}
// Update c
if !self.is_c_paused {
// Scale normalized mouse pos between -1.0 and 1.0;
let mouse_pos = self.input_state.normalized_mouse_pos() * 2.0 - Vector2::new(1.0, 1.0);
// Scale by our zoom (scale) level so when zooming in the movement on julia is not so drastic
self.c = mouse_pos * self.scale.x;
}
// Update how many iterations we have
if self.input_state.increase_iterations {
self.max_iters += 1;
}
if self.input_state.decrease_iterations {
if self.max_iters as i32 - 1 <= 0 {
self.max_iters = 0;
} else {
self.max_iters -= 1;
}
}
// Randomize our palette
if self.input_state.randomize_palette {
self.fractal_pipeline.randomize_palette();
}
// Toggle fullscreen
if self.input_state.toggle_fullscreen {
renderer.toggle_fullscreen()
}
}
/// Update input state
pub fn handle_input(&mut self, window_size: [u32; 2], event: &Event<()>) {
self.input_state.handle_input(window_size, event);
}
/// reset input state at the end of frame
pub fn reset_input_state(&mut self) {
self.input_state.reset()
}
}
fn state_is_pressed(state: ElementState) -> bool {
match state {
ElementState::Pressed => true,
ElementState::Released => false,
}
}
/// Just a very simple input state (mappings).
/// Winit only has Pressed and Released events, thus continuous movement needs toggles.
/// Panning is one of those where continuous movement feels better.
struct InputState {
pub window_size: [u32; 2],
pub pan_up: bool,
pub pan_down: bool,
pub pan_right: bool,
pub pan_left: bool,
pub increase_iterations: bool,
pub decrease_iterations: bool,
pub randomize_palette: bool,
pub toggle_fullscreen: bool,
pub toggle_julia: bool,
pub toggle_c: bool,
pub should_quit: bool,
pub scroll_delta: f32,
pub mouse_pos: Vector2<f32>,
}
impl InputState {
fn new() -> InputState {
InputState {
window_size: RenderOptions::default().window_size,
pan_up: false,
pan_down: false,
pan_right: false,
pan_left: false,
increase_iterations: false,
decrease_iterations: false,
randomize_palette: false,
toggle_fullscreen: false,
toggle_julia: false,
toggle_c: false,
should_quit: false,
scroll_delta: 0.0,
mouse_pos: Vector2::new(0.0, 0.0),
}
}
fn normalized_mouse_pos(&self) -> Vector2<f32> {
Vector2::new(
(self.mouse_pos.x / self.window_size[0] as f32).clamp(0.0, 1.0),
(self.mouse_pos.y / self.window_size[1] as f32).clamp(0.0, 1.0),
)
}
// Resets values that should be reset. All incremental mappings and toggles should be reset.
fn reset(&mut self) {
*self = InputState {
scroll_delta: 0.0,
toggle_fullscreen: false,
toggle_julia: false,
toggle_c: false,
randomize_palette: false,
increase_iterations: false,
decrease_iterations: false,
..*self
}
}
fn handle_input(&mut self, window_size: [u32; 2], event: &Event<()>) {
self.window_size = window_size;
if let winit::event::Event::WindowEvent { event, .. } = event {
match event {
WindowEvent::KeyboardInput { input, .. } => self.on_keyboard_event(input),
WindowEvent::MouseInput { state, button, .. } => {
self.on_mouse_click_event(*state, *button)
}
WindowEvent::CursorMoved { position, .. } => self.on_cursor_moved_event(position),
WindowEvent::MouseWheel { delta, .. } => self.on_mouse_wheel_event(delta),
_ => {}
}
}
}
/// Match keyboard event to our defined inputs
fn on_keyboard_event(&mut self, input: &KeyboardInput) {
if let Some(key_code) = input.virtual_keycode {
match key_code {
VirtualKeyCode::Escape => self.should_quit = state_is_pressed(input.state),
VirtualKeyCode::W => self.pan_up = state_is_pressed(input.state),
VirtualKeyCode::A => self.pan_left = state_is_pressed(input.state),
VirtualKeyCode::S => self.pan_down = state_is_pressed(input.state),
VirtualKeyCode::D => self.pan_right = state_is_pressed(input.state),
VirtualKeyCode::F => self.toggle_fullscreen = state_is_pressed(input.state),
VirtualKeyCode::Return => self.randomize_palette = state_is_pressed(input.state),
VirtualKeyCode::Equals => self.increase_iterations = state_is_pressed(input.state),
VirtualKeyCode::Minus => self.decrease_iterations = state_is_pressed(input.state),
VirtualKeyCode::Space => self.toggle_julia = state_is_pressed(input.state),
_ => (),
}
}
}
/// Update mouse scroll delta
fn on_mouse_wheel_event(&mut self, delta: &MouseScrollDelta) {
let change = match delta {
MouseScrollDelta::LineDelta(_x, y) => *y,
MouseScrollDelta::PixelDelta(pos) => pos.y as f32,
};
self.scroll_delta += change;
}
/// Update mouse position
fn on_cursor_moved_event(&mut self, pos: &PhysicalPosition<f64>) {
self.mouse_pos = Vector2::new(pos.x as f32, pos.y as f32);
}
/// Update toggle julia state (if right mouse is clicked)
fn on_mouse_click_event(&mut self, state: ElementState, mouse_btn: winit::event::MouseButton) {
match mouse_btn {
MouseButton::Right => self.toggle_c = state_is_pressed(state),
_ => (),
};
}
}

View File

@ -0,0 +1,239 @@
// Copyright (c) 2021 The vulkano developers
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE or
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT
// license <LICENSE-MIT or https://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
use crate::renderer::InterimImageView;
use cgmath::Vector2;
use rand::Rng;
use std::sync::Arc;
use vulkano::buffer::{BufferUsage, CpuAccessibleBuffer, TypedBufferAccess};
use vulkano::command_buffer::PrimaryCommandBuffer;
use vulkano::command_buffer::{AutoCommandBufferBuilder, CommandBufferUsage};
use vulkano::descriptor_set::PersistentDescriptorSet;
use vulkano::device::Queue;
use vulkano::image::ImageViewAbstract;
use vulkano::pipeline::{ComputePipeline, PipelineBindPoint};
use vulkano::sync::GpuFuture;
pub struct FractalComputePipeline {
gfx_queue: Arc<Queue>,
pipeline: Arc<ComputePipeline>,
palette: Arc<CpuAccessibleBuffer<[[f32; 4]]>>,
end_color: [f32; 4],
}
impl FractalComputePipeline {
pub fn new(gfx_queue: Arc<Queue>) -> FractalComputePipeline {
// Initial colors
let colors = vec![
[1.0, 0.0, 0.0, 1.0],
[1.0, 1.0, 0.0, 1.0],
[0.0, 1.0, 0.0, 1.0],
[0.0, 1.0, 1.0, 1.0],
[0.0, 0.0, 1.0, 1.0],
[1.0, 0.0, 1.0, 1.0],
];
let palette = CpuAccessibleBuffer::from_iter(
gfx_queue.device().clone(),
BufferUsage::all(),
false,
colors.into_iter(),
)
.unwrap();
let end_color = [0.0; 4];
let pipeline = {
let shader = cs::Shader::load(gfx_queue.device().clone()).unwrap();
Arc::new(
ComputePipeline::new(
gfx_queue.device().clone(),
&shader.main_entry_point(),
&(),
None,
|_| {},
)
.unwrap(),
)
};
FractalComputePipeline {
gfx_queue,
pipeline,
palette,
end_color,
}
}
/// Randomizes our color palette
pub fn randomize_palette(&mut self) {
let mut colors = vec![];
for _ in 0..self.palette.len() {
let r = rand::thread_rng().gen::<f32>();
let g = rand::thread_rng().gen::<f32>();
let b = rand::thread_rng().gen::<f32>();
let a = rand::thread_rng().gen::<f32>();
colors.push([r, g, b, a]);
}
self.palette = CpuAccessibleBuffer::from_iter(
self.gfx_queue.device().clone(),
BufferUsage::all(),
false,
colors.into_iter(),
)
.unwrap();
}
pub fn compute(
&mut self,
image: InterimImageView,
c: Vector2<f32>,
scale: Vector2<f32>,
translation: Vector2<f32>,
max_iters: u32,
is_julia: bool,
) -> Box<dyn GpuFuture> {
// Resize image if needed
let img_dims = image.image().dimensions().width_height();
let pipeline_layout = self.pipeline.layout();
let desc_layout = pipeline_layout.descriptor_set_layouts().get(0).unwrap();
let mut desc_set_builder = PersistentDescriptorSet::start(desc_layout.clone());
desc_set_builder
.add_image(image.clone())
.unwrap()
.add_buffer(self.palette.clone())
.unwrap();
let set = desc_set_builder.build().unwrap();
let mut builder = AutoCommandBufferBuilder::primary(
self.gfx_queue.device().clone(),
self.gfx_queue.family(),
CommandBufferUsage::OneTimeSubmit,
)
.unwrap();
let push_constants = cs::ty::PushConstants {
c: c.into(),
scale: scale.into(),
translation: translation.into(),
end_color: self.end_color,
palette_size: self.palette.len() as i32,
max_iters: max_iters as i32,
is_julia: is_julia as u32,
_dummy0: [0u8; 8], // Required for alignment
};
builder
.bind_pipeline_compute(self.pipeline.clone())
.bind_descriptor_sets(PipelineBindPoint::Compute, pipeline_layout.clone(), 0, set)
.push_constants(pipeline_layout.clone(), 0, push_constants)
.dispatch([img_dims[0] / 8, img_dims[1] / 8, 1])
.unwrap();
let command_buffer = builder.build().unwrap();
let finished = command_buffer.execute(self.gfx_queue.clone()).unwrap();
finished.then_signal_fence_and_flush().unwrap().boxed()
}
}
mod cs {
vulkano_shaders::shader! {
ty: "compute",
src: "
#version 450
layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
// Image to which we'll write our fractal
layout(set = 0, binding = 0, rgba8) uniform writeonly image2D img;
// Our palette as a dynamic buffer
layout(set = 0, binding = 1) buffer Palette {
vec4 data[];
} palette;
// Our variable inputs as push constants
layout(push_constant) uniform PushConstants {
vec2 c;
vec2 scale;
vec2 translation;
vec4 end_color;
int palette_size;
int max_iters;
bool is_julia;
} push_constants;
// Gets smooth color between current color (determined by iterations) and the next color in the palette
// by linearly interpolating the colors based on: https://linas.org/art-gallery/escape/smooth.html
vec4 get_color(
int palette_size,
vec4 end_color,
int i,
int max_iters,
float len_z
) {
if (i < max_iters) {
float iters_float = float(i) + 1.0 - log(log(len_z)) / log(2.0f);
float iters_floor = floor(iters_float);
float remainder = iters_float - iters_floor;
vec4 color_start = palette.data[int(iters_floor) % push_constants.palette_size];
vec4 color_end = palette.data[(int(iters_floor) + 1) % push_constants.palette_size];
return mix(color_start, color_end, remainder);
}
return end_color;
}
void main() {
// Scale image pixels to range
vec2 dims = vec2(imageSize(img));
float ar = dims.x / dims.y;
float x_over_width = (gl_GlobalInvocationID.x / dims.x);
float y_over_height = (gl_GlobalInvocationID.y / dims.y);
float x0 = ar * (push_constants.translation.x + (x_over_width - 0.5) * push_constants.scale.x);
float y0 = push_constants.translation.y + (y_over_height - 0.5) * push_constants.scale.y;
// Julia is like mandelbrot, but instead changing the constant `c` will change the shape
// you'll see. Thus we want to bind the c to mouse position.
// With mandelbrot, c = scaled xy position of the image. Z starts from zero.
// With julia, c = any value between the interesting range (-2.0 - 2.0), Z = scaled xy position of the image.
vec2 c;
vec2 z;
if (push_constants.is_julia) {
c = push_constants.c;
z = vec2(x0, y0);
} else {
c = vec2(x0, y0);
z = vec2(0.0, 0.0);
}
// Escape time algorithm:
// https://en.wikipedia.org/wiki/Plotting_algorithms_for_the_Mandelbrot_set
// It's an iterative algorithm where the bailout point (number of iterations) will determine
// the color we choose from the palette
int i;
float len_z;
for (i = 0; i < push_constants.max_iters; i += 1) {
z = vec2(
z.x * z.x - z.y * z.y + c.x,
z.y * z.x + z.x * z.y + c.y
);
len_z = length(z);
// Using 8.0 for bailout limit give a little nicer colors with smooth colors
// 2.0 is enough to 'determine' an escape will happen
if (len_z > 8.0) {
break;
}
}
vec4 write_color = get_color(
push_constants.palette_size,
push_constants.end_color,
i,
push_constants.max_iters,
len_z
);
imageStore(img, ivec2(gl_GlobalInvocationID.xy), write_color);
}"
}
}

View File

@ -0,0 +1,125 @@
// Copyright (c) 2021 The vulkano developers
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE or
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT
// license <LICENSE-MIT or https://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
use crate::app::FractalApp;
use crate::renderer::{image_over_frame_renderpass, RenderOptions, Renderer};
use vulkano::sync::GpuFuture;
use winit::{
event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoop},
platform::run_return::EventLoopExtRunReturn,
};
mod app;
mod fractal_compute_pipeline;
mod pixels_draw_pipeline;
mod place_over_frame;
mod renderer;
/// This is an example demonstrating an application with some more non-trivial functionality.
/// It should get you more up to speed with how you can use Vulkano.
/// It contains
/// - Compute pipeline to calculate Mandelbrot and Julia fractals writing them to an image target
/// - Graphics pipeline to draw the fractal image over a quad that covers the whole screen
/// - Renderpass rendering that image over swapchain image
/// - An organized Renderer with functionality good enough to copy to other projects
/// - Simple FractalApp to handle runtime state
/// - Simple Input system to interact with the application
fn main() {
// Create event loop
let mut event_loop = EventLoop::new();
// Create a renderer with a window & render options
let mut renderer = Renderer::new(
&event_loop,
RenderOptions {
title: "Fractal",
..RenderOptions::default()
},
);
// Add our render target image onto which we'll be rendering our fractals.
// View size None here means renderer will keep resizing the image on resize
let render_target_id = 0;
renderer.add_interim_image_view(render_target_id, None, renderer.image_format());
// Create app to hold the logic of our fractal explorer
let mut app = FractalApp::new(&renderer);
app.print_guide();
// Basic loop for our runtime
// 1. Handle events
// 2. Update state based on events
// 3. Compute & Render
// 4. Reset input state
// 5. Update time & title
loop {
if !handle_events(&mut event_loop, &mut renderer, &mut app) {
break;
}
app.update_state_after_inputs(&mut renderer);
compute_then_render(&mut renderer, &mut app, render_target_id);
app.reset_input_state();
app.update_time();
renderer.window().set_title(&format!(
"{} fps: {:.2} dt: {:.2}, Max Iterations: {}",
if app.is_julia { "Julia" } else { "Mandelbrot" },
app.avg_fps(),
app.dt(),
app.max_iters
));
}
}
/// Handle events and return `bool` if we should quit
fn handle_events(
event_loop: &mut EventLoop<()>,
renderer: &mut Renderer,
app: &mut FractalApp,
) -> bool {
let mut is_running = true;
event_loop.run_return(|event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match &event {
Event::WindowEvent { event, .. } => match event {
WindowEvent::CloseRequested => is_running = false,
WindowEvent::Resized(..) | WindowEvent::ScaleFactorChanged { .. } => {
renderer.resize()
}
_ => (),
},
Event::MainEventsCleared => *control_flow = ControlFlow::Exit,
_ => (),
}
// Pass event for app to handle our inputs
app.handle_input(renderer.window_size(), &event);
});
is_running && app.is_running()
}
/// Orchestrate rendering here
fn compute_then_render(renderer: &mut Renderer, app: &mut FractalApp, target_image_id: usize) {
// Start frame
let before_pipeline_future = match renderer.start_frame() {
Err(e) => {
println!("{}", e.to_string());
return;
}
Ok(future) => future,
};
// Retrieve target image
let target_image = renderer.get_interim_image_view(target_image_id);
// Compute our fractal (writes to target image). Join future with `before_pipeline_future`.
let after_compute = app
.compute(target_image.clone())
.join(before_pipeline_future);
// Render target image over frame. Input previous future.
let after_renderpass_future =
image_over_frame_renderpass(renderer, after_compute, target_image);
// Finish frame (which presents the view). Input last future
renderer.finish_frame(after_renderpass_future);
}

View File

@ -0,0 +1,215 @@
// Copyright (c) 2021 The vulkano developers
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE or
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT
// license <LICENSE-MIT or https://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
use std::sync::Arc;
use vulkano::buffer::TypedBufferAccess;
use vulkano::command_buffer::{AutoCommandBufferBuilder, CommandBufferUsage};
use vulkano::descriptor_set::PersistentDescriptorSet;
use vulkano::pipeline::viewport::Viewport;
use vulkano::pipeline::PipelineBindPoint;
use vulkano::sampler::{Filter, MipmapMode, Sampler};
use vulkano::{
buffer::{BufferUsage, CpuAccessibleBuffer},
command_buffer::SecondaryAutoCommandBuffer,
device::Queue,
image::ImageViewAbstract,
pipeline::GraphicsPipeline,
render_pass::Subpass,
sampler::SamplerAddressMode,
};
/// Vertex for textured quads
#[derive(Default, Debug, Clone, Copy)]
pub struct TexturedVertex {
pub position: [f32; 2],
pub tex_coords: [f32; 2],
}
vulkano::impl_vertex!(TexturedVertex, position, tex_coords);
pub fn textured_quad(width: f32, height: f32) -> (Vec<TexturedVertex>, Vec<u32>) {
(
vec![
TexturedVertex {
position: [-(width / 2.0), -(height / 2.0)],
tex_coords: [0.0, 1.0],
},
TexturedVertex {
position: [-(width / 2.0), height / 2.0],
tex_coords: [0.0, 0.0],
},
TexturedVertex {
position: [width / 2.0, height / 2.0],
tex_coords: [1.0, 0.0],
},
TexturedVertex {
position: [width / 2.0, -(height / 2.0)],
tex_coords: [1.0, 1.0],
},
],
vec![0, 2, 1, 0, 3, 2],
)
}
/// A subpass pipeline that fills a quad over frame
pub struct PixelsDrawPipeline {
gfx_queue: Arc<Queue>,
pipeline: Arc<GraphicsPipeline>,
vertices: Arc<CpuAccessibleBuffer<[TexturedVertex]>>,
indices: Arc<CpuAccessibleBuffer<[u32]>>,
}
impl PixelsDrawPipeline {
pub fn new(gfx_queue: Arc<Queue>, subpass: Subpass) -> PixelsDrawPipeline {
let (vertices, indices) = textured_quad(2.0, 2.0);
let vertex_buffer = CpuAccessibleBuffer::<[TexturedVertex]>::from_iter(
gfx_queue.device().clone(),
BufferUsage::vertex_buffer(),
false,
vertices.into_iter(),
)
.unwrap();
let index_buffer = CpuAccessibleBuffer::<[u32]>::from_iter(
gfx_queue.device().clone(),
BufferUsage::index_buffer(),
false,
indices.into_iter(),
)
.unwrap();
let pipeline = {
let vs = vs::Shader::load(gfx_queue.device().clone())
.expect("failed to create shader module");
let fs = fs::Shader::load(gfx_queue.device().clone())
.expect("failed to create shader module");
Arc::new(
GraphicsPipeline::start()
.vertex_input_single_buffer::<TexturedVertex>()
.vertex_shader(vs.main_entry_point(), ())
.triangle_list()
.fragment_shader(fs.main_entry_point(), ())
.viewports_dynamic_scissors_irrelevant(1)
.depth_stencil_disabled()
.render_pass(subpass)
.build(gfx_queue.device().clone())
.unwrap(),
)
};
PixelsDrawPipeline {
gfx_queue,
pipeline,
vertices: vertex_buffer,
indices: index_buffer,
}
}
fn create_descriptor_set(
&self,
image: Arc<dyn ImageViewAbstract + Send + Sync>,
) -> PersistentDescriptorSet {
let layout = self
.pipeline
.layout()
.descriptor_set_layouts()
.get(0)
.unwrap();
let sampler = Sampler::new(
self.gfx_queue.device().clone(),
Filter::Linear,
Filter::Linear,
MipmapMode::Linear,
SamplerAddressMode::Repeat,
SamplerAddressMode::Repeat,
SamplerAddressMode::Repeat,
0.0,
1.0,
0.0,
0.0,
)
.unwrap();
let mut desc_set_builder = PersistentDescriptorSet::start(layout.clone());
desc_set_builder
.add_sampled_image(image.clone(), sampler)
.unwrap();
desc_set_builder.build().unwrap()
}
/// Draw input `image` over a quad of size -1.0 to 1.0
pub fn draw(
&mut self,
viewport_dimensions: [u32; 2],
image: Arc<dyn ImageViewAbstract + Send + Sync>,
) -> SecondaryAutoCommandBuffer {
let mut builder = AutoCommandBufferBuilder::secondary_graphics(
self.gfx_queue.device().clone(),
self.gfx_queue.family(),
CommandBufferUsage::MultipleSubmit,
self.pipeline.subpass().clone(),
)
.unwrap();
let desc_set = self.create_descriptor_set(image);
builder
.set_viewport(
0,
[Viewport {
origin: [0.0, 0.0],
dimensions: [viewport_dimensions[0] as f32, viewport_dimensions[1] as f32],
depth_range: 0.0..1.0,
}],
)
.bind_pipeline_graphics(self.pipeline.clone())
.bind_descriptor_sets(
PipelineBindPoint::Graphics,
self.pipeline.layout().clone(),
0,
desc_set,
)
.bind_vertex_buffers(0, self.vertices.clone())
.bind_index_buffer(self.indices.clone())
.draw_indexed(self.indices.len() as u32, 1, 0, 0, 0)
.unwrap();
builder.build().unwrap()
}
}
mod vs {
vulkano_shaders::shader! {
ty: "vertex",
src: "
#version 450
layout(location=0) in vec2 position;
layout(location=1) in vec2 tex_coords;
layout(location = 0) out vec2 f_tex_coords;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
f_tex_coords = tex_coords;
}
"
}
}
mod fs {
vulkano_shaders::shader! {
ty: "fragment",
src: "
#version 450
layout(location = 0) in vec2 v_tex_coords;
layout(location = 0) out vec4 f_color;
layout(set = 0, binding = 0) uniform sampler2D tex;
void main() {
f_color = texture(tex, v_tex_coords);
}
"
}
}

View File

@ -0,0 +1,109 @@
// Copyright (c) 2021 The vulkano developers
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE or
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT
// license <LICENSE-MIT or https://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
use std::sync::Arc;
use vulkano::{
command_buffer::{AutoCommandBufferBuilder, CommandBufferUsage, SubpassContents},
device::Queue,
format::Format,
render_pass::{Framebuffer, RenderPass, Subpass},
sync::GpuFuture,
};
use crate::pixels_draw_pipeline::PixelsDrawPipeline;
use crate::renderer::{FinalImageView, InterimImageView};
/// A render pass which places an incoming image over frame filling it
pub struct RenderPassPlaceOverFrame {
gfx_queue: Arc<Queue>,
render_pass: Arc<RenderPass>,
pixels_draw_pipeline: PixelsDrawPipeline,
}
impl RenderPassPlaceOverFrame {
pub fn new(gfx_queue: Arc<Queue>, output_format: Format) -> RenderPassPlaceOverFrame {
let render_pass = Arc::new(
vulkano::single_pass_renderpass!(gfx_queue.device().clone(),
attachments: {
color: {
load: Clear,
store: Store,
format: output_format,
samples: 1,
}
},
pass: {
color: [color],
depth_stencil: {}
}
)
.unwrap(),
);
let subpass = Subpass::from(render_pass.clone(), 0).unwrap();
let pixels_draw_pipeline = PixelsDrawPipeline::new(gfx_queue.clone(), subpass);
RenderPassPlaceOverFrame {
gfx_queue,
render_pass,
pixels_draw_pipeline,
}
}
/// Place view exactly over swapchain image target.
/// Texture draw pipeline uses a quad onto which it places the view.
pub fn render<F>(
&mut self,
before_future: F,
view: InterimImageView,
target: FinalImageView,
) -> Box<dyn GpuFuture>
where
F: GpuFuture + 'static,
{
// Get dimensions
let img_dims = target.image().dimensions();
// Create framebuffer (must be in same order as render pass description in `new`
let framebuffer = Arc::new(
Framebuffer::start(self.render_pass.clone())
.add(target)
.unwrap()
.build()
.unwrap(),
);
// Create primary command buffer builder
let mut command_buffer_builder = AutoCommandBufferBuilder::primary(
self.gfx_queue.device().clone(),
self.gfx_queue.family(),
CommandBufferUsage::OneTimeSubmit,
)
.unwrap();
// Begin render pass
command_buffer_builder
.begin_render_pass(
framebuffer,
SubpassContents::SecondaryCommandBuffers,
vec![[0.0; 4].into()],
)
.unwrap();
// Create secondary command buffer from texture pipeline & send draw commands
let cb = self.pixels_draw_pipeline.draw(img_dims, view);
// Execute above commands (subpass)
command_buffer_builder.execute_commands(cb).unwrap();
// End render pass
command_buffer_builder.end_render_pass().unwrap();
// Build command buffer
let command_buffer = command_buffer_builder.build().unwrap();
// Execute primary command buffer
let after_future = before_future
.then_execute(self.gfx_queue.clone(), command_buffer)
.unwrap();
after_future.boxed()
}
}

View File

@ -0,0 +1,481 @@
// Copyright (c) 2021 The vulkano developers
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE or
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT
// license <LICENSE-MIT or https://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
use crate::place_over_frame::RenderPassPlaceOverFrame;
use vulkano_win::VkSurfaceBuild;
use std::collections::HashMap;
use std::sync::Arc;
use vulkano::device::physical::{PhysicalDevice, PhysicalDeviceType};
use vulkano::device::{Device, DeviceExtensions, Features, Queue};
use vulkano::format::Format;
use vulkano::image::view::ImageView;
use vulkano::image::{AttachmentImage, ImageUsage, ImageViewAbstract, SampleCount, SwapchainImage};
use vulkano::instance::debug::DebugCallback;
use vulkano::instance::Instance;
use vulkano::instance::InstanceExtensions;
use vulkano::swapchain::{
AcquireError, ColorSpace, FullscreenExclusive, PresentMode, Surface, SurfaceTransform,
Swapchain, SwapchainCreationError,
};
use vulkano::sync::{FlushError, GpuFuture};
use vulkano::{swapchain, sync, Version};
use winit::event_loop::EventLoop;
use winit::window::{Fullscreen, Window, WindowBuilder};
/// Final render target (swapchain image)
pub type FinalImageView = Arc<ImageView<Arc<SwapchainImage<Window>>>>;
/// Other intermediate render targets
pub type InterimImageView = Arc<ImageView<Arc<AttachmentImage>>>;
/// A simple struct to organize renderpasses.
/// You could add more here. E.g. the `frame_system`
/// from the deferred examples...
pub struct RenderPasses {
pub place_over_frame: RenderPassPlaceOverFrame,
}
#[derive(Debug, Copy, Clone)]
pub struct RenderOptions {
pub title: &'static str,
pub window_size: [u32; 2],
pub v_sync: bool,
}
impl Default for RenderOptions {
fn default() -> Self {
RenderOptions {
title: "App",
window_size: [1920, 1080],
v_sync: false,
}
}
}
pub struct Renderer {
_debug_callback: DebugCallback,
_instance: Arc<Instance>,
device: Arc<Device>,
surface: Arc<Surface<Window>>,
queue: Arc<Queue>,
swap_chain: Arc<Swapchain<Window>>,
image_index: usize,
final_views: Vec<FinalImageView>,
/// Image view that is to be rendered with our pipeline.
/// (bool refers to whether it should get resized with swapchain resize)
interim_image_views: HashMap<usize, (InterimImageView, bool)>,
recreate_swapchain: bool,
previous_frame_end: Option<Box<dyn GpuFuture>>,
render_passes: RenderPasses,
is_fullscreen: bool,
}
impl Renderer {
/// Creates a new GPU renderer for window with given parameters
pub fn new(event_loop: &EventLoop<()>, opts: RenderOptions) -> Self {
println!("Creating renderer for window size {:?}", opts.window_size);
// Add instance extensions based on needs
let instance_extensions = InstanceExtensions {
ext_debug_utils: true,
..vulkano_win::required_extensions()
};
// Create instance
#[cfg(not(target_os = "macos"))]
let layers = vec!["VK_LAYER_LUNARG_standard_validation"];
#[cfg(target_os = "macos")]
let layers = vec!["VK_LAYER_KHRONOS_validation"];
let _instance = Instance::new(None, Version::V1_2, &instance_extensions, layers)
.expect("Failed to create instance");
// Create debug callback for printing vulkan errors and warnings
let _debug_callback = DebugCallback::errors_and_warnings(&_instance, |msg| {
println!(
"{} {:?} {:?}: {}",
msg.layer_prefix.unwrap_or("unknown"),
msg.ty,
msg.severity,
msg.description
);
})
.unwrap();
// Get desired device
let physical_device = PhysicalDevice::enumerate(&_instance)
.min_by_key(|p| match p.properties().device_type {
PhysicalDeviceType::DiscreteGpu => 0,
PhysicalDeviceType::IntegratedGpu => 1,
PhysicalDeviceType::VirtualGpu => 2,
PhysicalDeviceType::Cpu => 3,
PhysicalDeviceType::Other => 4,
})
.unwrap();
println!("Using device {}", physical_device.properties().device_name);
// Create rendering surface along with window
let surface = WindowBuilder::new()
.with_inner_size(winit::dpi::LogicalSize::new(
opts.window_size[0],
opts.window_size[1],
))
.with_title(opts.title)
.build_vk_surface(event_loop, _instance.clone())
.unwrap();
println!("Window scale factor {}", surface.window().scale_factor());
// Create device
let (device, queue) = Self::create_device(physical_device, surface.clone());
// Create swap chain & frame(s) to which we'll render
let (swap_chain, final_images) = Self::create_swap_chain(
surface.clone(),
physical_device,
device.clone(),
queue.clone(),
if opts.v_sync {
PresentMode::Fifo
} else {
PresentMode::Immediate
},
);
let previous_frame_end = Some(sync::now(device.clone()).boxed());
let is_fullscreen = swap_chain.surface().window().fullscreen().is_some();
let image_format = final_images.first().unwrap().format();
let render_passes = RenderPasses {
place_over_frame: RenderPassPlaceOverFrame::new(queue.clone(), image_format),
};
Renderer {
_debug_callback,
_instance,
device,
surface,
queue,
swap_chain,
image_index: 0,
final_views: final_images,
interim_image_views: HashMap::new(),
previous_frame_end,
recreate_swapchain: false,
render_passes,
is_fullscreen,
}
}
/// Creates vulkan device with required queue families and required extensions
fn create_device(
physical: PhysicalDevice,
surface: Arc<Surface<Window>>,
) -> (Arc<Device>, Arc<Queue>) {
let queue_family = physical
.queue_families()
.find(|&q| q.supports_graphics() && surface.is_supported(q).unwrap_or(false))
.unwrap();
// Add device extensions based on needs,
let device_extensions = DeviceExtensions {
khr_swapchain: true,
..DeviceExtensions::none()
};
// Add device features
let features = Features {
fill_mode_non_solid: true,
..Features::none()
};
let (device, mut queues) = {
Device::new(
physical,
&features,
&physical.required_extensions().union(&device_extensions),
[(queue_family, 0.5)].iter().cloned(),
)
.unwrap()
};
(device, queues.next().unwrap())
}
/// Creates swapchain and swapchain images
fn create_swap_chain(
surface: Arc<Surface<Window>>,
physical: PhysicalDevice,
device: Arc<Device>,
queue: Arc<Queue>,
present_mode: PresentMode,
) -> (Arc<Swapchain<Window>>, Vec<FinalImageView>) {
let caps = surface.capabilities(physical).unwrap();
let alpha = caps.supported_composite_alpha.iter().next().unwrap();
let format = caps.supported_formats[0].0;
let dimensions: [u32; 2] = surface.window().inner_size().into();
let (swap_chain, images) = Swapchain::start(device, surface)
.num_images(caps.min_image_count)
.format(format)
.dimensions(dimensions)
.usage(ImageUsage::color_attachment())
.sharing_mode(&queue)
.composite_alpha(alpha)
.transform(SurfaceTransform::Identity)
.present_mode(present_mode)
.fullscreen_exclusive(FullscreenExclusive::Default)
.clipped(true)
.color_space(ColorSpace::SrgbNonLinear)
.layers(1)
.build()
.unwrap();
let images = images
.into_iter()
.map(|image| ImageView::new(image).unwrap())
.collect::<Vec<_>>();
(swap_chain, images)
}
/// Return default image format for images (swapchain format may differ)
pub fn image_format(&self) -> Format {
Format::R8G8B8A8_UNORM
}
/// Return swapchain image format
#[allow(unused)]
pub fn swapchain_format(&self) -> Format {
self.final_views[self.image_index].format()
}
/// Returns the index of last swapchain image that is the next render target
/// All camera views will render onto their image at the same index
#[allow(unused)]
pub fn image_index(&self) -> usize {
self.image_index
}
/// Access device
pub fn device(&self) -> Arc<Device> {
self.device.clone()
}
/// Access rendering queue
pub fn queue(&self) -> Arc<Queue> {
self.queue.clone()
}
/// Render target surface
#[allow(unused)]
pub fn surface(&self) -> Arc<Surface<Window>> {
self.surface.clone()
}
/// Winit window
pub fn window(&self) -> &Window {
self.surface.window()
}
/// Winit window size
#[allow(unused)]
pub fn window_size(&self) -> [u32; 2] {
let size = self.window().inner_size();
[size.width, size.height]
}
/// Size of the final swapchain image (surface)
pub fn final_image_size(&self) -> [u32; 2] {
self.final_views[0].image().dimensions().width_height()
}
/// Return final image which can be used as a render pipeline target
pub fn final_image(&self) -> FinalImageView {
self.final_views[self.image_index].clone()
}
/// Return scale factor accounted window size
#[allow(unused)]
pub fn resolution(&self) -> [u32; 2] {
let size = self.window().inner_size();
let scale_factor = self.window().scale_factor();
[
(size.width as f64 / scale_factor) as u32,
(size.height as f64 / scale_factor) as u32,
]
}
/// Add interim image view that can be used as a render target.
pub fn add_interim_image_view(
&mut self,
key: usize,
view_size: Option<[u32; 2]>,
format: Format,
) {
let image = ImageView::new(
AttachmentImage::multisampled_with_usage(
self.device(),
if view_size.is_some() {
view_size.unwrap()
} else {
self.final_image_size()
},
SampleCount::Sample1,
format,
ImageUsage {
sampled: true,
// So we can use the image as an input attachment
input_attachment: true,
// So we can write to the image in e.g. a compute shader
storage: true,
..ImageUsage::none()
},
)
.unwrap(),
)
.unwrap();
self.interim_image_views
.insert(key, (image.clone(), !view_size.is_some()));
}
/// Get interim image view by key
pub fn get_interim_image_view(&mut self, key: usize) -> InterimImageView {
self.interim_image_views.get(&key).unwrap().clone().0
}
/// Remove an interim image view from the renderer
pub fn remove_interim_image_view(&mut self, key: usize) {
self.interim_image_views.remove(&key);
}
/// Toggles fullscreen view
pub fn toggle_fullscreen(&mut self) {
self.is_fullscreen = !self.is_fullscreen;
self.window().set_fullscreen(if self.is_fullscreen {
Some(Fullscreen::Borderless(self.window().current_monitor()))
} else {
None
});
}
/// Resize swapchain and camera view images
pub fn resize(&mut self) {
self.recreate_swapchain = true
}
/*================
RENDERING
=================*/
/// Acquires next swapchain image and increments image index
/// This is the first to call in render orchestration.
/// Returns a gpu future representing the time after which the swapchain image has been acquired
/// and previous frame ended.
/// After this, execute command buffers and return future from them to `finish_frame`.
pub(crate) fn start_frame(&mut self) -> Result<Box<dyn GpuFuture>, AcquireError> {
// Recreate swap chain if needed (when resizing of window occurs or swapchain is outdated)
// Also resize render views if needed
if self.recreate_swapchain {
self.recreate_swapchain_and_views();
}
// Acquire next image in the swapchain
let (image_num, suboptimal, acquire_future) =
match swapchain::acquire_next_image(self.swap_chain.clone(), None) {
Ok(r) => r,
Err(AcquireError::OutOfDate) => {
self.recreate_swapchain = true;
return Err(AcquireError::OutOfDate);
}
Err(e) => panic!("Failed to acquire next image: {:?}", e),
};
if suboptimal {
self.recreate_swapchain = true;
}
// Update our image index
self.image_index = image_num;
let future = self.previous_frame_end.take().unwrap().join(acquire_future);
Ok(future.boxed())
}
/// Finishes render by presenting the swapchain
pub(crate) fn finish_frame(&mut self, after_future: Box<dyn GpuFuture>) {
let future = after_future
.then_swapchain_present(
self.queue.clone(),
self.swap_chain.clone(),
self.image_index,
)
.then_signal_fence_and_flush();
match future {
Ok(future) => {
// Prevent OutOfMemory error on Nvidia :(
// https://github.com/vulkano-rs/vulkano/issues/627
match future.wait(None) {
Ok(x) => x,
Err(err) => println!("{:?}", err),
}
self.previous_frame_end = Some(future.boxed());
}
Err(FlushError::OutOfDate) => {
self.recreate_swapchain = true;
self.previous_frame_end = Some(sync::now(self.device.clone()).boxed());
}
Err(e) => {
println!("Failed to flush future: {:?}", e);
self.previous_frame_end = Some(sync::now(self.device.clone()).boxed());
}
}
}
/// Swapchain is recreated when resized. Interim image views that should follow swapchain
/// are also recreated
fn recreate_swapchain_and_views(&mut self) {
let dimensions: [u32; 2] = self.window().inner_size().into();
let (new_swapchain, new_images) =
match self.swap_chain.recreate().dimensions(dimensions).build() {
Ok(r) => r,
Err(SwapchainCreationError::UnsupportedDimensions) => {
println!(
"{}",
SwapchainCreationError::UnsupportedDimensions.to_string()
);
return;
}
Err(e) => panic!("Failed to recreate swapchain: {:?}", e),
};
self.swap_chain = new_swapchain;
let new_images = new_images
.into_iter()
.map(|image| ImageView::new(image).unwrap())
.collect::<Vec<_>>();
self.final_views = new_images;
// Resize images that follow swapchain size
let resizable_views = self
.interim_image_views
.iter()
.filter(|(_, (_img, follow_swapchain))| *follow_swapchain)
.map(|c| *c.0)
.collect::<Vec<usize>>();
for i in resizable_views {
self.remove_interim_image_view(i);
self.add_interim_image_view(i, None, self.image_format());
}
self.recreate_swapchain = false;
}
}
/// Between `start_frame` and `end_frame` use this pipeline to fill framebuffer with your interim image
pub fn image_over_frame_renderpass<F>(
renderer: &mut Renderer,
before_pipeline_future: F,
image: InterimImageView,
) -> Box<dyn GpuFuture>
where
F: GpuFuture + 'static,
{
renderer
.render_passes
.place_over_frame
.render(before_pipeline_future, image, renderer.final_image())
.then_signal_fence_and_flush()
.unwrap()
.boxed()
}