Dairy-Drift/imgui_glow_renderer/src/lib.rs

1153 lines
40 KiB
Rust

//! Renderer for [`imgui-rs`][imgui] using the [`glow`] library for OpenGL.
//!
//! This is heavily influenced by the
//! [example from upstream](https://github.com/ocornut/imgui/blob/fe245914114588f272b0924538fdd43f6c127a26/backends/imgui_impl_opengl3.cpp).
//!
//! # Basic usage
//!
//! A few code [examples] are provided in the source.
//!
//! [examples]: https://github.com/imgui-rs/imgui-rs/tree/main/imgui-glow-renderer/examples
//!
//! In short, create either an [`AutoRenderer`] (for basic usage) or [`Renderer`]
//! (for slightly more customizable operation), then call the `render(...)`
//! method with draw data from [`imgui`].
//!
//! # OpenGL (ES) versions
//!
//! This renderer is expected to work with OpenGL version 3.3 and above, and
//! OpenGL ES version 3.0 or above. This should cover the vast majority of even
//! fairly dated hardware. Please submit an issue for any incompatibilities
//! found with these OpenGL versions, pull requests to extend support to earlier
//! versions are welcomed.
//!
//! # Scope
//!
//! Consider this an example renderer. It is intended to be sufficent for simple
//! applications running imgui-rs as the final rendering step. If your application
//! has more specific needs, it's probably best to write your own renderer, in
//! which case this can be a useful starting point. This renderer is also not
//! foolproof (largely due to the global nature of the OpenGL state). For example,
//! a few "internal" functions are marked `pub` to allow the user more
//! fine-grained control at the cost of allowing potential rendering errors.
//!
//! # sRGB
//!
//! When outputting colors to a screen, colors need to be converted from a
//! linear color space to a non-linear space matching the monitor (e.g. sRGB).
//! When using the [`AutoRenderer`], this library will convert colors to sRGB
//! as a step in the shader. When initialising a [`Renderer`], you can choose
//! whether or not to include this step in the shader or not when calling
//! [`Renderer::initialize`].
//!
//! This library also assumes that textures have their internal format
//! set appropriately when uploaded to OpenGL. That is, assuming your texture
//! is sRGB (if you don't know, it probably is) the `internal_format` is
//! one of the `SRGB*` values.
use std::{borrow::Cow, error::Error, fmt::Display, mem::size_of, num::NonZeroU32};
use imgui::{internal::RawWrapper, DrawCmd, DrawData, DrawVert};
use crate::versions::{GlVersion, GlslVersion};
// Re-export glow to make it easier for users to use the correct version.
pub use glow;
use glow::{Context, HasContext};
pub mod versions;
pub type GlBuffer = <Context as HasContext>::Buffer;
pub type GlTexture = <Context as HasContext>::Texture;
pub type GlVertexArray = <Context as HasContext>::VertexArray;
type GlProgram = <Context as HasContext>::Program;
type GlUniformLocation = <Context as HasContext>::UniformLocation;
/// Renderer which owns the OpenGL context and handles textures itself. Also
/// converts all output colors to sRGB for display. Useful for simple applications,
/// but more complicated applications may prefer to use [`Renderer`], or even
/// write their own renderer based on this code.
///
/// OpenGL context is still available to the rest of the application through
/// the [`gl_context`](Self::gl_context) method.
pub struct AutoRenderer {
gl: std::sync::Arc<glow::Context>,
texture_map: SimpleTextureMap,
renderer: Renderer,
}
impl AutoRenderer {
/// # Errors
/// Any error initialising the OpenGL objects (including shaders) will
/// result in an error.
pub fn initialize(
gl: std::sync::Arc<glow::Context>,
imgui_context: &mut imgui::Context,
) -> Result<Self, InitError> {
let mut texture_map = SimpleTextureMap::default();
let renderer = Renderer::initialize(&gl, imgui_context, &mut texture_map, true)?;
Ok(Self {
gl,
texture_map,
renderer,
})
}
/// Note: no need to provide a `mut` version of this, as all methods on
/// [`glow::HasContext`] are immutable.
#[inline]
pub fn gl_context(&self) -> &glow::Context {
&self.gl
}
#[inline]
pub fn texture_map(&self) -> &SimpleTextureMap {
&self.texture_map
}
#[inline]
pub fn texture_map_mut(&mut self) -> &mut SimpleTextureMap {
&mut self.texture_map
}
#[inline]
pub fn renderer(&self) -> &Renderer {
&self.renderer
}
/// # Errors
/// Some OpenGL errors trigger an error (few are explicitly checked,
/// however)
#[inline]
pub fn render(&mut self, draw_data: &DrawData) -> Result<(), RenderError> {
self.renderer.render(&self.gl, &self.texture_map, draw_data)
}
}
impl Drop for AutoRenderer {
fn drop(&mut self) {
self.renderer.destroy(&self.gl);
}
}
/// Main renderer. Borrows the OpenGL context and [texture map](TextureMap)
/// when required.
pub struct Renderer {
shaders: Shaders,
state_backup: GlStateBackup,
pub vbo_handle: Option<GlBuffer>,
pub ebo_handle: Option<GlBuffer>,
pub font_atlas_texture: Option<GlTexture>,
#[cfg(feature = "bind_vertex_array_support")]
pub vertex_array_object: Option<GlVertexArray>,
pub gl_version: GlVersion,
pub has_clip_origin_support: bool,
pub is_destroyed: bool,
}
impl Renderer {
/// Create the renderer, initialising OpenGL objects and shaders.
///
/// `output_srgb` controls whether the shader outputs sRGB colors, or linear
/// RGB colors. In short:
/// - If you're outputting to a display and haven't specified the framebuffer
/// is sRGB (e.g. with `gl.enable(glow::FRAMEBUFFER_SRGB)`), then you probably
/// want `output_srgb=true`.
/// - OpenGL ES doesn't support sRGB framebuffers, so you almost always
/// want `output_srgb=true` if you're using OpenGL ES and you're outputting
/// to a display.
/// - If you're outputting to a display with an sRGB framebuffer (e.g. with
/// `gl.enable(glow::FRAMEBUFFER_SRGB)`), then you probably want
/// `output_srgb=false`, as OpenGL will convert to sRGB itself.
/// - If you're not outputting to a display, but instead to some intermediate
/// framebuffer, then you probably want `output_srgb=false` to keep the
/// colors in linear color space, and then convert them to sRGB at some
/// later stage.
///
/// # Errors
/// Any error initialising the OpenGL objects (including shaders) will
/// result in an error.
pub fn initialize<T: TextureMap>(
gl: &Context,
imgui_context: &mut imgui::Context,
texture_map: &mut T,
output_srgb: bool,
) -> Result<Self, InitError> {
#![allow(
clippy::similar_names,
clippy::cast_sign_loss,
clippy::shadow_unrelated
)]
let gl_version = GlVersion::read(gl);
#[cfg(feature = "clip_origin_support")]
let has_clip_origin_support = {
let support = gl_version.clip_origin_support();
#[cfg(feature = "gl_extensions_support")]
if support {
support
} else {
let extensions_count = unsafe { gl.get_parameter_i32(glow::NUM_EXTENSIONS) } as u32;
(0..extensions_count).any(|index| {
let extension_name =
unsafe { gl.get_parameter_indexed_string(glow::EXTENSIONS, index) };
extension_name == "GL_ARB_clip_control"
})
}
#[cfg(not(feature = "gl_extensions_support"))]
support
};
#[cfg(not(feature = "clip_origin_support"))]
let has_clip_origin_support = false;
let mut state_backup = GlStateBackup::default();
state_backup.pre_init(gl);
let font_atlas_texture = prepare_font_atlas(gl, imgui_context.fonts(), texture_map)?;
let shaders = Shaders::new(gl, gl_version, output_srgb)?;
let vbo_handle = unsafe { gl.create_buffer() }.map_err(InitError::CreateBufferObject)?;
let ebo_handle = unsafe { gl.create_buffer() }.map_err(InitError::CreateBufferObject)?;
state_backup.post_init(gl);
let out = Self {
shaders,
state_backup,
vbo_handle: Some(vbo_handle),
ebo_handle: Some(ebo_handle),
font_atlas_texture: Some(font_atlas_texture),
#[cfg(feature = "bind_vertex_array_support")]
vertex_array_object: None,
gl_version,
has_clip_origin_support,
is_destroyed: false,
};
// Leave this until the end of the function to avoid changing state if
// there was ever an error above
out.configure_imgui_context(imgui_context);
Ok(out)
}
/// This must be called before being dropped to properly free OpenGL
/// resources.
pub fn destroy(&mut self, gl: &Context) {
if self.is_destroyed {
return;
}
if let Some(h) = self.vbo_handle {
unsafe { gl.delete_buffer(h) };
self.vbo_handle = None;
}
if let Some(h) = self.ebo_handle {
unsafe { gl.delete_buffer(h) };
self.ebo_handle = None;
}
if let Some(p) = self.shaders.program {
unsafe { gl.delete_program(p) };
self.shaders.program = None;
}
if let Some(h) = self.font_atlas_texture {
unsafe { gl.delete_texture(h) };
self.font_atlas_texture = None;
}
self.is_destroyed = true;
}
/// # Errors
/// Some OpenGL errors trigger an error (few are explicitly checked,
/// however)
pub fn render<T: TextureMap>(
&mut self,
gl: &Context,
texture_map: &T,
draw_data: &DrawData,
) -> Result<(), RenderError> {
if self.is_destroyed {
return Err(Self::renderer_destroyed());
}
let fb_width = draw_data.display_size[0] * draw_data.framebuffer_scale[0];
let fb_height = draw_data.display_size[1] * draw_data.framebuffer_scale[1];
if !(fb_width > 0.0 && fb_height > 0.0) {
return Ok(());
}
gl_debug_message(gl, "imgui-rs-glow: start render");
self.state_backup.pre_render(gl, self.gl_version);
#[cfg(feature = "bind_vertex_array_support")]
if self.gl_version.bind_vertex_array_support() {
unsafe {
self.vertex_array_object = Some(gl
.create_vertex_array()
.map_err(|err| format!("Error creating vertex array object: {}", err))?);
gl.bind_vertex_array(self.vertex_array_object);
}
}
self.set_up_render_state(gl, draw_data, fb_width, fb_height)?;
gl_debug_message(gl, "start loop over draw lists");
for draw_list in draw_data.draw_lists() {
unsafe {
gl.buffer_data_u8_slice(
glow::ARRAY_BUFFER,
to_byte_slice(draw_list.vtx_buffer()),
glow::STREAM_DRAW,
);
gl.buffer_data_u8_slice(
glow::ELEMENT_ARRAY_BUFFER,
to_byte_slice(draw_list.idx_buffer()),
glow::STREAM_DRAW,
);
}
gl_debug_message(gl, "start loop over commands");
for command in draw_list.commands() {
match command {
DrawCmd::Elements { count, cmd_params } => self.render_elements(
gl,
texture_map,
count,
cmd_params,
draw_data,
fb_width,
fb_height,
),
DrawCmd::RawCallback { callback, raw_cmd } => unsafe {
callback(draw_list.raw(), raw_cmd)
},
DrawCmd::ResetRenderState => {
self.set_up_render_state(gl, draw_data, fb_width, fb_height)?
}
}
}
}
#[cfg(feature = "bind_vertex_array_support")]
if self.gl_version.bind_vertex_array_support() {
unsafe { gl.delete_vertex_array(self.vertex_array_object.unwrap()) };
self.vertex_array_object = None;
}
self.state_backup.post_render(gl, self.gl_version);
gl_debug_message(gl, "imgui-rs-glow: complete render");
Ok(())
}
/// # Errors
/// Few GL calls are checked for errors, but any that are found will result
/// in an error. Errors from the state manager lifecycle callbacks will also
/// result in an error.
pub fn set_up_render_state(
&mut self,
gl: &Context,
draw_data: &DrawData,
fb_width: f32,
fb_height: f32,
) -> Result<(), RenderError> {
#![allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
if self.is_destroyed {
return Err(Self::renderer_destroyed());
}
unsafe {
gl.active_texture(glow::TEXTURE0);
gl.enable(glow::BLEND);
gl.blend_equation(glow::FUNC_ADD);
gl.blend_func_separate(
glow::SRC_ALPHA,
glow::ONE_MINUS_SRC_ALPHA,
glow::ONE,
glow::ONE_MINUS_SRC_ALPHA,
);
gl.disable(glow::CULL_FACE);
gl.disable(glow::DEPTH_TEST);
gl.disable(glow::STENCIL_TEST);
gl.enable(glow::SCISSOR_TEST);
#[cfg(feature = "primitive_restart_support")]
if self.gl_version.primitive_restart_support() {
gl.disable(glow::PRIMITIVE_RESTART);
}
#[cfg(feature = "polygon_mode_support")]
if self.gl_version.polygon_mode_support() {
gl.polygon_mode(glow::FRONT_AND_BACK, glow::FILL);
}
gl.viewport(0, 0, fb_width as _, fb_height as _);
}
#[cfg(feature = "clip_origin_support")]
let clip_origin_is_lower_left = if self.has_clip_origin_support {
unsafe { gl.get_parameter_i32(glow::CLIP_ORIGIN) != glow::UPPER_LEFT as i32 }
} else {
true
};
#[cfg(not(feature = "clip_origin_support"))]
let clip_origin_is_lower_left = true;
let projection_matrix = calculate_matrix(draw_data, clip_origin_is_lower_left);
unsafe {
gl.use_program(self.shaders.program);
gl.uniform_1_i32(Some(&self.shaders.texture_uniform_location), 0);
gl.uniform_matrix_4_f32_slice(
Some(&self.shaders.matrix_uniform_location),
false,
&projection_matrix,
);
}
#[cfg(feature = "bind_sampler_support")]
if self.gl_version.bind_sampler_support() {
unsafe { gl.bind_sampler(0, None) };
}
// TODO: soon it should be possible for these to be `const` functions
let position_field_offset = memoffset::offset_of!(DrawVert, pos) as _;
let uv_field_offset = memoffset::offset_of!(DrawVert, uv) as _;
let color_field_offset = memoffset::offset_of!(DrawVert, col) as _;
unsafe {
gl.bind_buffer(glow::ARRAY_BUFFER, self.vbo_handle);
gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, self.ebo_handle);
gl.enable_vertex_attrib_array(self.shaders.position_attribute_index);
gl.vertex_attrib_pointer_f32(
self.shaders.position_attribute_index,
2,
glow::FLOAT,
false,
size_of::<imgui::DrawVert>() as _,
position_field_offset,
);
gl.enable_vertex_attrib_array(self.shaders.uv_attribute_index);
gl.vertex_attrib_pointer_f32(
self.shaders.uv_attribute_index,
2,
glow::FLOAT,
false,
size_of::<imgui::DrawVert>() as _,
uv_field_offset,
);
gl.enable_vertex_attrib_array(self.shaders.color_attribute_index);
gl.vertex_attrib_pointer_f32(
self.shaders.color_attribute_index,
4,
glow::UNSIGNED_BYTE,
true,
size_of::<imgui::DrawVert>() as _,
color_field_offset,
);
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn render_elements<T: TextureMap>(
&self,
gl: &Context,
texture_map: &T,
element_count: usize,
element_params: imgui::DrawCmdParams,
draw_data: &DrawData,
fb_width: f32,
fb_height: f32,
) {
#![allow(
clippy::similar_names,
clippy::cast_possible_truncation,
clippy::cast_possible_wrap
)]
let imgui::DrawCmdParams {
clip_rect,
texture_id,
vtx_offset,
idx_offset,
} = element_params;
let clip_off = draw_data.display_pos;
let scale = draw_data.framebuffer_scale;
let clip_x1 = (clip_rect[0] - clip_off[0]) * scale[0];
let clip_y1 = (clip_rect[1] - clip_off[1]) * scale[1];
let clip_x2 = (clip_rect[2] - clip_off[0]) * scale[0];
let clip_y2 = (clip_rect[3] - clip_off[1]) * scale[1];
if clip_x1 >= fb_width || clip_y1 >= fb_height || clip_x2 < 0.0 || clip_y2 < 0.0 {
return;
}
unsafe {
gl.scissor(
clip_x1 as i32,
(fb_height - clip_y2) as i32,
(clip_x2 - clip_x1) as i32,
(clip_y2 - clip_y1) as i32,
);
gl.bind_texture(glow::TEXTURE_2D, texture_map.gl_texture(texture_id));
#[cfg(feature = "vertex_offset_support")]
let with_offset = self.gl_version.vertex_offset_support();
#[cfg(not(feature = "vertex_offset_support"))]
let with_offset = false;
if with_offset {
gl.draw_elements_base_vertex(
glow::TRIANGLES,
element_count as _,
imgui_index_type_as_gl(),
(idx_offset * size_of::<imgui::DrawIdx>()) as _,
vtx_offset as _,
);
} else {
gl.draw_elements(
glow::TRIANGLES,
element_count as _,
imgui_index_type_as_gl(),
(idx_offset * size_of::<imgui::DrawIdx>()) as _,
);
}
}
}
fn configure_imgui_context(&self, imgui_context: &mut imgui::Context) {
imgui_context.set_renderer_name(Some(format!(
"imgui-rs-glow-render {}",
env!("CARGO_PKG_VERSION")
)));
#[cfg(feature = "vertex_offset_support")]
if self.gl_version.vertex_offset_support() {
imgui_context
.io_mut()
.backend_flags
.insert(imgui::BackendFlags::RENDERER_HAS_VTX_OFFSET);
}
}
fn renderer_destroyed() -> RenderError {
"Renderer is destroyed".into()
}
}
/// Trait for mapping imgui texture IDs to OpenGL textures.
///
/// [`register`] should be called after uploading a texture to OpenGL to get a
/// [`imgui::TextureId`] corresponding to it.
///
/// [`register`]: Self::register
///
/// Then [`gl_texture`] can be called to find the OpenGL texture corresponding to
/// that [`imgui::TextureId`].
///
/// [`gl_texture`]: Self::gl_texture
pub trait TextureMap {
fn register(&mut self, gl_texture: GlTexture) -> Option<imgui::TextureId>;
fn gl_texture(&self, imgui_texture: imgui::TextureId) -> Option<GlTexture>;
}
/// Texture map where the imgui texture ID is simply numerically equal to the
/// OpenGL texture ID.
#[derive(Default)]
pub struct SimpleTextureMap();
impl TextureMap for SimpleTextureMap {
#[inline(always)]
fn register(&mut self, gl_texture: glow::Texture) -> Option<imgui::TextureId> {
Some(imgui::TextureId::new(gl_texture.0.get() as _))
}
#[inline(always)]
fn gl_texture(&self, imgui_texture: imgui::TextureId) -> Option<glow::Texture> {
#[allow(clippy::cast_possible_truncation)]
Some(glow::NativeTexture(NonZeroU32::new(imgui_texture.id() as _).unwrap()))
}
}
/// [`imgui::Textures`] is a simple choice for a texture map.
impl TextureMap for imgui::Textures<glow::Texture> {
fn register(&mut self, gl_texture: glow::Texture) -> Option<imgui::TextureId> {
Some(self.insert(gl_texture))
}
fn gl_texture(&self, imgui_texture: imgui::TextureId) -> Option<glow::Texture> {
self.get(imgui_texture).copied()
}
}
/// This OpenGL state backup is based on the upstream OpenGL example from
/// imgui, where an attempt is made to save and restore the OpenGL context state
/// before and after rendering.
///
/// If you're writing your own renderer, you can likely streamline most of this.
///
/// It is unlikely that any such attempt will be comprehensive for all possible
/// applications, due to the complexity of OpenGL and the possibility of
/// arbitrary extensions. However, it remains as a useful tool for quickly
/// getting started. If your application needs more state to be backed up and
/// restored, it is probably best to do this manually before/after calling
/// the render method rather than opening an issue to add more to this
/// struct.
#[allow(clippy::struct_excessive_bools)]
#[derive(Default)]
pub struct GlStateBackup {
active_texture: u32,
program: u32,
texture: u32,
#[cfg(feature = "bind_sampler_support")]
sampler: Option<u32>,
array_buffer: u32,
#[cfg(feature = "polygon_mode_support")]
polygon_mode: Option<[i32; 2]>,
viewport: [i32; 4],
scissor_box: [i32; 4],
blend_src_rgb: i32,
blend_dst_rgb: i32,
blend_src_alpha: i32,
blend_dst_alpha: i32,
blend_equation_rgb: i32,
blend_equation_alpha: i32,
blend_enabled: bool,
cull_face_enabled: bool,
depth_test_enabled: bool,
stencil_test_enabled: bool,
scissor_test_enabled: bool,
#[cfg(feature = "primitive_restart_support")]
primitive_restart_enabled: Option<bool>,
#[cfg(feature = "bind_vertex_array_support")]
vertex_array_object: Option<u32>,
}
fn to_native_gl<T>(handle: u32, constructor: fn(NonZeroU32) -> T) -> Option<T> {
if handle != 0 {
Some(constructor(NonZeroU32::new(handle).unwrap()))
} else {
None
}
}
impl GlStateBackup {
fn pre_init(&mut self, gl: &Context) {
self.texture = unsafe { gl.get_parameter_i32(glow::TEXTURE_BINDING_2D) as _ };
}
fn post_init(&mut self, gl: &Context) {
#[allow(clippy::cast_sign_loss)]
unsafe {
gl.bind_texture(glow::TEXTURE_2D, to_native_gl(self.texture, glow::NativeTexture));
}
}
fn pre_render(&mut self, gl: &Context, gl_version: GlVersion) {
#[allow(clippy::cast_sign_loss)]
unsafe {
self.active_texture = gl.get_parameter_i32(glow::ACTIVE_TEXTURE) as _;
self.program = gl.get_parameter_i32(glow::CURRENT_PROGRAM) as _;
self.texture = gl.get_parameter_i32(glow::TEXTURE_BINDING_2D) as _;
#[cfg(feature = "bind_sampler_support")]
if gl_version.bind_sampler_support() {
self.sampler = Some(gl.get_parameter_i32(glow::SAMPLER_BINDING) as _);
} else {
self.sampler = None;
}
self.array_buffer = gl.get_parameter_i32(glow::ARRAY_BUFFER_BINDING) as _;
#[cfg(feature = "bind_vertex_array_support")]
if gl_version.bind_vertex_array_support() {
self.vertex_array_object =
Some(gl.get_parameter_i32(glow::VERTEX_ARRAY_BINDING) as _);
}
#[cfg(feature = "polygon_mode_support")]
if gl_version.polygon_mode_support() {
if self.polygon_mode.is_none() {
self.polygon_mode = Some(Default::default());
}
gl.get_parameter_i32_slice(glow::POLYGON_MODE, self.polygon_mode.as_mut().unwrap());
} else {
self.polygon_mode = None;
}
gl.get_parameter_i32_slice(glow::VIEWPORT, &mut self.viewport);
gl.get_parameter_i32_slice(glow::SCISSOR_BOX, &mut self.scissor_box);
self.blend_src_rgb = gl.get_parameter_i32(glow::BLEND_SRC_RGB);
self.blend_dst_rgb = gl.get_parameter_i32(glow::BLEND_DST_RGB);
self.blend_src_alpha = gl.get_parameter_i32(glow::BLEND_SRC_ALPHA);
self.blend_dst_alpha = gl.get_parameter_i32(glow::BLEND_DST_ALPHA);
self.blend_equation_rgb = gl.get_parameter_i32(glow::BLEND_EQUATION_RGB);
self.blend_equation_alpha = gl.get_parameter_i32(glow::BLEND_EQUATION_ALPHA);
self.blend_enabled = gl.is_enabled(glow::BLEND);
self.cull_face_enabled = gl.is_enabled(glow::CULL_FACE);
self.depth_test_enabled = gl.is_enabled(glow::DEPTH_TEST);
self.stencil_test_enabled = gl.is_enabled(glow::STENCIL_TEST);
self.scissor_test_enabled = gl.is_enabled(glow::SCISSOR_TEST);
#[cfg(feature = "primitive_restart_support")]
if gl_version.primitive_restart_support() {
self.primitive_restart_enabled = Some(gl.is_enabled(glow::PRIMITIVE_RESTART));
} else {
self.primitive_restart_enabled = None;
}
}
}
fn post_render(&mut self, gl: &Context, _gl_version: GlVersion) {
#![allow(clippy::cast_sign_loss)]
unsafe {
gl.use_program(to_native_gl(self.program, glow::NativeProgram));
gl.bind_texture(glow::TEXTURE_2D, to_native_gl(self.texture, glow::NativeTexture));
#[cfg(feature = "bind_sampler_support")]
if let Some(sampler) = self.sampler {
gl.bind_sampler(0, to_native_gl(sampler, glow::NativeSampler));
}
gl.active_texture(self.active_texture as _);
#[cfg(feature = "bind_vertex_array_support")]
if let Some(vao) = self.vertex_array_object {
gl.bind_vertex_array(to_native_gl(vao, glow::NativeVertexArray));
}
gl.bind_buffer(glow::ARRAY_BUFFER, to_native_gl(self.array_buffer, glow::NativeBuffer));
gl.blend_equation_separate(
self.blend_equation_rgb as _,
self.blend_equation_alpha as _,
);
gl.blend_func_separate(
self.blend_src_rgb as _,
self.blend_dst_rgb as _,
self.blend_src_alpha as _,
self.blend_dst_alpha as _,
);
if self.blend_enabled {
gl.enable(glow::BLEND)
} else {
gl.disable(glow::BLEND);
}
if self.cull_face_enabled {
gl.enable(glow::CULL_FACE)
} else {
gl.disable(glow::CULL_FACE)
}
if self.depth_test_enabled {
gl.enable(glow::DEPTH_TEST)
} else {
gl.disable(glow::DEPTH_TEST)
}
if self.stencil_test_enabled {
gl.enable(glow::STENCIL_TEST)
} else {
gl.disable(glow::STENCIL_TEST)
}
if self.scissor_test_enabled {
gl.enable(glow::SCISSOR_TEST)
} else {
gl.disable(glow::SCISSOR_TEST)
}
#[cfg(feature = "primitive_restart_support")]
if let Some(restart_enabled) = self.primitive_restart_enabled {
if restart_enabled {
gl.enable(glow::PRIMITIVE_RESTART)
} else {
gl.disable(glow::PRIMITIVE_RESTART)
}
}
#[cfg(feature = "polygon_mode_support")]
if let Some([mode, _]) = self.polygon_mode {
gl.polygon_mode(glow::FRONT_AND_BACK, mode as _);
}
gl.viewport(
self.viewport[0],
self.viewport[1],
self.viewport[2],
self.viewport[3],
);
gl.scissor(
self.scissor_box[0],
self.scissor_box[1],
self.scissor_box[2],
self.scissor_box[3],
);
}
}
}
/// Parses `GL_VERSION` and `GL_SHADING_LANGUAGE_VERSION` at runtime in order to
/// generate shaders which should work on a wide variety of modern devices
/// (GL >= 3.3 and GLES >= 2.0 are expected to work).
struct Shaders {
program: Option<GlProgram>,
texture_uniform_location: GlUniformLocation,
matrix_uniform_location: GlUniformLocation,
position_attribute_index: u32,
uv_attribute_index: u32,
color_attribute_index: u32,
}
impl Shaders {
fn new(gl: &Context, gl_version: GlVersion, output_srgb: bool) -> Result<Self, ShaderError> {
let (vertex_source, fragment_source) =
Self::get_shader_sources(gl, gl_version, output_srgb)?;
let vertex_shader =
unsafe { gl.create_shader(glow::VERTEX_SHADER) }.map_err(ShaderError::CreateShader)?;
unsafe {
gl.shader_source(vertex_shader, &vertex_source);
gl.compile_shader(vertex_shader);
if !gl.get_shader_compile_status(vertex_shader) {
return Err(ShaderError::CompileShader(
gl.get_shader_info_log(vertex_shader),
));
}
}
let fragment_shader = unsafe { gl.create_shader(glow::FRAGMENT_SHADER) }
.map_err(ShaderError::CreateShader)?;
unsafe {
gl.shader_source(fragment_shader, &fragment_source);
gl.compile_shader(fragment_shader);
if !gl.get_shader_compile_status(fragment_shader) {
return Err(ShaderError::CompileShader(
gl.get_shader_info_log(fragment_shader),
));
}
}
let program = unsafe { gl.create_program() }.map_err(ShaderError::CreateProgram)?;
unsafe {
gl.attach_shader(program, vertex_shader);
gl.attach_shader(program, fragment_shader);
gl.link_program(program);
if !gl.get_program_link_status(program) {
return Err(ShaderError::LinkProgram(gl.get_program_info_log(program)));
}
gl.detach_shader(program, vertex_shader);
gl.detach_shader(program, fragment_shader);
gl.delete_shader(vertex_shader);
gl.delete_shader(fragment_shader);
}
Ok(unsafe {
Self {
program: Some(program),
texture_uniform_location: gl
.get_uniform_location(program, "tex")
.ok_or_else(|| ShaderError::UniformNotFound("tex".into()))?,
matrix_uniform_location: gl
.get_uniform_location(program, "matrix")
.ok_or_else(|| ShaderError::UniformNotFound("matrix".into()))?,
position_attribute_index: gl
.get_attrib_location(program, "position")
.ok_or_else(|| ShaderError::AttributeNotFound("position".into()))?,
uv_attribute_index: gl
.get_attrib_location(program, "uv")
.ok_or_else(|| ShaderError::AttributeNotFound("uv".into()))?,
color_attribute_index: gl
.get_attrib_location(program, "color")
.ok_or_else(|| ShaderError::AttributeNotFound("color".into()))?,
}
})
}
fn get_shader_sources(
gl: &Context,
gl_version: GlVersion,
output_srgb: bool,
) -> Result<(String, String), ShaderError> {
const VERTEX_BODY: &str = r#"
layout (location = 0) in vec2 position;
layout (location = 1) in vec2 uv;
layout (location = 2) in vec4 color;
uniform mat4 matrix;
out vec2 fragment_uv;
out vec4 fragment_color;
// Because imgui only specifies sRGB colors
vec4 srgb_to_linear(vec4 srgb_color) {
// Calcuation as documented by OpenGL
vec3 srgb = srgb_color.rgb;
vec3 selector = ceil(srgb - 0.04045);
vec3 less_than_branch = srgb / 12.92;
vec3 greater_than_branch = pow((srgb + 0.055) / 1.055, vec3(2.4));
return vec4(
mix(less_than_branch, greater_than_branch, selector),
srgb_color.a
);
}
void main() {
fragment_uv = uv;
fragment_color = srgb_to_linear(color);
gl_Position = matrix * vec4(position.xy, 0, 1);
}
"#;
const FRAGMENT_BODY: &str = r#"
in vec2 fragment_uv;
in vec4 fragment_color;
uniform sampler2D tex;
layout (location = 0) out vec4 out_color;
vec4 linear_to_srgb(vec4 linear_color) {
vec3 linear = linear_color.rgb;
vec3 selector = ceil(linear - 0.0031308);
vec3 less_than_branch = linear * 12.92;
vec3 greater_than_branch = pow(linear, vec3(1.0/2.4)) * 1.055 - 0.055;
return vec4(
mix(less_than_branch, greater_than_branch, selector),
linear_color.a
);
}
void main() {
vec4 linear_color = fragment_color * texture(tex, fragment_uv.st);
#ifdef OUTPUT_SRGB
out_color = linear_to_srgb(linear_color);
#else
out_color = linear_color;
#endif
}
"#;
let glsl_version = GlslVersion::read(gl);
// Find the lowest common denominator version
let is_gles = gl_version.is_gles || glsl_version.is_gles;
let (major, minor) = if let std::cmp::Ordering::Less = gl_version
.major
.cmp(&glsl_version.major)
.then(gl_version.minor.cmp(&glsl_version.minor))
{
(gl_version.major, gl_version.minor)
} else {
(glsl_version.major, glsl_version.minor)
};
if is_gles && major < 2 {
return Err(ShaderError::IncompatibleVersion(format!(
"This auto-shader OpenGL version 3.0 or OpenGL ES version 2.0 or higher, found: ES {}.{}",
major, minor
)));
}
if !is_gles && major < 3 {
return Err(ShaderError::IncompatibleVersion(format!(
"This auto-shader OpenGL version 3.0 or OpenGL ES version 2.0 or higher, found: {}.{}",
major, minor
)));
}
let vertex_source = format!(
"#version {version}{es_extras}\n{body}",
version = major * 100 + minor * 10,
es_extras = if is_gles {
" es\nprecision mediump float;"
} else {
""
},
body = VERTEX_BODY,
);
let fragment_source = format!(
"#version {version}{es_extras}{defines}\n{body}",
version = major * 100 + minor * 10,
es_extras = if is_gles {
" es\nprecision mediump float;"
} else {
""
},
defines = if output_srgb {
"\n#define OUTPUT_SRGB"
} else {
""
},
body = FRAGMENT_BODY,
);
Ok((vertex_source, fragment_source))
}
}
#[derive(Debug)]
pub enum ShaderError {
IncompatibleVersion(String),
CreateShader(String),
CreateProgram(String),
CompileShader(String),
LinkProgram(String),
UniformNotFound(Cow<'static, str>),
AttributeNotFound(Cow<'static, str>),
}
impl Error for ShaderError {}
impl Display for ShaderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::IncompatibleVersion(msg) => write!(
f,
"Shader not compatible with OpenGL version found in the context: {}",
msg
),
Self::CreateShader(msg) => write!(f, "Error creating shader object: {}", msg),
Self::CreateProgram(msg) => write!(f, "Error creating program object: {}", msg),
Self::CompileShader(msg) => write!(f, "Error compiling shader: {}", msg),
Self::LinkProgram(msg) => write!(f, "Error linking shader program: {}", msg),
Self::UniformNotFound(uniform_name) => {
write!(f, "Uniform `{}` not found in shader program", uniform_name)
}
Self::AttributeNotFound(attribute_name) => {
write!(
f,
"Attribute `{}` not found in shader program",
attribute_name
)
}
}
}
}
#[derive(Debug)]
pub enum InitError {
Shader(ShaderError),
CreateBufferObject(String),
CreateTexture(String),
RegisterTexture,
UserError(String),
}
impl Error for InitError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Shader(error) => Some(error),
_ => None,
}
}
}
impl Display for InitError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Shader(error) => write!(f, "Shader initialisation error: {}", error),
Self::CreateBufferObject(msg) => write!(f, "Error creating buffer object: {}", msg),
Self::CreateTexture(msg) => write!(f, "Error creating texture object: {}", msg),
Self::RegisterTexture => write!(f, "Error registering texture in texture map"),
Self::UserError(msg) => write!(f, "Initialization error: {}", msg),
}
}
}
impl From<ShaderError> for InitError {
fn from(error: ShaderError) -> Self {
Self::Shader(error)
}
}
pub type RenderError = String;
fn prepare_font_atlas<T: TextureMap>(
gl: &Context,
fonts: &mut imgui::FontAtlas,
texture_map: &mut T,
) -> Result<GlTexture, InitError> {
#![allow(clippy::cast_possible_wrap)]
let atlas_texture = fonts.build_rgba32_texture();
let gl_texture = unsafe { gl.create_texture() }.map_err(InitError::CreateTexture)?;
unsafe {
gl.bind_texture(glow::TEXTURE_2D, Some(gl_texture));
gl.tex_parameter_i32(
glow::TEXTURE_2D,
glow::TEXTURE_MIN_FILTER,
glow::LINEAR as _,
);
gl.tex_parameter_i32(
glow::TEXTURE_2D,
glow::TEXTURE_MAG_FILTER,
glow::LINEAR as _,
);
gl.tex_image_2d(
glow::TEXTURE_2D,
0,
glow::SRGB8_ALPHA8 as _,
atlas_texture.width as _,
atlas_texture.height as _,
0,
glow::RGBA,
glow::UNSIGNED_BYTE,
Some(atlas_texture.data),
);
}
fonts.tex_id = texture_map
.register(gl_texture)
.ok_or(InitError::RegisterTexture)?;
Ok(gl_texture)
}
// this CFG guard disables apple usage of this function -- apple only has supported up to opengl 3.3
#[cfg(all(not(target_vendor = "apple"), feature = "debug_message_insert_support"))]
fn gl_debug_message<G: glow::HasContext>(gl: &G, message: impl AsRef<str>) {
unsafe {
gl.debug_message_insert(
glow::DEBUG_SOURCE_APPLICATION,
glow::DEBUG_TYPE_MARKER,
0,
glow::DEBUG_SEVERITY_NOTIFICATION,
message,
)
};
}
#[cfg(any(target_vendor = "apple", not(feature = "debug_message_insert_support")))]
fn gl_debug_message<G: glow::HasContext>(_gl: &G, _message: impl AsRef<str>) {}
fn calculate_matrix(draw_data: &DrawData, clip_origin_is_lower_left: bool) -> [f32; 16] {
#![allow(clippy::deprecated_cfg_attr)]
let left = draw_data.display_pos[0];
let right = draw_data.display_pos[0] + draw_data.display_size[0];
let top = draw_data.display_pos[1];
let bottom = draw_data.display_pos[1] + draw_data.display_size[1];
#[cfg(feature = "clip_origin_support")]
let (top, bottom) = if clip_origin_is_lower_left {
(top, bottom)
} else {
(bottom, top)
};
#[cfg_attr(rustfmt, rustfmt::skip)]
{
[
2.0 / (right - left) , 0.0 , 0.0 , 0.0,
0.0 , (2.0 / (top - bottom)) , 0.0 , 0.0,
0.0 , 0.0 , -1.0, 0.0,
(right + left) / (left - right), (top + bottom) / (bottom - top), 0.0 , 1.0,
]
}
}
unsafe fn to_byte_slice<T>(slice: &[T]) -> &[u8] {
std::slice::from_raw_parts(slice.as_ptr().cast(), slice.len() * size_of::<T>())
}
const fn imgui_index_type_as_gl() -> u32 {
match size_of::<imgui::DrawIdx>() {
1 => glow::UNSIGNED_BYTE,
2 => glow::UNSIGNED_SHORT,
_ => glow::UNSIGNED_INT,
}
}