diff --git a/res/text.f.glsl b/res/text.f.glsl index d3265712..d2defb39 100644 --- a/res/text.f.glsl +++ b/res/text.f.glsl @@ -1,17 +1,14 @@ #version 330 core in vec2 TexCoords; -flat in int InstanceId; +in vec3 fg; layout(location = 0, index = 0) out vec4 color; layout(location = 0, index = 1) out vec4 alphaMask; uniform sampler2D mask; -uniform ivec3 textColor[32]; void main() { - int i = InstanceId; alphaMask = vec4(texture(mask, TexCoords).rgb, 1.0); - vec3 textColorF = vec3(textColor[i]) / vec3(255.0, 255.0, 255.0); - color = vec4(textColorF, 1.0); + color = vec4(fg, 1.0); } diff --git a/res/text.v.glsl b/res/text.v.glsl index 66bfad0c..276b1dc3 100644 --- a/res/text.v.glsl +++ b/res/text.v.glsl @@ -1,46 +1,51 @@ #version 330 core layout (location = 0) in vec2 position; +// Cell properties +layout (location = 1) in vec2 gridCoords; + +// glyph properties +layout (location = 2) in vec4 glyph; + +// uv mapping +layout (location = 3) in vec4 uv; + +// text fg color +layout (location = 4) in vec3 textColor; + out vec2 TexCoords; -flat out int InstanceId; +out vec3 fg; // Terminal properties uniform vec2 termDim; uniform vec2 cellDim; uniform vec2 cellSep; -// Cell properties -uniform vec2 gridCoords[32]; - -// glyph properties -uniform vec2 glyphScale[32]; -uniform vec2 glyphOffset[32]; - -// uv mapping -uniform vec2 uvScale[32]; -uniform vec2 uvOffset[32]; - // Orthographic projection uniform mat4 projection; void main() { - int i = gl_InstanceID; + vec2 glyphOffset = glyph.xy; + vec2 glyphSize = glyph.zw; + + vec2 uvOffset = uv.xy; + vec2 uvSize = uv.zw; // Position of cell from top-left - vec2 cellPosition = (cellDim + cellSep) * gridCoords[i]; + vec2 cellPosition = (cellDim + cellSep) * gridCoords; // Invert Y since framebuffer origin is bottom-left cellPosition.y = termDim.y - cellPosition.y - cellDim.y; // Glyphs are offset within their cell; account for y-flip - vec2 cellOffset = vec2(glyphOffset[i].x, - glyphOffset[i].y - glyphScale[i].y); + vec2 cellOffset = vec2(glyphOffset.x, + glyphOffset.y - glyphSize.y); // position coordinates are normalized on [0, 1] - vec2 finalPosition = glyphScale[i] * position + cellPosition + cellOffset; + vec2 finalPosition = glyphSize * position + cellPosition + cellOffset; gl_Position = projection * vec4(finalPosition.xy, 0.0, 1.0); - TexCoords = vec2(position.x, 1 - position.y) * uvScale[i] + uvOffset[i]; - InstanceId = i; + TexCoords = uvOffset + vec2(position.x, 1 - position.y) * uvSize; + fg = textColor / vec3(255.0, 255.0, 255.0); } diff --git a/src/main.rs b/src/main.rs index 79c368df..e9817331 100644 --- a/src/main.rs +++ b/src/main.rs @@ -194,17 +194,21 @@ fn main() { { let _sampler = meter.sampler(); - // Draw the grid - renderer.render_grid(terminal.grid(), &glyph_cache, &props); + renderer.with_api(&props, |mut api| { + // Draw the grid + api.render_grid(terminal.grid(), &glyph_cache); - // Also draw the cursor - renderer.render_cursor(terminal.cursor(), &glyph_cache, &props); + // Also draw the cursor + api.render_cursor(terminal.cursor(), &glyph_cache); + }) } // 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, &props, &color); + renderer.with_api(&props, |mut api| { + api.render_string(&timing[..], &glyph_cache, &color); + }); window.swap_buffers().unwrap(); } diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index fe7919fd..ff2963f7 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -7,7 +7,6 @@ use std::ptr; use std::sync::Arc; use std::sync::atomic::{Ordering, AtomicBool}; -use arrayvec::ArrayVec; use cgmath::{self, Matrix}; use euclid::{Rect, Size2D, Point2D}; use gl::types::*; @@ -23,7 +22,29 @@ use super::{Rgb, TermProps, GlyphCache}; 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"); +/// Text drawing program +/// +/// Uniforms are prefixed with "u", and vertex attributes are prefixed with "a". #[derive(Debug)] +pub struct ShaderProgram { + // Program id + id: GLuint, + + /// projection matrix uniform + u_projection: GLint, + + /// Terminal dimensions (pixels) + u_term_dim: GLint, + + /// Cell dimensions (pixels) + u_cell_dim: GLint, + + /// Cell separation (pixels) + u_cell_sep: GLint, +} + + +#[derive(Debug, Clone)] pub struct Glyph { tex_id: GLuint, top: f32, @@ -36,15 +57,46 @@ pub struct Glyph { uv_height: f32, } +#[derive(Debug)] +struct InstanceData { + // coords + col: f32, + row: f32, + // glyph offset + left: f32, + top: f32, + // glyph scale + width: f32, + height: f32, + // uv offset + uv_left: f32, + uv_bot: f32, + // uv scale + uv_width: f32, + uv_height: f32, + // color + r: f32, + g: f32, + b: f32, +} + +#[derive(Debug)] pub struct QuadRenderer { program: ShaderProgram, should_reload: Arc, vao: GLuint, vbo: GLuint, ebo: GLuint, - active_color: Rgb, + vbo_instance: GLuint, atlas: Vec, active_tex: GLuint, + batch: Batch, +} + +#[derive(Debug)] +pub struct RenderApi<'a> { + active_tex: &'a mut GLuint, + batch: &'a mut Batch, } #[derive(Debug)] @@ -60,44 +112,17 @@ struct ElementIndex { } #[derive(Debug)] -struct Batch { +pub struct Batch { tex: GLuint, - coords: ArrayVec<[Point2D; BATCH_MAX]>, - color: ArrayVec<[RgbUpload; BATCH_MAX]>, - glyph_scale: ArrayVec<[Point2D; BATCH_MAX]>, - glyph_offset: ArrayVec<[Point2D; BATCH_MAX]>, - uv_scale: ArrayVec<[Point2D; BATCH_MAX]>, - uv_offset: ArrayVec<[Point2D; BATCH_MAX]>, -} - -#[derive(Debug)] -struct RgbUpload { - r: i32, - g: i32, - b: i32, -} - -impl From for RgbUpload { - #[inline] - fn from(color: Rgb) -> RgbUpload { - RgbUpload { - r: color.r as i32, - g: color.g as i32, - b: color.b as i32, - } - } + instances: Vec, } impl Batch { + #[inline] pub fn new() -> Batch { Batch { tex: 0, - coords: ArrayVec::new(), - color: ArrayVec::new(), - glyph_scale: ArrayVec::new(), - glyph_offset: ArrayVec::new(), - uv_scale: ArrayVec::new(), - uv_offset: ArrayVec::new(), + instances: Vec::with_capacity(BATCH_MAX), } } @@ -106,12 +131,24 @@ impl Batch { self.tex = glyph.tex_id; } - self.coords.push(Point2D::new(col, row)); - self.color.push(RgbUpload::from(color)); - self.glyph_scale.push(Point2D::new(glyph.width, glyph.height)); - self.glyph_offset.push(Point2D::new(glyph.left, glyph.top)); - self.uv_scale.push(Point2D::new(glyph.uv_width, glyph.uv_height)); - self.uv_offset.push(Point2D::new(glyph.uv_left, glyph.uv_bot)); + self.instances.push(InstanceData { + col: col, + row: row, + + top: glyph.top, + left: glyph.left, + width: glyph.width, + height: glyph.height, + + uv_bot: glyph.uv_bot, + uv_left: glyph.uv_left, + uv_width: glyph.uv_width, + uv_height: glyph.uv_height, + + r: color.r as f32, + g: color.g as f32, + b: color.b as f32, + }); } #[inline] @@ -121,7 +158,7 @@ impl Batch { #[inline] pub fn len(&self) -> usize { - self.color.len() + self.instances.len() } #[inline] @@ -134,24 +171,20 @@ impl Batch { self.len() == 0 } - pub fn clear(&mut self) { - self.tex = 0; - self.coords.clear(); - self.color.clear(); - self.glyph_scale.clear(); - self.glyph_offset.clear(); - self.uv_scale.clear(); - self.uv_offset.clear(); + #[inline] + pub fn size(&self) -> usize { + self.len() * size_of::() } - pub fn render(&mut self, renderer: &mut QuadRenderer) { - renderer.render_batch(self); - self.clear(); + pub fn clear(&mut self) { + self.tex = 0; + self.instances.clear(); } } /// Maximum items to be drawn in a batch. -const BATCH_MAX: usize = 32usize; +const BATCH_MAX: usize = 4096; +const ATLAS_SIZE: i32 = 512; impl QuadRenderer { // TODO should probably hand this a transform instead of width/height @@ -162,14 +195,19 @@ impl QuadRenderer { let mut vbo: GLuint = 0; let mut ebo: GLuint = 0; + let mut vbo_instance: GLuint = 0; + + unsafe { gl::GenVertexArrays(1, &mut vao); gl::GenBuffers(1, &mut vbo); gl::GenBuffers(1, &mut ebo); + gl::GenBuffers(1, &mut vbo_instance); gl::BindVertexArray(vao); - gl::BindBuffer(gl::ARRAY_BUFFER, vbo); - + // ---------------------------- + // setup vertex position buffer + // ---------------------------- // Top right, Bottom right, Bottom left, Top left let vertices = [ PackedVertex { x: 1.0, y: 1.0 }, @@ -178,33 +216,69 @@ impl QuadRenderer { PackedVertex { x: 0.0, y: 1.0 }, ]; - gl::BufferData( - gl::ARRAY_BUFFER, - (size_of::() * vertices.len()) as GLsizeiptr, - vertices.as_ptr() as *const _, - gl::STATIC_DRAW - ); + gl::BindBuffer(gl::ARRAY_BUFFER, vbo); + gl::VertexAttribPointer(0, 2, + gl::FLOAT, gl::FALSE, + size_of::() as i32, + ptr::null()); + gl::EnableVertexAttribArray(0); + + gl::BufferData(gl::ARRAY_BUFFER, + (size_of::() * vertices.len()) as GLsizeiptr, + vertices.as_ptr() as *const _, + gl::STATIC_DRAW); + + // --------------------- + // Set up element buffer + // --------------------- let indices: [u32; 6] = [0, 1, 3, 1, 2, 3]; gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, ebo); gl::BufferData(gl::ELEMENT_ARRAY_BUFFER, - 6 * size_of::() as isize, + (6 * size_of::()) as isize, indices.as_ptr() as *const _, gl::STATIC_DRAW); - gl::EnableVertexAttribArray(0); - - // positions - gl::VertexAttribPointer(0, 2, + // ---------------------------- + // Setup vertex instance buffer + // ---------------------------- + gl::BindBuffer(gl::ARRAY_BUFFER, vbo_instance); + gl::BufferData(gl::ARRAY_BUFFER, + (BATCH_MAX * size_of::()) as isize, + ptr::null(), gl::STREAM_DRAW); + // coords + gl::VertexAttribPointer(1, 2, gl::FLOAT, gl::FALSE, - size_of::() as i32, + size_of::() as i32, ptr::null()); + gl::EnableVertexAttribArray(1); + gl::VertexAttribDivisor(1, 1); + // glyphoffset + gl::VertexAttribPointer(2, 4, + gl::FLOAT, gl::FALSE, + size_of::() as i32, + (2 * size_of::()) as *const _); + gl::EnableVertexAttribArray(2); + gl::VertexAttribDivisor(2, 1); + // uv + gl::VertexAttribPointer(3, 4, + gl::FLOAT, gl::FALSE, + size_of::() as i32, + (6 * size_of::()) as *const _); + gl::EnableVertexAttribArray(3); + gl::VertexAttribDivisor(3, 1); + // color + gl::VertexAttribPointer(4, 3, + gl::FLOAT, gl::FALSE, + size_of::() as i32, + (10 * size_of::()) as *const _); + gl::EnableVertexAttribArray(4); + gl::VertexAttribDivisor(4, 1); gl::BindVertexArray(0); gl::BindBuffer(gl::ARRAY_BUFFER, 0); - gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, 0); } let should_reload = Arc::new(AtomicBool::new(false)); @@ -245,118 +319,40 @@ impl QuadRenderer { vao: vao, vbo: vbo, ebo: ebo, - active_color: Rgb { r: 0, g: 0, b: 0 }, + vbo_instance: vbo_instance, atlas: Vec::new(), active_tex: 0, + batch: Batch::new(), }; - let atlas = renderer.create_atlas(1024); + let atlas = renderer.create_atlas(ATLAS_SIZE); renderer.atlas.push(atlas); renderer } - /// Render a string in a predefined location. Used for printing render time for profiling and - /// optimization. - pub fn render_string(&mut self, - s: &str, - glyph_cache: &GlyphCache, - props: &TermProps, - color: &Rgb) + pub fn with_api(&mut self, props: &TermProps, mut func: F) + where F: FnMut(RenderApi) { - self.prepare_render(props); - - let row = 40.0; - let mut col = 100.0; - - let mut batch = Batch::new(); - - for c in s.chars() { - if let Some(glyph) = glyph_cache.get(&c) { - batch.add_item(row, col, *color, glyph); - } - - col += 1.0; - - // Render batch and clear if it's full - if batch.full() { - batch.render(self); - } + if self.should_reload.load(Ordering::Relaxed) { + self.reload_shaders(props.width as u32, props.height as u32); } - if !batch.is_empty() { - batch.render(self); - } - - self.finish_render(); - } - - pub fn render_cursor(&mut self, - cursor: term::Cursor, - glyph_cache: &GlyphCache, - props: &TermProps) - { - self.prepare_render(props); - - if let Some(glyph) = glyph_cache.get(&term::CURSOR_SHAPE) { - let mut batch = Batch::new(); - batch.add_item(cursor.y as f32, cursor.x as f32, term::DEFAULT_FG, glyph); - batch.render(self); - } - - self.finish_render(); - } - - pub fn render_grid(&mut self, grid: &Grid, glyph_cache: &GlyphCache, props: &TermProps) { - self.prepare_render(props); - - // All draws are batched - let mut batch = Batch::new(); - - for (i, row) in grid.rows().enumerate() { - for (j, cell) in row.cells().enumerate() { - // Skip empty cells - if cell.c == ' ' { - continue; - } - - // Add cell to batch if the glyph is laoded - if let Some(glyph) = glyph_cache.get(&cell.c) { - batch.add_item(i as f32, j as f32, cell.fg, glyph); - } - - // Render batch and clear if it's full - if batch.full() { - batch.render(self); - } - } - } - - // Could have some data in a batch still; render it. - if !batch.is_empty() { - batch.render(self); - } - - self.finish_render(); - } - - fn prepare_render(&mut self, props: &TermProps) { unsafe { self.program.activate(); self.program.set_term_uniforms(props); gl::BindVertexArray(self.vao); - gl::BindBuffer(gl::ARRAY_BUFFER, self.vbo); gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, self.ebo); + gl::BindBuffer(gl::ARRAY_BUFFER, self.vbo_instance); gl::ActiveTexture(gl::TEXTURE0); } - if self.should_reload.load(Ordering::Relaxed) { - self.reload_shaders(props.width as u32, props.height as u32); - } - } + func(RenderApi { + active_tex: &mut self.active_tex, + batch: &mut self.batch, + }); - fn finish_render(&mut self) { unsafe { gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, 0); gl::BindBuffer(gl::ARRAY_BUFFER, 0); @@ -386,27 +382,9 @@ impl QuadRenderer { }; self.active_tex = 0; - self.active_color = Rgb { r: 0, g: 0, b: 0 }; self.program = program; } - fn render_batch(&mut self, batch: &Batch) { - self.program.set_uniforms(batch); - - // Bind texture if necessary - if self.active_tex != batch.tex { - unsafe { - gl::BindTexture(gl::TEXTURE_2D, batch.tex); - } - self.active_tex = batch.tex; - } - - unsafe { - let count = batch.len() as GLsizei; - gl::DrawElementsInstanced(gl::TRIANGLES, 6, gl::UNSIGNED_INT, ptr::null(), count); - } - } - /// Load a glyph into a texture atlas /// /// If the current atlas is full, a new one will be created. @@ -414,7 +392,7 @@ impl QuadRenderer { match self.atlas.last_mut().unwrap().insert(rasterized, &mut self.active_tex) { Ok(glyph) => glyph, Err(_) => { - let atlas = self.create_atlas(1024); + let atlas = self.create_atlas(ATLAS_SIZE); self.atlas.push(atlas); self.load_glyph(rasterized) } @@ -459,6 +437,89 @@ impl QuadRenderer { } } +impl<'a> RenderApi<'a> { + fn render_batch(&mut self) { + unsafe { + gl::BufferSubData(gl::ARRAY_BUFFER, 0, self.batch.size() as isize, + self.batch.instances.as_ptr() as *const _); + } + + // Bind texture if necessary + if *self.active_tex != self.batch.tex { + unsafe { + gl::BindTexture(gl::TEXTURE_2D, self.batch.tex); + } + *self.active_tex = self.batch.tex; + } + + unsafe { + gl::DrawElementsInstanced(gl::TRIANGLES, + 6, gl::UNSIGNED_INT, ptr::null(), + self.batch.len() as GLsizei); + } + + self.batch.clear(); + } + /// Render a string in a predefined location. Used for printing render time for profiling and + /// optimization. + pub fn render_string(&mut self, + s: &str, + glyph_cache: &GlyphCache, + color: &Rgb) + { + let row = 40.0; + let mut col = 100.0; + + for c in s.chars() { + if let Some(glyph) = glyph_cache.get(&c) { + self.add_render_item(row, col, *color, glyph); + } + + col += 1.0; + } + } + + #[inline] + fn add_render_item(&mut self, row: f32, col: f32, color: Rgb, glyph: &Glyph) { + self.batch.add_item(row, col, color, glyph); + + // Render batch and clear if it's full + if self.batch.full() { + self.render_batch(); + } + } + + pub fn render_cursor(&mut self, cursor: term::Cursor, glyph_cache: &GlyphCache) { + if let Some(glyph) = glyph_cache.get(&term::CURSOR_SHAPE) { + self.add_render_item(cursor.y as f32, cursor.x as f32, term::DEFAULT_FG, glyph); + } + } + + pub fn render_grid(&mut self, grid: &Grid, glyph_cache: &GlyphCache) { + for (i, row) in grid.rows().enumerate() { + for (j, cell) in row.cells().enumerate() { + // Skip empty cells + if cell.c == ' ' { + continue; + } + + // Add cell to batch if the glyph is laoded + if let Some(glyph) = glyph_cache.get(&cell.c) { + self.add_render_item(i as f32, j as f32, cell.fg, glyph); + } + } + } + } +} + +impl<'a> Drop for RenderApi<'a> { + fn drop(&mut self) { + if !self.batch.is_empty() { + self.render_batch(); + } + } +} + fn get_rect(glyph: &Glyph, x: f32, y: f32) -> Rect { Rect::new( Point2D::new(x + glyph.left as f32, y - (glyph.height - glyph.top) as f32), @@ -466,41 +527,6 @@ fn get_rect(glyph: &Glyph, x: f32, y: f32) -> Rect { ) } -pub struct ShaderProgram { - // Program id - id: GLuint, - - /// projection matrix uniform - u_projection: GLint, - - /// color uniform - u_color: GLint, - - /// Terminal dimensions (pixels) - u_term_dim: GLint, - - /// Cell dimensions (pixels) - u_cell_dim: GLint, - - /// Cell separation (pixels) - u_cell_sep: GLint, - - /// Cell coordinates in grid - u_cell_coord: GLint, - - /// Glyph scale - u_glyph_scale: GLint, - - /// Glyph offset - u_glyph_offset: GLint, - - /// Atlas scale - u_uv_scale: GLint, - - /// Atlas offset - u_uv_offset: GLint, -} - impl ShaderProgram { pub fn activate(&self) { unsafe { @@ -541,47 +567,23 @@ impl ShaderProgram { } // get uniform locations - let (projection, color, term_dim, cell_dim, cell_sep) = unsafe { + let (projection, term_dim, cell_dim, cell_sep) = unsafe { ( gl::GetUniformLocation(program, cptr!(b"projection\0")), - gl::GetUniformLocation(program, cptr!(b"textColor\0")), gl::GetUniformLocation(program, cptr!(b"termDim\0")), gl::GetUniformLocation(program, cptr!(b"cellDim\0")), gl::GetUniformLocation(program, cptr!(b"cellSep\0")), ) }; - assert_uniform_valid!(projection, color, term_dim, cell_dim, cell_sep); - - let (cell_coord, glyph_scale, glyph_offset, uv_scale, uv_offset) = unsafe { - ( - gl::GetUniformLocation(program, cptr!(b"gridCoords\0")), - gl::GetUniformLocation(program, cptr!(b"glyphScale\0")), - gl::GetUniformLocation(program, cptr!(b"glyphOffset\0")), - gl::GetUniformLocation(program, cptr!(b"uvScale\0")), - gl::GetUniformLocation(program, cptr!(b"uvOffset\0")), - ) - }; - - assert_uniform_valid!(cell_coord, glyph_scale, glyph_offset, uv_scale, uv_offset); - - // Initialize to known color (black) - unsafe { - gl::Uniform3i(color, 0, 0, 0); - } + assert_uniform_valid!(projection, term_dim, cell_dim, cell_sep); let shader = ShaderProgram { id: program, u_projection: projection, - u_color: color, u_term_dim: term_dim, u_cell_dim: cell_dim, u_cell_sep: cell_sep, - u_cell_coord: cell_coord, - u_glyph_scale: glyph_scale, - u_glyph_offset: glyph_offset, - u_uv_scale: uv_scale, - u_uv_offset: uv_offset, }; // set projection uniform @@ -608,18 +610,6 @@ impl ShaderProgram { } } - fn set_uniforms(&self, batch: &Batch) { - let len = batch.len(); - unsafe { - gl::Uniform2fv(self.u_cell_coord, len as i32, batch.coords.as_ptr() as *const _); - gl::Uniform2fv(self.u_glyph_scale, len as i32, batch.glyph_scale.as_ptr() as *const _); - gl::Uniform2fv(self.u_glyph_offset, len as i32, batch.glyph_offset.as_ptr() as *const _); - gl::Uniform2fv(self.u_uv_scale, len as i32, batch.uv_scale.as_ptr() as *const _); - gl::Uniform2fv(self.u_uv_offset, len as i32, batch.uv_offset.as_ptr() as *const _); - gl::Uniform3iv(self.u_color, len as i32, batch.color.as_ptr() as *const _); - } - } - fn create_program(vertex: GLuint, fragment: GLuint) -> GLuint { unsafe { let program = gl::CreateProgram(); @@ -667,6 +657,14 @@ impl ShaderProgram { } } +impl Drop for ShaderProgram { + fn drop(&mut self) { + unsafe { + gl::DeleteProgram(self.id); + } + } +} + fn get_program_info_log(program: GLuint) -> String { // Get expected log length let mut max_length: GLint = 0; @@ -780,6 +778,7 @@ impl From for ShaderCreationError { /// │ │ │ │ │ <- Row considered full when next glyph doesn't /// └─────┴─────┴─────┴───────────┘ fit in the row. /// (0, 0) x-> +#[derive(Debug)] struct Atlas { /// Texture id for this atlas id: GLuint,