Add live-reload for shaders

Recompiling the entire program whenever a shader changes is slow, and it
can interrupt flow. Shader reloads are essentially instantaneous now. If
the new shader fails to compile, no state is changed; the previous
program continues to be used.
This commit is contained in:
Joe Wilm 2016-06-04 10:54:33 -07:00
parent c475c82c69
commit f944b517fa
No known key found for this signature in database
GPG Key ID: 39B57C6972F518DA
4 changed files with 342 additions and 50 deletions

150
Cargo.lock generated
View File

@ -8,6 +8,7 @@ dependencies = [
"gl_generator 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"glutin 0.4.9 (git+https://github.com/jwilm/glutin?rev=c95e6973ace3cbf321123a64588b27f032675be9)",
"libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"notify 2.5.5 (git+https://github.com/jwilm/rsnotify?branch=add-ignore-op)",
"servo-fontconfig 0.2.0 (git+https://github.com/jwilm/rust-fontconfig)",
]
@ -21,6 +22,11 @@ name = "bitflags"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "bitflags"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "bitflags"
version = "0.6.0"
@ -36,6 +42,16 @@ name = "byteorder"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "bytes"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "cfg-if"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "cgl"
version = "0.1.5"
@ -142,6 +158,14 @@ dependencies = [
"pkg-config 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "filetime"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "freetype-rs"
version = "0.9.0"
@ -172,6 +196,24 @@ dependencies = [
"winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "fsevent"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bitflags 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"fsevent-sys 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "fsevent-sys"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "gcc"
version = "0.3.28"
@ -242,6 +284,14 @@ dependencies = [
"x11-dl 2.4.0 (git+https://github.com/jwilm/x11-rs?rev=40f08df7b4408980b922b3c6e258c9c6765c2c24)",
]
[[package]]
name = "inotify"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "kernel32-sys"
version = "0.2.2"
@ -320,6 +370,73 @@ dependencies = [
"winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "mio"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"miow 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"net2 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
"nix 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"slab 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "miow"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"net2 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
"ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "net2"
version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cfg-if 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
"ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "nix"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bitflags 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "notify"
version = "2.5.5"
source = "git+https://github.com/jwilm/rsnotify?branch=add-ignore-op#0ca41a4807c427e6cf47d7e75735df62d2e86708"
dependencies = [
"bitflags 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"filetime 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"fsevent 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
"fsevent-sys 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"inotify 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"mio 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
"walkdir 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "num"
version = "0.1.32"
@ -477,6 +594,11 @@ dependencies = [
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "slab"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "tempfile"
version = "2.1.3"
@ -489,6 +611,16 @@ dependencies = [
"winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "time"
version = "0.1.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "user32-sys"
version = "0.1.2"
@ -498,6 +630,15 @@ dependencies = [
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "walkdir"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "wayland-client"
version = "0.5.12"
@ -560,6 +701,15 @@ name = "winapi-build"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "ws2_32-sys"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "x11-dl"
version = "2.4.0"

View File

@ -12,6 +12,7 @@ freetype-rs = "0.9.0"
libc = "*"
cgmath = "0.7"
euclid = "0.6"
notify = { git = "https://github.com/jwilm/rsnotify", branch = "add-ignore-op" }
[build-dependencies]
gl_generator = "0.5"

View File

@ -11,6 +11,7 @@ extern crate libc;
extern crate glutin;
extern crate cgmath;
extern crate euclid;
extern crate notify;
#[macro_use]
mod macros;
@ -61,6 +62,7 @@ struct TermProps {
cell_height: f32,
sep_y: f32,
height: f32,
width: f32,
}
fn main() {
@ -178,17 +180,18 @@ fn main() {
gl::Clear(gl::COLOR_BUFFER_BIT);
}
let props = TermProps {
cell_width: cell_width as f32,
sep_x: sep_x as f32,
cell_height: cell_height as f32,
sep_y: sep_y as f32,
height: height as f32,
width: width as f32,
};
{
let _sampler = meter.sampler();
let props = TermProps {
cell_width: cell_width as f32,
sep_x: sep_x as f32,
cell_height: cell_height as f32,
sep_y: sep_y as f32,
height: height as f32,
};
// Draw the grid
renderer.render_grid(terminal.grid(), &glyph_cache, &props);
@ -199,7 +202,7 @@ fn main() {
// Draw render timer
let timing = format!("{:.3} usec", meter.average());
let color = Rgb { r: 0xd5, g: 0x4e, b: 0x53 };
renderer.render_string(&timing[..], &glyph_cache, cell_width, &color);
renderer.render_string(&timing[..], &glyph_cache, &props, &color);
window.swap_buffers().unwrap();
}

View File

@ -1,23 +1,32 @@
use std::ffi::CString;
use std::fs::File;
use std::io::{self, Read};
use std::mem::size_of;
use std::path::{PathBuf, Path};
use std::ptr;
use std::sync::Arc;
use std::sync::atomic::{Ordering, AtomicBool};
use cgmath::{self, Matrix};
use euclid::{Rect, Size2D, Point2D};
use gl::types::*;
use gl;
use notify::{Watcher as WatcherApi, RecommendedWatcher as Watcher, op};
use text::RasterizedGlyph;
use grid::Grid;
use term;
use super::{Rgb, TermProps, GlyphCache};
static TEXT_SHADER_V: &'static str = include_str!("../../res/text.v.glsl");
static TEXT_SHADER_F: &'static str = include_str!("../../res/text.f.glsl");
// static TEXT_SHADER_V: &'static str = include_str!("../../res/text.v.glsl");
// static TEXT_SHADER_F: &'static str = include_str!("../../res/text.f.glsl");
static TEXT_SHADER_F_PATH: &'static str = concat!(env!("CARGO_MANIFEST_DIR"), "/res/text.f.glsl");
static TEXT_SHADER_V_PATH: &'static str = concat!(env!("CARGO_MANIFEST_DIR"), "/res/text.v.glsl");
pub struct QuadRenderer {
program: ShaderProgram,
should_reload: Arc<AtomicBool>,
vao: GLuint,
vbo: GLuint,
ebo: GLuint,
@ -39,7 +48,7 @@ pub struct PackedVertex {
impl QuadRenderer {
// TODO should probably hand this a transform instead of width/height
pub fn new(width: u32, height: u32) -> QuadRenderer {
let program = ShaderProgram::new(width, height);
let program = ShaderProgram::new(width, height).unwrap();
let mut vao: GLuint = 0;
let mut vbo: GLuint = 0;
@ -88,8 +97,41 @@ impl QuadRenderer {
gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, 0);
}
let should_reload = Arc::new(AtomicBool::new(false));
let should_reload2 = should_reload.clone();
::std::thread::spawn(move || {
let (tx, rx) = ::std::sync::mpsc::channel();
let mut watcher = Watcher::new(tx).unwrap();
watcher.watch(TEXT_SHADER_F_PATH).expect("watch fragment shader");
watcher.watch(TEXT_SHADER_V_PATH).expect("watch vertex shader");
loop {
let event = rx.recv().expect("watcher event");
let ::notify::Event { path, op } = event;
if let Ok(op) = op {
if op.contains(op::RENAME) {
continue;
}
if op.contains(op::IGNORED) {
if let Some(path) = path.as_ref() {
if let Err(err) = watcher.watch(path) {
println!("failed to establish watch on {:?}: {:?}", path, err);
}
}
// This is last event we see after saving in vim
should_reload2.store(true, Ordering::Relaxed);
}
}
}
});
let mut renderer = QuadRenderer {
program: program,
should_reload: should_reload,
vao: vao,
vbo: vbo,
ebo: ebo,
@ -109,10 +151,10 @@ impl QuadRenderer {
pub fn render_string(&mut self,
s: &str,
glyph_cache: &GlyphCache,
cell_width: u32,
props: &TermProps,
color: &Rgb)
{
self.prepare_render();
self.prepare_render(props);
let (mut x, mut y) = (800f32, 20f32);
@ -121,7 +163,7 @@ impl QuadRenderer {
self.render(glyph, x, y, color);
}
x += cell_width as f32 + 2f32;
x += props.cell_width as f32 + 2f32;
}
self.finish_render();
@ -132,7 +174,7 @@ impl QuadRenderer {
glyph_cache: &GlyphCache,
props: &TermProps)
{
self.prepare_render();
self.prepare_render(props);
if let Some(glyph) = glyph_cache.get(&term::CURSOR_SHAPE) {
let y = (props.cell_height + props.sep_y) * (cursor.y as f32);
let x = (props.cell_width + props.sep_x) * (cursor.x as f32);
@ -146,7 +188,7 @@ impl QuadRenderer {
}
pub fn render_grid(&mut self, grid: &Grid, glyph_cache: &GlyphCache, props: &TermProps) {
self.prepare_render();
self.prepare_render(props);
for i in 0..grid.rows() {
let row = &grid[i];
for j in 0..row.cols() {
@ -166,7 +208,7 @@ impl QuadRenderer {
self.finish_render();
}
fn prepare_render(&self) {
fn prepare_render(&mut self, props: &TermProps) {
unsafe {
self.program.activate();
@ -175,6 +217,10 @@ impl QuadRenderer {
gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, self.ebo);
gl::ActiveTexture(gl::TEXTURE0);
}
if self.should_reload.load(Ordering::Relaxed) {
self.reload_shaders(props.width as u32, props.height as u32);
}
}
fn finish_render(&mut self) {
@ -187,6 +233,30 @@ impl QuadRenderer {
}
}
pub fn reload_shaders(&mut self, width: u32, height: u32) {
self.should_reload.store(false, Ordering::Relaxed);
let program = match ShaderProgram::new(width, height) {
Ok(program) => program,
Err(err) => {
match err {
ShaderCreationError::Io(err) => {
println!("Error reading shader file: {}", err);
},
ShaderCreationError::Compile(path, log) => {
println!("Error compiling shader at {:?}", path);
io::copy(&mut log.as_bytes(), &mut io::stdout()).unwrap();
}
}
return;
}
};
self.active_tex = 0;
self.active_color = Rgb { r: 0, g: 0, b: 0 };
self.program = program;
}
fn render(&mut self, glyph: &Glyph, x: f32, y: f32, color: &Rgb) {
if &self.active_color != color {
unsafe {
@ -308,9 +378,10 @@ impl ShaderProgram {
}
}
pub fn new(width: u32, height: u32) -> ShaderProgram {
let vertex_shader = ShaderProgram::create_vertex_shader();
let fragment_shader = ShaderProgram::create_fragment_shader();
pub fn new(width: u32, height: u32) -> Result<ShaderProgram, ShaderCreationError> {
let vertex_shader = ShaderProgram::create_shader(TEXT_SHADER_V_PATH, gl::VERTEX_SHADER)?;
let fragment_shader = ShaderProgram::create_shader(TEXT_SHADER_F_PATH,
gl::FRAGMENT_SHADER)?;
let program = ShaderProgram::create_program(vertex_shader, fragment_shader);
unsafe {
@ -358,11 +429,10 @@ impl ShaderProgram {
}
shader.deactivate();
shader
Ok(shader)
}
fn create_program(vertex: GLuint, fragment: GLuint) -> GLuint {
unsafe {
let program = gl::CreateProgram();
gl::AttachShader(program, vertex);
@ -379,41 +449,109 @@ impl ShaderProgram {
}
}
fn create_fragment_shader() -> GLuint {
fn create_shader(path: &str, kind: GLenum) -> Result<GLuint, ShaderCreationError> {
let source = CString::new(read_file(path)?).unwrap();
let shader = unsafe {
let shader = gl::CreateShader(kind);
gl::ShaderSource(shader, 1, &source.as_ptr(), ptr::null());
gl::CompileShader(shader);
shader
};
let mut success: GLint = 0;
unsafe {
let fragment_shader = gl::CreateShader(gl::FRAGMENT_SHADER);
let fragment_source = CString::new(TEXT_SHADER_F).unwrap();
gl::ShaderSource(fragment_shader, 1, &fragment_source.as_ptr(), ptr::null());
gl::CompileShader(fragment_shader);
let mut success: GLint = 0;
gl::GetShaderiv(fragment_shader, gl::COMPILE_STATUS, &mut success);
if success != (gl::TRUE as GLint) {
panic!("failed to compiler fragment shader");
}
fragment_shader
gl::GetShaderiv(shader, gl::COMPILE_STATUS, &mut success);
}
}
fn create_vertex_shader() -> GLuint {
unsafe {
let vertex_shader = gl::CreateShader(gl::VERTEX_SHADER);
let vertex_source = CString::new(TEXT_SHADER_V).unwrap();
gl::ShaderSource(vertex_shader, 1, &vertex_source.as_ptr(), ptr::null());
gl::CompileShader(vertex_shader);
if success == (gl::TRUE as GLint) {
Ok(shader)
} else {
// Read log
let log = get_shader_info_log(shader);
let mut success: GLint = 0;
gl::GetShaderiv(vertex_shader, gl::COMPILE_STATUS, &mut success);
// Cleanup
unsafe { gl::DeleteShader(shader); }
if success != (gl::TRUE as GLint) {
panic!("failed to compiler vertex shader");
}
vertex_shader
Err(ShaderCreationError::Compile(PathBuf::from(path), log))
}
}
}
fn get_shader_info_log(shader: GLuint) -> String {
// Get expected log length
let mut max_length: GLint = 0;
unsafe {
gl::GetShaderiv(shader, gl::INFO_LOG_LENGTH, &mut max_length);
}
// Read the info log
let mut actual_length: GLint = 0;
let mut buf: Vec<u8> = Vec::with_capacity(max_length as usize);
unsafe {
gl::GetShaderInfoLog(shader, max_length, &mut actual_length, buf.as_mut_ptr() as *mut _);
}
// Build a string
unsafe {
buf.set_len(actual_length as usize);
}
// XXX should we expect opengl to return garbage?
String::from_utf8(buf).unwrap()
}
fn read_file(path: &str) -> Result<String, io::Error> {
let mut f = File::open(path)?;
let mut buf = String::new();
f.read_to_string(&mut buf)?;
Ok(buf)
}
#[derive(Debug)]
pub enum ShaderCreationError {
/// Error reading file
Io(io::Error),
/// Error compiling shader
Compile(PathBuf, String),
}
impl ::std::error::Error for ShaderCreationError {
fn cause(&self) -> Option<&::std::error::Error> {
match *self {
ShaderCreationError::Io(ref err) => Some(err),
ShaderCreationError::Compile(_, _) => None,
}
}
fn description(&self) -> &str {
match *self {
ShaderCreationError::Io(ref err) => err.description(),
ShaderCreationError::Compile(ref _path, ref s) => s.as_str(),
}
}
}
impl ::std::fmt::Display for ShaderCreationError {
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
match *self {
ShaderCreationError::Io(ref err) => write!(f, "Error creating shader: {}", err),
ShaderCreationError::Compile(ref _path, ref s) => {
write!(f, "Error compiling shader: {}", s)
},
}
}
}
impl From<io::Error> for ShaderCreationError {
fn from(val: io::Error) -> ShaderCreationError {
ShaderCreationError::Io(val)
}
}
/// Manages a single texture atlas
///
/// The strategy for filling an atlas looks roughly like this: