Refactor Instanced Drawing to use Vertex Arrays

Per-instanced data was previously stored in uniforms. This required
several OpenGL calls to upload all of the data, and it was more complex
to prepare (several vecs vs one).

Additionally, drawing APIs are now accessible through a `RenderApi`
(obtained through `QuadRenderer::with_api`) which enables some RAII
patterns. Specifically, checks for batch flushing are handled in Drop.
This commit is contained in:
Joe Wilm 2016-06-06 13:20:35 -07:00
parent 1f3f9add49
commit ed7aa96907
No known key found for this signature in database
GPG Key ID: 39B57C6972F518DA
4 changed files with 283 additions and 278 deletions

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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<AtomicBool>,
vao: GLuint,
vbo: GLuint,
ebo: GLuint,
active_color: Rgb,
vbo_instance: GLuint,
atlas: Vec<Atlas>,
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<f32>; BATCH_MAX]>,
color: ArrayVec<[RgbUpload; BATCH_MAX]>,
glyph_scale: ArrayVec<[Point2D<f32>; BATCH_MAX]>,
glyph_offset: ArrayVec<[Point2D<f32>; BATCH_MAX]>,
uv_scale: ArrayVec<[Point2D<f32>; BATCH_MAX]>,
uv_offset: ArrayVec<[Point2D<f32>; BATCH_MAX]>,
}
#[derive(Debug)]
struct RgbUpload {
r: i32,
g: i32,
b: i32,
}
impl From<Rgb> 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<InstanceData>,
}
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::<InstanceData>()
}
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::<PackedVertex>() * 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::<PackedVertex>() as i32,
ptr::null());
gl::EnableVertexAttribArray(0);
gl::BufferData(gl::ARRAY_BUFFER,
(size_of::<PackedVertex>() * 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::<u32>() as isize,
(6 * size_of::<u32>()) 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::<InstanceData>()) as isize,
ptr::null(), gl::STREAM_DRAW);
// coords
gl::VertexAttribPointer(1, 2,
gl::FLOAT, gl::FALSE,
size_of::<PackedVertex>() as i32,
size_of::<InstanceData>() as i32,
ptr::null());
gl::EnableVertexAttribArray(1);
gl::VertexAttribDivisor(1, 1);
// glyphoffset
gl::VertexAttribPointer(2, 4,
gl::FLOAT, gl::FALSE,
size_of::<InstanceData>() as i32,
(2 * size_of::<f32>()) as *const _);
gl::EnableVertexAttribArray(2);
gl::VertexAttribDivisor(2, 1);
// uv
gl::VertexAttribPointer(3, 4,
gl::FLOAT, gl::FALSE,
size_of::<InstanceData>() as i32,
(6 * size_of::<f32>()) as *const _);
gl::EnableVertexAttribArray(3);
gl::VertexAttribDivisor(3, 1);
// color
gl::VertexAttribPointer(4, 3,
gl::FLOAT, gl::FALSE,
size_of::<InstanceData>() as i32,
(10 * size_of::<f32>()) 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<F>(&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<f32> {
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<f32> {
)
}
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<io::Error> 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,