mirror of
https://github.com/vulkano-rs/vulkano.git
synced 2024-11-25 16:25:31 +00:00
Add an interactive fractal example (#1707)
* Add interactive fractal example * Rename folder * Update guide
This commit is contained in:
parent
54cb28d4e5
commit
e0839af295
@ -22,3 +22,5 @@ png = "0.17"
|
||||
time = "0.3"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
ron = "0.6"
|
||||
rand = "0.8.4"
|
||||
|
||||
|
318
examples/src/bin/interactive_fractal/app.rs
Normal file
318
examples/src/bin/interactive_fractal/app.rs
Normal 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),
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
}
|
239
examples/src/bin/interactive_fractal/fractal_compute_pipeline.rs
Normal file
239
examples/src/bin/interactive_fractal/fractal_compute_pipeline.rs
Normal 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);
|
||||
}"
|
||||
}
|
||||
}
|
125
examples/src/bin/interactive_fractal/main.rs
Normal file
125
examples/src/bin/interactive_fractal/main.rs
Normal 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);
|
||||
}
|
215
examples/src/bin/interactive_fractal/pixels_draw_pipeline.rs
Normal file
215
examples/src/bin/interactive_fractal/pixels_draw_pipeline.rs
Normal 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);
|
||||
}
|
||||
"
|
||||
}
|
||||
}
|
109
examples/src/bin/interactive_fractal/place_over_frame.rs
Normal file
109
examples/src/bin/interactive_fractal/place_over_frame.rs
Normal 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()
|
||||
}
|
||||
}
|
481
examples/src/bin/interactive_fractal/renderer.rs
Normal file
481
examples/src/bin/interactive_fractal/renderer.rs
Normal 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()
|
||||
}
|
Loading…
Reference in New Issue
Block a user