use clap::{App, Arg};
use futures::executor::block_on;
use log::error;
use winit::{
event::*,
event_loop::{ControlFlow, EventLoop},
window::WindowBuilder,
};
mod canvas;
mod dashboard;
mod postprocessing;
mod push_constants;
mod skeletons;
mod texture;
mod uniforms;
mod utils;
mod vector;
use crate::canvas::message::CanvasMessage;
use crate::dashboard::{Dashboard, DashboardMessage};
use canvas::Canvas;
use std::cmp::max;
use std::fs;
use std::path::Path;
use std::sync::mpsc::channel;
use winit::dpi::PhysicalSize;
fn main() {
env_logger::init();
let matches = setup_program_args();
if let Some(shader_file) = matches.value_of("shader") {
if matches.is_present("generate") {
let path = std::path::Path::new(shader_file);
if path.exists() {
error!(
"There is already a file present at {}, canceling write.",
shader_file
);
return;
}
std::fs::write(&path, skeletons::SHADER_SKELETON).unwrap();
}
let fs_spv_data = match utils::load_shader(shader_file) {
Ok(data) => data,
Err(e) => {
error!("Error compiling/loading shader: {}", e);
return;
}
};
let mut images_to_load: Vec<String> = Vec::new();
if let Some(files) = matches.values_of("textures") {
for a_file in files {
images_to_load.push(String::from(a_file));
}
}
let mut canvas_width = 1920;
let mut canvas_height = 1280;
if let Some(width) = matches.value_of("width") {
canvas_width = width.parse::<i32>().unwrap()
}
if let Some(height) = matches.value_of("height") {
canvas_height = height.parse::<i32>().unwrap()
}
let mut custom_uniforms = None;
let mut push_constants = None;
if let Some(uniforms_file) = matches.value_of("uniforms") {
let text =
fs::read_to_string(uniforms_file).expect("Error reading uniforms from file.");
let json_data = json::parse(&text).expect("Error parsing JSON.");
let cu = uniforms::load_uniforms_from_json(&json_data);
if !cu.is_empty() {
custom_uniforms = Some(cu);
}
let pc = push_constants::load_push_constants_from_json(&json_data);
if !pc.is_empty() {
push_constants = Some(pc);
}
}
let event_loop = EventLoop::new();
let render_window = WindowBuilder::new().build(&event_loop).unwrap();
render_window.set_title("Canvas");
render_window.set_inner_size(PhysicalSize::new(canvas_width, canvas_height));
render_window.set_decorations(true);
render_window.set_resizable(true);
let mut images: Vec<image::DynamicImage> = Vec::new();
for a_file in &images_to_load {
let an_image = image::open(Path::new(a_file));
match an_image {
Ok(img) => images.push(img),
Err(error) => {
error!("Error loading image: {}", error);
return;
}
}
}
let (dashboard_tx, state_rx) = channel::<DashboardMessage>();
let (state_tx, dashboard_rx) = channel::<CanvasMessage>();
let mut canvas = block_on(Canvas::new(
render_window,
fs_spv_data,
Some(images),
custom_uniforms,
push_constants,
state_tx,
state_rx,
));
if let Some(postprocess_shaders) = matches.values_of("postprocess") {
let mut postprocess_shader_modules = Vec::with_capacity(postprocess_shaders.len());
for shader in postprocess_shaders {
postprocess_shader_modules.push(utils::load_shader(shader).unwrap());
}
for module in postprocess_shader_modules {
canvas.add_post_processing_shader(module);
}
}
if let Some(interval_str) = matches.value_of("auto-update") {
let interval = max(
interval_str
.parse::<u64>()
.expect("Invalid update interval provided. Must be integer"),
80,
);
canvas.watch_shader_file(shader_file, interval);
if let Some(uniforms_file) = matches.value_of("uniforms") {
canvas.watch_uniforms_file(uniforms_file, interval);
}
}
let dashboard_window_builder = WindowBuilder::new().with_resizable(true);
let dashboard_window = dashboard_window_builder.build(&event_loop).unwrap();
dashboard_window.set_title("Dashboard");
dashboard_window.set_inner_size(PhysicalSize::new(500, 900));
let mut dashboard = block_on(Dashboard::new(dashboard_window, dashboard_tx, dashboard_rx));
event_loop.run(move |event, _, control_flow| {
dashboard.input(&event);
match event {
Event::RedrawRequested(_) => {
canvas.update();
canvas.render_canvas();
canvas.post_render();
dashboard.update();
dashboard.render_dashboard();
dashboard.post_render();
}
Event::MainEventsCleared => {
canvas.window.request_redraw();
dashboard.frame_tick();
}
Event::WindowEvent {
ref event,
window_id,
} => {
let mut handled = false;
if window_id == canvas.window.id() {
handled = canvas.input(event);
}
if !handled {
match event {
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
WindowEvent::KeyboardInput { input, .. } => match input {
KeyboardInput {
state: ElementState::Pressed,
virtual_keycode: Some(VirtualKeyCode::Escape),
..
} => {
canvas.exit_requested();
*control_flow = ControlFlow::Exit
}
_ => {}
},
_ => {}
}
}
}
_ => {}
}
});
} else {
error!("Please provide a fragment shader.")
}
}
fn setup_program_args() -> clap::ArgMatches {
App::new("Easel")
.version("1.0.1")
.author("Siddharth A. <sid.atre@me.com>")
.arg(
Arg::new("shader")
.about("The fragment shader to use.")
.index(1)
.required(true),
)
.arg(
Arg::new("textures")
.long_about("List of images to load. Textures are bound to the shader Set 1 in the order specified here.")
.required(false)
.takes_value(true)
.short('t')
.long("textures")
.multiple(true)
)
.arg(
Arg::new("width")
.about("Width of canvas")
.required(false)
.takes_value(true)
.short('w')
.long("width")
.default_value("1920")
)
.arg(
Arg::new("height")
.about("Height of canvas")
.required(false)
.takes_value(true)
.short('h')
.long("height")
.default_value("1280")
)
.arg(
Arg::new("auto-update")
.long_about("Check the shader and/or uniforms files on this interval (ms). If changed, updates render pipelines. Default is the minimum.")
.required(false)
.takes_value(true)
.short('a')
.default_value("80")
.long("auto-update")
)
.arg(
Arg::new("uniforms")
.long_about("Provide a JSON file with custom uniforms. Uniforms are bound in Set 2 in the order provided.")
.required(false)
.takes_value(true)
.short('u')
.long("uniforms")
)
.arg(Arg::new("postprocess")
.long_about("Provided a shader to run after main fragment shader. Multiple can be provided. Postprocessing operations are applied in the order given here.")
.required(false)
.takes_value(true)
.multiple(true)
.short('p')
.long("postprocess"))
.arg(Arg::new("generate")
.long_about("Generate a basic skeleton for an Easel shader. The shader is written to disk and then loaded.")
.required(false)
.short('g')
.long("generate")
)
.get_matches()
}