frienderer 0.13.0

Very simple OpenGL renderer, mainly for GUIs.
Documentation
use std::{mem::offset_of, rc::Rc};

use glam::{Vec2, Vec4, u16vec2, vec2};
use glow::HasContext;
use rustc_hash::{FxHashMap, FxHashSet};

use crate::{
	TextureHandle,
	common::{create_shader_program, pop_debug_group, push_debug_group, slice_as_bytes},
};

const SRC_VERT_HYPERQUAD: &str = include_str!("./shaders/hyperquad.vert");
const SRC_FRAG_HYPERQUAD: &str = include_str!("./shaders/hyperquad.frag");

pub struct HyperQuadBatchRenderer {
	gl: Rc<glow::Context>,
	shader: glow::Program,
	u_viewport: glow::UniformLocation,
	// u_smooth_delta: glow::UniformLocation,
	// u_images: glow::UniformLocation,
	vao: glow::VertexArray,
	vbo: glow::Buffer,
	ebo: glow::Buffer,

	quads: Vec<HyperQuad>,
	vertices: Vec<[HyperQuadVertex; 4]>,
	indices: Vec<[u32; 6]>,
	textures_to_batch: FxHashSet<TextureHandle>,
	textures_to_batch_index_map: FxHashMap<TextureHandle, u16>,

	prev_vertices_count: usize,
	gl_idx_buffer_count: i32,
	gl_idx_buffer_capacity: i32,
}

impl HyperQuadBatchRenderer {
	pub fn new(gl: Rc<glow::Context>, viewport: Vec2, scale_factor: f32) -> Self {
		unsafe {
			push_debug_group(&gl, "HyperQuadBatchRenderer: init");

			let shader = create_shader_program(&gl, SRC_VERT_HYPERQUAD, SRC_FRAG_HYPERQUAD);

			let u_viewport = gl.get_uniform_location(shader, "u_viewport").unwrap();
			gl.uniform_2_f32(Some(&u_viewport), viewport.x, viewport.y);
			let u_smooth_delta = gl.get_uniform_location(shader, "u_smooth_delta").unwrap();
			gl.uniform_1_f32(Some(&u_smooth_delta), 0.5 / scale_factor);
			let u_images = gl.get_uniform_location(shader, "u_images").unwrap();
			gl.uniform_1_i32_slice(Some(&u_images), &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);

			let vao = gl.create_vertex_array().unwrap();
			gl.bind_vertex_array(Some(vao));
			let vbo = gl.create_buffer().unwrap();
			gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo));
			let ebo = gl.create_buffer().unwrap();
			gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(ebo));

			let size_vertex = size_of::<HyperQuadVertex>() as i32;

			#[rustfmt::skip]
            {
                let a_pos       = gl.get_attrib_location(shader, "pos"     ).unwrap();
                let a_uv        = gl.get_attrib_location(shader, "uv"      ).unwrap();
                let a_size      = gl.get_attrib_location(shader, "size"    ).unwrap();
                let a_color     = gl.get_attrib_location(shader, "color"   ).unwrap();
                let a_control   = gl.get_attrib_location(shader, "control" ).unwrap();
                let a_box_blur  = gl.get_attrib_location(shader, "box_blur").unwrap();

                let a_border    = gl.get_attrib_location(shader, "border").unwrap();
                let a_radius    = gl.get_attrib_location(shader, "radius").unwrap();

                gl.vertex_attrib_pointer_f32(a_pos,       2, glow::FLOAT, false, size_vertex, offset_of!(HyperQuadVertex, pos)      as i32);
                gl.vertex_attrib_pointer_f32(a_uv,        2, glow::FLOAT, false, size_vertex, offset_of!(HyperQuadVertex, uv)       as i32);
                gl.vertex_attrib_pointer_f32(a_size,      2, glow::FLOAT, false, size_vertex, offset_of!(HyperQuadVertex, size)     as i32);
                gl.vertex_attrib_pointer_i32(a_color,     1, glow::INT,          size_vertex, offset_of!(HyperQuadVertex, color)    as i32);
                gl.vertex_attrib_pointer_i32(a_control,   1, glow::SHORT,        size_vertex, offset_of!(HyperQuadVertex, control)  as i32);
                gl.vertex_attrib_pointer_i32(a_box_blur,  1, glow::SHORT,        size_vertex, offset_of!(HyperQuadVertex, box_blur) as i32);

                gl.vertex_attrib_pointer_f32(a_border,    4, glow::FLOAT, false, size_vertex, offset_of!(HyperQuadVertex, border)   as i32);
                gl.vertex_attrib_pointer_f32(a_radius,    4, glow::FLOAT, false, size_vertex, offset_of!(HyperQuadVertex, radius)   as i32);

                gl.enable_vertex_attrib_array(a_pos);
                gl.enable_vertex_attrib_array(a_uv);
                gl.enable_vertex_attrib_array(a_size);
                gl.enable_vertex_attrib_array(a_color);
                gl.enable_vertex_attrib_array(a_control);
                gl.enable_vertex_attrib_array(a_box_blur);

                gl.enable_vertex_attrib_array(a_border);
                gl.enable_vertex_attrib_array(a_radius);
            };

			pop_debug_group(&gl);

			Self {
				gl,
				shader,
				u_viewport,
				// u_smooth_delta,
				// u_images,
				vao,
				vbo,
				ebo,

				quads: Vec::new(),
				vertices: Vec::new(),
				indices: Vec::new(),
				textures_to_batch: FxHashSet::default(),
				textures_to_batch_index_map: FxHashMap::default(),

				prev_vertices_count: 0,
				gl_idx_buffer_count: 0,
				gl_idx_buffer_capacity: 0,
			}
		}
	}

	pub fn push_quad(&mut self, quad: HyperQuad) {
		self.quads.push(quad);
	}

	/// Returns the number of executed draw calls.
	pub fn draw(&mut self) -> usize {
		let mut draw_call_count = 0;

		let gl = &self.gl;
		unsafe {
			push_debug_group(gl, "HyperQuadBatchRenderer: draw");

			let mut quad_idx = 0;
			while quad_idx < self.quads.len() {
				self.textures_to_batch.clear();
				self.textures_to_batch_index_map.clear();
				self.vertices.clear();

				let quad_start = quad_idx;
				while quad_idx < self.quads.len() {
					if self.textures_to_batch.len() == 16 {
						break;
					}

					let quad = self.quads[quad_idx];
					if let Some(texture) = quad.texture {
						self.textures_to_batch.insert(texture);
					}

					quad_idx += 1;
				}
				let quad_end = quad_idx;

				let mut texture_bindings = [None; 16];
				for (i, texture) in self.textures_to_batch.drain().enumerate() {
					self.textures_to_batch_index_map.insert(texture, i as u16);
					texture_bindings[i] = Some(texture.0);
				}

				for quad in &self.quads[quad_start..quad_end] {
					let texture_index = (quad.texture)
						.map(|texture| self.textures_to_batch_index_map[&texture])
						.unwrap_or_default();
					self.vertices.push(quad.vertices(texture_index));
				}

				if self.vertices.len() > self.prev_vertices_count {
					gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.vbo));
					gl.buffer_data_u8_slice(glow::ARRAY_BUFFER, slice_as_bytes(&self.vertices), glow::DYNAMIC_DRAW);

					for i in self.prev_vertices_count..self.vertices.len() {
						self.indices.push(quad_indices(i as u32));
					}

					gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(self.ebo));
					gl.buffer_data_u8_slice(
						glow::ELEMENT_ARRAY_BUFFER,
						slice_as_bytes(&self.indices),
						glow::STATIC_DRAW,
					);

					self.prev_vertices_count = self.vertices.len();
				} else {
					gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.vbo));
					gl.buffer_sub_data_u8_slice(glow::ARRAY_BUFFER, 0, slice_as_bytes(&self.vertices));
				}

				self.gl_idx_buffer_count = (self.vertices.len() * 6) as i32;
				self.gl_idx_buffer_capacity = self.gl_idx_buffer_capacity.max(self.gl_idx_buffer_count);

				gl.bind_vertex_array(Some(self.vao));
				gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.vbo));
				gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(self.ebo));
				gl.use_program(Some(self.shader));

				for (i, texture_binding) in texture_bindings.into_iter().enumerate() {
					gl.active_texture(glow::TEXTURE0 + i as u32);
					gl.bind_texture(glow::TEXTURE_2D, texture_binding);
				}

				gl.draw_elements(glow::TRIANGLES, self.gl_idx_buffer_count, glow::UNSIGNED_INT, 0);
				draw_call_count += 1;
			}
			self.quads.clear();

			pop_debug_group(gl);
		}

		draw_call_count
	}

	pub fn resize(&mut self, viewport: Vec2) {
		let gl = &self.gl;
		unsafe {
			push_debug_group(gl, "HyperQuadBatchRenderer: resize");

			gl.use_program(Some(self.shader));
			gl.uniform_2_f32(Some(&self.u_viewport), viewport.x, viewport.y);

			pop_debug_group(gl);
		}
	}
}

impl Drop for HyperQuadBatchRenderer {
	fn drop(&mut self) {
		let gl = &self.gl;
		unsafe {
			push_debug_group(gl, "HyperQuadBatchRenderer: drop");

			gl.delete_program(self.shader);
			gl.delete_vertex_array(self.vao);

			gl.delete_buffer(self.vbo);
			gl.delete_buffer(self.ebo);

			pop_debug_group(gl);
		}
	}
}

#[repr(C)]
#[derive(Debug, Clone, Copy, Default)]
pub struct HyperQuad {
	pub pos: Vec2,
	pub size: Vec2,
	pub uv_pos: Vec2,
	pub uv_size: Vec2,
	pub color: u32,
	pub texture: Option<TextureHandle>,
	pub has_borders: bool,
	pub box_blur: f32,
	pub border: Vec4,
	pub radius: Vec4,
	pub rotation: f32,
	pub origin: Vec2,
}

impl HyperQuad {
	fn vertices(self, texture_index: u16) -> [HyperQuadVertex; 4] {
		let r = Vec2::from_angle(self.rotation);

		let (pmin, pmax) = (Vec2::ZERO - self.origin, Vec2::ONE - self.origin);

		let pos_uvs = [
			(
				(vec2(pmin.x, pmin.y) * self.size).rotate(r) + self.pos,
				self.uv_pos + vec2(0.0, 0.0) * self.uv_size,
				u16vec2(0, 0),
			),
			(
				(vec2(pmin.x, pmax.y) * self.size).rotate(r) + self.pos,
				self.uv_pos + vec2(0.0, 1.0) * self.uv_size,
				u16vec2(0, 1),
			),
			(
				(vec2(pmax.x, pmax.y) * self.size).rotate(r) + self.pos,
				self.uv_pos + vec2(1.0, 1.0) * self.uv_size,
				u16vec2(1, 1),
			),
			(
				(vec2(pmax.x, pmin.y) * self.size).rotate(r) + self.pos,
				self.uv_pos + vec2(1.0, 0.0) * self.uv_size,
				u16vec2(1, 0),
			),
		];

		let control = texture_index & 0b1111;
		let control = control | ((self.texture.is_some() as u16) << 4);
		let control = control | ((self.has_borders as u16) << 5);

		let size = self.size;
		let color = self.color;
		let border = self.border.clamp(Vec4::ZERO, Vec4::MAX);
		let radius = self.radius.clamp(Vec4::ZERO, Vec4::MAX);
		let box_blur = (self.box_blur * 4.0).round().clamp(i16::MIN as f32, i16::MAX as f32) as i16;

		pos_uvs.map(|(pos, uv, local_uv)| HyperQuadVertex {
			pos,
			uv,
			size,
			color,
			control: control | (local_uv.x << 6) | (local_uv.y << 7),
			border,
			radius,
			box_blur,
		})
	}
}

fn quad_indices(index: u32) -> [u32; 6] {
	let i = index * 4;
	[i, 1 + i, 2 + i, i, 2 + i, 3 + i]
}

#[repr(C)]
#[derive(Debug, Clone, Copy, Default)]
struct HyperQuadVertex {
	pub pos: Vec2,
	pub uv: Vec2,
	pub size: Vec2,
	pub color: u32,
	pub control: u16,
	pub box_blur: i16,
	pub border: Vec4,
	pub radius: Vec4,
}