easy_imgui_renderer/
renderer.rs

1use std::mem::size_of;
2
3use crate::glow::{self, HasContext};
4use anyhow::{Result, anyhow};
5use cgmath::{EuclideanSpace, Matrix3, Point2, Transform};
6use easy_imgui::{self as imgui, PlatformIo, TextureId};
7use easy_imgui_opengl::{self as glr};
8use easy_imgui_sys::*;
9use imgui::{Color, Vector2};
10
11/// The main `Renderer` type.
12pub struct Renderer {
13    imgui: imgui::Context,
14    gl: glr::GlContext,
15    bg_color: Option<imgui::Color>,
16    matrix: Option<Matrix3<f32>>,
17    objs: GlObjects,
18}
19
20struct GlObjects {
21    program: glr::Program,
22    vao: glr::VertexArray,
23    vbuf: glr::Buffer,
24    ibuf: glr::Buffer,
25    a_pos_location: u32,
26    a_uv_location: u32,
27    a_color_location: u32,
28    u_matrix_location: glow::UniformLocation,
29    u_tex_location: glow::UniformLocation,
30}
31
32impl Renderer {
33    /// Creates a new renderer object.
34    ///
35    /// You need to provide the OpenGL context yourself.
36    pub fn new(gl: glr::GlContext) -> Result<Renderer> {
37        Self::with_builder(gl, &imgui::ContextBuilder::new())
38    }
39
40    /// Creates a new renderer object.
41    ///
42    /// Just like `new()` but you can specify context parameters.
43    pub fn with_builder(gl: glr::GlContext, builder: &imgui::ContextBuilder) -> Result<Renderer> {
44        let program;
45        let vao;
46        let (vbuf, ibuf);
47        let a_pos_location;
48        let a_uv_location;
49        let a_color_location;
50        let u_matrix_location;
51        let u_tex_location;
52
53        let mut imgui = unsafe { builder.build() };
54
55        unsafe {
56            if !cfg!(target_arch = "wasm32") {
57                imgui.io_mut().inner().add_backend_flags(
58                    imgui::BackendFlags::HasMouseCursors
59                        | imgui::BackendFlags::HasSetMousePos
60                        | imgui::BackendFlags::RendererHasVtxOffset,
61                );
62            }
63            imgui
64                .io_mut()
65                .inner()
66                .add_backend_flags(imgui::BackendFlags::RendererHasTextures);
67
68            let pio = imgui.platform_io_mut();
69            let max_tex_size = gl.get_parameter_i32(glow::MAX_TEXTURE_SIZE);
70            log::info!("Max texture size {max_tex_size}");
71            if pio.Renderer_TextureMaxWidth == 0 || pio.Renderer_TextureMaxWidth > max_tex_size {
72                pio.Renderer_TextureMaxWidth = max_tex_size;
73                pio.Renderer_TextureMaxHeight = max_tex_size;
74            }
75            let fonts = imgui.io_mut().font_atlas_mut().inner();
76            if fonts.TexMaxWidth == 0 || fonts.TexMaxWidth > max_tex_size {
77                fonts.TexMaxWidth = max_tex_size;
78                fonts.TexMaxHeight = max_tex_size;
79            }
80
81            let glsl_version = if cfg!(not(target_arch = "wasm32")) {
82                "#version 150\n"
83            } else {
84                "#version 300 es\n"
85            };
86            program = gl_program_from_source(&gl, Some(glsl_version), include_str!("shader.glsl"))?;
87            vao = glr::VertexArray::generate(&gl)?;
88            gl.bind_vertex_array(Some(vao.id()));
89
90            let a_pos = program.attrib_by_name("pos").unwrap();
91            a_pos_location = a_pos.location();
92            gl.enable_vertex_attrib_array(a_pos_location);
93
94            let a_uv = program.attrib_by_name("uv").unwrap();
95            a_uv_location = a_uv.location();
96            gl.enable_vertex_attrib_array(a_uv_location);
97
98            let a_color = program.attrib_by_name("color").unwrap();
99            a_color_location = a_color.location();
100            gl.enable_vertex_attrib_array(a_color_location);
101
102            let u_matrix = program.uniform_by_name("matrix").unwrap();
103            u_matrix_location = u_matrix.location();
104
105            let u_tex = program.uniform_by_name("tex").unwrap();
106            u_tex_location = u_tex.location();
107
108            vbuf = glr::Buffer::generate(&gl)?;
109            ibuf = glr::Buffer::generate(&gl)?;
110        }
111        Ok(Renderer {
112            imgui,
113            gl,
114            bg_color: Some(Color::new(0.45, 0.55, 0.60, 1.0)),
115            matrix: None,
116            objs: GlObjects {
117                program,
118                vao,
119                vbuf,
120                ibuf,
121                a_pos_location,
122                a_uv_location,
123                a_color_location,
124                u_matrix_location,
125                u_tex_location,
126            },
127        })
128    }
129    /// Gets a reference to the OpenGL context.
130    pub fn gl_context(&self) -> &glr::GlContext {
131        &self.gl
132    }
133    /// Sets the default background color.
134    ///
135    /// The set color will be used for `glClear(GL_COLOR_BUFFER_BIT)`.
136    /// Set to `None` to avoid this, and use [`easy_imgui::UiBuilder::pre_render`] to do whatever clearing
137    /// you need, if anything.
138    pub fn set_background_color(&mut self, color: Option<Color>) {
139        self.bg_color = color;
140    }
141    /// Sets the 2D (3x3) matrix transformation for the UI display.
142    ///
143    /// If you set this matrix to `Some` then it is your responsibility to also call the appropriate `gl.viewport()`.
144    pub fn set_matrix(&mut self, matrix: Option<Matrix3<f32>>) {
145        self.matrix = matrix;
146    }
147    /// Gets the background color.
148    pub fn background_color(&self) -> Option<Color> {
149        self.bg_color
150    }
151    /// Gets the stored Dear ImGui context.
152    pub fn imgui(&mut self) -> &mut imgui::Context {
153        &mut self.imgui
154    }
155    /// Sets the UI size, in logical units, and the scale factor.
156    pub fn set_size(&mut self, size: Vector2, scale: f32) {
157        unsafe {
158            self.imgui.set_size(size, scale);
159        }
160    }
161    /// Gets the UI size, in logical units.
162    pub fn size(&mut self) -> Vector2 {
163        self.imgui.io().display_size()
164    }
165    /// Builds and renders a UI frame, using the `app` [`easy_imgui::UiBuilder`].
166    pub fn do_frame<A: imgui::UiBuilder>(&mut self, app: &mut A) {
167        unsafe {
168            let mut imgui = self.imgui.set_current();
169
170            imgui.do_frame(
171                app,
172                |ctx| {
173                    let display_size = ctx.io().display_size();
174                    let scale = ctx.io().display_scale();
175                    if self.matrix.is_none() {
176                        self.gl.viewport(
177                            0,
178                            0,
179                            (display_size.x * scale) as i32,
180                            (display_size.y * scale) as i32,
181                        );
182                    }
183                    if let Some(bg) = self.bg_color {
184                        self.gl.clear_color(bg.r, bg.g, bg.b, bg.a);
185                        self.gl.clear(glow::COLOR_BUFFER_BIT);
186                    }
187                    Self::update_textures(&self.gl, ctx.platform_io_mut());
188                },
189                |draw_data| {
190                    Self::render(&self.gl, &self.objs, draw_data, self.matrix.as_ref());
191                },
192            );
193        }
194    }
195
196    unsafe fn update_textures(gl: &glr::GlContext, pio: &mut PlatformIo) {
197        unsafe {
198            for tex in pio.textures_mut() {
199                Self::update_texture(gl, tex);
200            }
201        }
202    }
203    unsafe fn update_texture(gl: &glr::GlContext, tex: &mut ImTextureData) {
204        unsafe {
205            match tex.Status {
206                ImTextureStatus::ImTextureStatus_WantCreate => {
207                    log::debug!("Texture create {}", tex.UniqueID);
208                    let pixels = tex.Pixels;
209                    let tex_id = glr::Texture::generate(gl).unwrap();
210                    gl.bind_texture(glow::TEXTURE_2D, Some(tex_id.id()));
211                    gl.tex_parameter_i32(
212                        glow::TEXTURE_2D,
213                        glow::TEXTURE_WRAP_S,
214                        glow::CLAMP_TO_EDGE as i32,
215                    );
216                    gl.tex_parameter_i32(
217                        glow::TEXTURE_2D,
218                        glow::TEXTURE_WRAP_T,
219                        glow::CLAMP_TO_EDGE as i32,
220                    );
221                    gl.tex_parameter_i32(
222                        glow::TEXTURE_2D,
223                        glow::TEXTURE_MIN_FILTER,
224                        glow::LINEAR as i32,
225                    );
226                    gl.tex_parameter_i32(
227                        glow::TEXTURE_2D,
228                        glow::TEXTURE_MAG_FILTER,
229                        glow::LINEAR as i32,
230                    );
231                    gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MAX_LEVEL, 0);
232                    gl.pixel_store_i32(glow::UNPACK_ROW_LENGTH, 0);
233                    gl.tex_image_2d(
234                        glow::TEXTURE_2D,
235                        0,
236                        glow::RGBA as i32,
237                        tex.Width,
238                        tex.Height,
239                        0,
240                        glow::RGBA,
241                        glow::UNSIGNED_BYTE,
242                        glow::PixelUnpackData::Slice(Some(std::slice::from_raw_parts(
243                            pixels,
244                            (tex.Width * tex.Height * 4) as usize,
245                        ))),
246                    );
247                    gl.bind_texture(glow::TEXTURE_2D, None);
248                    tex.Status = ImTextureStatus::ImTextureStatus_OK;
249                    // Warning: SetTexUserId() is inline;
250                    tex.TexID = Self::map_tex(tex_id.id()).id();
251                    std::mem::forget(tex_id);
252                }
253                ImTextureStatus::ImTextureStatus_WantUpdates => {
254                    log::debug!("Texture update {}", tex.UniqueID);
255                    let tex_id = Self::unmap_tex(TextureId::from_id(tex.TexID)).unwrap();
256                    gl.bind_texture(glow::TEXTURE_2D, Some(tex_id));
257                    gl.pixel_store_i32(glow::UNPACK_ROW_LENGTH, tex.Width);
258                    // TODO: GL ES doesn't have GL_UNPACK_ROW_LENGTH, so we need to (A) copy to a contiguous buffer or (B) upload line by line.
259                    for r in &tex.Updates {
260                        let ptr = tex.Pixels.offset(
261                            ((i32::from(r.x) + i32::from(r.y) * tex.Width) * tex.BytesPerPixel)
262                                as isize,
263                        );
264                        let mem = std::slice::from_raw_parts(
265                            ptr,
266                            (4 * i32::from(r.h) * tex.Width) as usize,
267                        );
268                        gl.tex_sub_image_2d(
269                            glow::TEXTURE_2D,
270                            0,
271                            i32::from(r.x),
272                            i32::from(r.y),
273                            i32::from(r.w),
274                            i32::from(r.h),
275                            glow::RGBA,
276                            glow::UNSIGNED_BYTE,
277                            glow::PixelUnpackData::Slice(Some(mem)),
278                        );
279                    }
280                    gl.pixel_store_i32(glow::UNPACK_ROW_LENGTH, 0);
281                    tex.Status = ImTextureStatus::ImTextureStatus_OK;
282                    gl.bind_texture(glow::TEXTURE_2D, None);
283                }
284                ImTextureStatus::ImTextureStatus_WantDestroy => {
285                    log::debug!("Texture destroy {}", tex.UniqueID);
286                    if let Some(tex_id) = Self::delete_tex(TextureId::from_id(tex.TexID)) {
287                        gl.delete_texture(tex_id);
288                        tex.TexID = 0;
289                    }
290                    tex.Status = ImTextureStatus::ImTextureStatus_Destroyed;
291                }
292                _ => {}
293            }
294        }
295    }
296
297    unsafe fn render(
298        gl: &glow::Context,
299        objs: &GlObjects,
300        draw_data: &ImDrawData,
301        matrix: Option<&Matrix3<f32>>,
302    ) {
303        unsafe {
304            enum ScissorViewportMatrix {
305                Default,
306                Custom(Matrix3<f32>),
307                None,
308            }
309            let default_matrix;
310            let (matrix, viewport_matrix) = match matrix {
311                None => {
312                    let ImVec2 { x: left, y: top } = draw_data.DisplayPos;
313                    let ImVec2 {
314                        x: width,
315                        y: height,
316                    } = draw_data.DisplaySize;
317                    let right = left + width;
318                    let bottom = top + height;
319                    gl.enable(glow::SCISSOR_TEST);
320                    default_matrix = Matrix3::new(
321                        2.0 / width,
322                        0.0,
323                        0.0,
324                        0.0,
325                        -2.0 / height,
326                        0.0,
327                        -(right + left) / width,
328                        (top + bottom) / height,
329                        1.0,
330                    );
331                    (&default_matrix, ScissorViewportMatrix::Default)
332                }
333                Some(matrix) => {
334                    // If there is a custom matrix we have to compute the scissor rectangle in viewport coordinates.
335                    // This only works if the transformed scissor rectangle is axis aligned, ie the rotation is 0°, 90°, 180° or 270°.
336                    // TODO: for other angles a fragment shader would be needed, maybe with a `discard`.
337                    // A rotation of multiple of 90° always has two 0 in the matrix:
338                    // * 0° and 180°: at (0,0) and (1,1), the sines.
339                    // * 90° and 270°: at (0,1) and (1,0), the cosines.
340                    if (matrix[0][0].abs() < f32::EPSILON && matrix[1][1].abs() < f32::EPSILON)
341                        || (matrix[1][0].abs() < f32::EPSILON && matrix[0][1].abs() < f32::EPSILON)
342                    {
343                        let mut viewport = [0; 4];
344                        gl.get_parameter_i32_slice(glow::VIEWPORT, &mut viewport);
345                        let viewport_x = viewport[0] as f32;
346                        let viewport_y = viewport[1] as f32;
347                        let viewport_w2 = viewport[2] as f32 / 2.0;
348                        let viewport_h2 = viewport[3] as f32 / 2.0;
349                        let vm = Matrix3::new(
350                            viewport_w2,
351                            0.0,
352                            0.0,
353                            0.0,
354                            viewport_h2,
355                            0.0,
356                            viewport_x + viewport_w2,
357                            viewport_y + viewport_h2,
358                            1.0,
359                        );
360                        gl.enable(glow::SCISSOR_TEST);
361                        (matrix, ScissorViewportMatrix::Custom(vm * matrix))
362                    } else {
363                        gl.disable(glow::SCISSOR_TEST);
364                        (matrix, ScissorViewportMatrix::None)
365                    }
366                }
367            };
368
369            gl.bind_vertex_array(Some(objs.vao.id()));
370            gl.use_program(Some(objs.program.id()));
371            gl.bind_buffer(glow::ARRAY_BUFFER, Some(objs.vbuf.id()));
372            gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(objs.ibuf.id()));
373            gl.enable(glow::BLEND);
374            gl.blend_func_separate(
375                glow::SRC_ALPHA,
376                glow::ONE_MINUS_SRC_ALPHA,
377                glow::ONE,
378                glow::ONE_MINUS_SRC_ALPHA,
379            );
380            gl.disable(glow::CULL_FACE);
381            gl.disable(glow::DEPTH_TEST);
382
383            gl.active_texture(glow::TEXTURE0);
384            gl.uniform_1_i32(Some(&objs.u_tex_location), 0);
385
386            gl.uniform_matrix_3_f32_slice(
387                Some(&objs.u_matrix_location),
388                false,
389                AsRef::<[f32; 9]>::as_ref(matrix),
390            );
391
392            for cmd_list in &draw_data.CmdLists {
393                let cmd_list = &**cmd_list;
394
395                gl.buffer_data_u8_slice(
396                    glow::ARRAY_BUFFER,
397                    glr::as_u8_slice(&cmd_list.VtxBuffer),
398                    glow::DYNAMIC_DRAW,
399                );
400                gl.buffer_data_u8_slice(
401                    glow::ELEMENT_ARRAY_BUFFER,
402                    glr::as_u8_slice(&cmd_list.IdxBuffer),
403                    glow::DYNAMIC_DRAW,
404                );
405                let stride = size_of::<ImDrawVert>() as i32;
406                gl.vertex_attrib_pointer_f32(
407                    objs.a_pos_location,
408                    2, /*xy*/
409                    glow::FLOAT,
410                    false,
411                    stride,
412                    0,
413                );
414                gl.vertex_attrib_pointer_f32(
415                    objs.a_uv_location,
416                    2, /*xy*/
417                    glow::FLOAT,
418                    false,
419                    stride,
420                    8,
421                );
422                gl.vertex_attrib_pointer_f32(
423                    objs.a_color_location,
424                    4, /*rgba*/
425                    glow::UNSIGNED_BYTE,
426                    true,
427                    stride,
428                    16,
429                );
430
431                for cmd in &cmd_list.CmdBuffer {
432                    match viewport_matrix {
433                        ScissorViewportMatrix::Default => {
434                            let clip_x = cmd.ClipRect.x - draw_data.DisplayPos.x;
435                            let clip_y = cmd.ClipRect.y - draw_data.DisplayPos.y;
436                            let clip_w = cmd.ClipRect.z - cmd.ClipRect.x;
437                            let clip_h = cmd.ClipRect.w - cmd.ClipRect.y;
438                            let scale = draw_data.FramebufferScale.x;
439                            gl.scissor(
440                                (clip_x * scale) as i32,
441                                ((draw_data.DisplaySize.y - (clip_y + clip_h)) * scale) as i32,
442                                (clip_w * scale) as i32,
443                                (clip_h * scale) as i32,
444                            );
445                        }
446                        ScissorViewportMatrix::Custom(vm) => {
447                            let pos = Vector2::new(draw_data.DisplayPos.x, draw_data.DisplayPos.y);
448                            let clip_aa = Vector2::new(cmd.ClipRect.x, cmd.ClipRect.y) - pos;
449                            let clip_bb = Vector2::new(cmd.ClipRect.z, cmd.ClipRect.w) - pos;
450                            let clip_aa = vm.transform_point(Point2::from_vec(clip_aa));
451                            let clip_bb = vm.transform_point(Point2::from_vec(clip_bb));
452                            gl.scissor(
453                                clip_aa.x.min(clip_bb.x).round() as i32,
454                                clip_aa.y.min(clip_bb.y).round() as i32,
455                                (clip_bb.x - clip_aa.x).abs().round() as i32,
456                                (clip_bb.y - clip_aa.y).abs().round() as i32,
457                            );
458                        }
459                        ScissorViewportMatrix::None => {}
460                    }
461
462                    match cmd.UserCallback {
463                        Some(cb) => {
464                            cb(cmd_list, cmd);
465                        }
466                        None => {
467                            // bindgen: inline function
468                            let tex_id = if cmd.TexRef._TexData.is_null() {
469                                cmd.TexRef._TexID
470                            } else {
471                                (*cmd.TexRef._TexData).TexID
472                            };
473                            let tex_id = TextureId::from_id(tex_id);
474                            gl.bind_texture(glow::TEXTURE_2D, Self::unmap_tex(tex_id));
475
476                            if cfg!(target_arch = "wasm32") {
477                                gl.draw_elements(
478                                    glow::TRIANGLES,
479                                    cmd.ElemCount as i32,
480                                    if size_of::<ImDrawIdx>() == 2 {
481                                        glow::UNSIGNED_SHORT
482                                    } else {
483                                        glow::UNSIGNED_INT
484                                    },
485                                    (size_of::<ImDrawIdx>() * cmd.IdxOffset as usize) as i32,
486                                );
487                            } else {
488                                gl.draw_elements_base_vertex(
489                                    glow::TRIANGLES,
490                                    cmd.ElemCount as i32,
491                                    if size_of::<ImDrawIdx>() == 2 {
492                                        glow::UNSIGNED_SHORT
493                                    } else {
494                                        glow::UNSIGNED_INT
495                                    },
496                                    (size_of::<ImDrawIdx>() * cmd.IdxOffset as usize) as i32,
497                                    cmd.VtxOffset as i32,
498                                );
499                            }
500                        }
501                    }
502                }
503            }
504            gl.use_program(None);
505            gl.bind_vertex_array(None);
506            gl.disable(glow::SCISSOR_TEST);
507        }
508    }
509    /// Maps an OpenGL texture to an ImGui texture.
510    pub fn map_tex(ntex: glow::Texture) -> TextureId {
511        #[cfg(target_arch = "wasm32")]
512        {
513            let mut tex_map = WASM_TEX_MAP.lock().unwrap();
514            let id = match tex_map.iter().position(|t| t.is_none()) {
515                Some(free) => {
516                    tex_map[free] = Some(ntex);
517                    free
518                }
519                None => {
520                    let id = tex_map.len();
521                    tex_map.push(Some(ntex));
522                    id
523                }
524            };
525            unsafe { TextureId::from_id(id as u64) }
526        }
527        #[cfg(not(target_arch = "wasm32"))]
528        {
529            unsafe { TextureId::from_id(ntex.0.get() as ImTextureID) }
530        }
531    }
532    /// Gets an OpenGL texture from an ImGui texture.
533    pub fn unmap_tex(tex: TextureId) -> Option<glow::Texture> {
534        #[cfg(target_arch = "wasm32")]
535        {
536            let tex_map = WASM_TEX_MAP.lock().unwrap();
537            let id = tex.id() as usize;
538            tex_map.get(id).cloned().flatten()
539        }
540        #[cfg(not(target_arch = "wasm32"))]
541        {
542            Some(glow::NativeTexture(std::num::NonZeroU32::new(
543                tex.id() as u32
544            )?))
545        }
546    }
547
548    pub fn delete_tex(tex: TextureId) -> Option<glow::Texture> {
549        #[cfg(target_arch = "wasm32")]
550        {
551            let mut tex_map = WASM_TEX_MAP.lock().unwrap();
552            let id = tex.id() as usize;
553            tex_map.get_mut(id).map(|x| x.take()).flatten()
554        }
555
556        #[cfg(not(target_arch = "wasm32"))]
557        {
558            Self::unmap_tex(tex)
559        }
560    }
561}
562
563#[cfg(target_arch = "wasm32")]
564static WASM_TEX_MAP: std::sync::Mutex<Vec<Option<glow::Texture>>> =
565    std::sync::Mutex::new(Vec::new());
566
567impl Drop for Renderer {
568    fn drop(&mut self) {
569        unsafe {
570            let gl = self.gl.clone();
571            let imgui = self.imgui();
572            imgui.io_mut().font_atlas_mut().inner().Clear();
573
574            // Destroy all textures
575            for tex in imgui.platform_io_mut().textures_mut() {
576                if tex.RefCount == 1 {
577                    tex.Status = ImTextureStatus::ImTextureStatus_WantDestroy;
578                    Self::update_texture(&gl, tex);
579                }
580            }
581        }
582    }
583}
584
585pub fn gl_program_from_source(
586    gl: &glr::GlContext,
587    prefix: Option<&str>,
588    shaders: &str,
589) -> Result<glr::Program> {
590    let split = shaders
591        .find("###")
592        .ok_or_else(|| anyhow!("shader marker not found"))?;
593    let vertex = &shaders[..split];
594    let frag = &shaders[split..];
595    let split_2 = frag
596        .find('\n')
597        .ok_or_else(|| anyhow!("shader marker not valid"))?;
598
599    let mut frag = &frag[split_2 + 1..];
600
601    let geom = if let Some(split) = frag.find("###") {
602        let geom = &frag[split..];
603        frag = &frag[..split];
604        let split_2 = geom
605            .find('\n')
606            .ok_or_else(|| anyhow!("shader marker not valid"))?;
607        Some(&geom[split_2 + 1..])
608    } else {
609        None
610    };
611
612    use std::borrow::Cow;
613
614    let (vertex, frag, geom) = match prefix {
615        None => (
616            Cow::Borrowed(vertex),
617            Cow::Borrowed(frag),
618            geom.map(Cow::Borrowed),
619        ),
620        Some(prefix) => (
621            Cow::Owned(format!("{prefix}{vertex}")),
622            Cow::Owned(format!("{prefix}{frag}")),
623            geom.map(|s| Cow::Owned(format!("{prefix}{s}"))),
624        ),
625    };
626    let prg = glr::Program::from_source(gl, &vertex, &frag, geom.as_deref())?;
627    Ok(prg)
628}