1#![allow(clippy::collapsible_else_if)]
2#![allow(unsafe_code)]
3
4use std::{collections::HashMap, sync::Arc};
5
6use egui::{
7 emath::Rect,
8 epaint::{Mesh, PaintCallbackInfo, Primitive, Vertex},
9};
10use glow::HasContext as _;
11use memoffset::offset_of;
12
13use crate::check_for_gl_error;
14use crate::misc_util::{compile_shader, link_program};
15use crate::shader_version::ShaderVersion;
16use crate::vao;
17
18pub use glow::Context;
20
21const VERT_SRC: &str = include_str!("shader/vertex.glsl");
22const FRAG_SRC: &str = include_str!("shader/fragment.glsl");
23
24trait TextureFilterExt {
25 fn glow_code(&self, mipmap: Option<egui::TextureFilter>) -> u32;
26}
27
28impl TextureFilterExt for egui::TextureFilter {
29 fn glow_code(&self, mipmap: Option<egui::TextureFilter>) -> u32 {
30 match (self, mipmap) {
31 (Self::Linear, None) => glow::LINEAR,
32 (Self::Nearest, None) => glow::NEAREST,
33 (Self::Linear, Some(Self::Linear)) => glow::LINEAR_MIPMAP_LINEAR,
34 (Self::Nearest, Some(Self::Linear)) => glow::NEAREST_MIPMAP_LINEAR,
35 (Self::Linear, Some(Self::Nearest)) => glow::LINEAR_MIPMAP_NEAREST,
36 (Self::Nearest, Some(Self::Nearest)) => glow::NEAREST_MIPMAP_NEAREST,
37 }
38 }
39}
40
41trait TextureWrapModeExt {
42 fn glow_code(&self) -> u32;
43}
44
45impl TextureWrapModeExt for egui::TextureWrapMode {
46 fn glow_code(&self) -> u32 {
47 match self {
48 Self::ClampToEdge => glow::CLAMP_TO_EDGE,
49 Self::Repeat => glow::REPEAT,
50 Self::MirroredRepeat => glow::MIRRORED_REPEAT,
51 }
52 }
53}
54
55#[derive(Debug)]
56pub struct PainterError(String);
57
58impl std::error::Error for PainterError {}
59
60impl std::fmt::Display for PainterError {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 write!(f, "OpenGL: {}", self.0)
63 }
64}
65
66impl From<String> for PainterError {
67 #[inline]
68 fn from(value: String) -> Self {
69 Self(value)
70 }
71}
72
73pub struct Painter {
83 gl: Arc<glow::Context>,
84
85 max_texture_side: usize,
86
87 program: glow::Program,
88 u_screen_size: glow::UniformLocation,
89 u_sampler: glow::UniformLocation,
90 is_webgl_1: bool,
91 vao: crate::vao::VertexArrayObject,
92 srgb_textures: bool,
93 supports_srgb_framebuffer: bool,
94 vbo: glow::Buffer,
95 element_array_buffer: glow::Buffer,
96
97 textures: HashMap<egui::TextureId, glow::Texture>,
98
99 next_native_tex_id: u64,
100
101 textures_to_destroy: Vec<glow::Texture>,
103
104 destroyed: bool,
106}
107
108pub struct CallbackFn {
118 f: Box<dyn Fn(PaintCallbackInfo, &Painter) + Sync + Send>,
119}
120
121impl CallbackFn {
122 pub fn new<F: Fn(PaintCallbackInfo, &Painter) + Sync + Send + 'static>(callback: F) -> Self {
123 let f = Box::new(callback);
124 Self { f }
125 }
126}
127
128impl Painter {
129 pub fn new(
142 gl: Arc<glow::Context>,
143 shader_prefix: &str,
144 shader_version: Option<ShaderVersion>,
145 dithering: bool,
146 ) -> Result<Self, PainterError> {
147 profiling::function_scope!();
148 crate::check_for_gl_error_even_in_release!(&gl, "before Painter::new");
149
150 unsafe {
152 let version = gl.get_parameter_string(glow::VERSION);
153 let renderer = gl.get_parameter_string(glow::RENDERER);
154 let vendor = gl.get_parameter_string(glow::VENDOR);
155 log::debug!(
156 "\nopengl version: {version}\nopengl renderer: {renderer}\nopengl vendor: {vendor}"
157 );
158 }
159
160 #[cfg(not(target_arch = "wasm32"))]
161 if gl.version().major < 2 {
162 return Err(PainterError("egui_glow requires opengl 2.0+. ".to_owned()));
165 }
166
167 let max_texture_side = unsafe { gl.get_parameter_i32(glow::MAX_TEXTURE_SIZE) } as usize;
168 let shader_version = shader_version.unwrap_or_else(|| ShaderVersion::get(&gl));
169 let is_webgl_1 = shader_version == ShaderVersion::Es100;
170 let shader_version_declaration = shader_version.version_declaration();
171 log::debug!("Shader header: {shader_version_declaration:?}.");
172
173 let supported_extensions = gl.supported_extensions();
174 log::trace!("OpenGL extensions: {supported_extensions:?}");
175 let srgb_textures = false; let supports_srgb_framebuffer = !cfg!(target_arch = "wasm32")
178 && supported_extensions.iter().any(|extension| {
179 extension.ends_with("ARB_framebuffer_sRGB")
181 });
182 log::debug!("SRGB framebuffer Support: {supports_srgb_framebuffer}");
183
184 unsafe {
185 let vert = compile_shader(
186 &gl,
187 glow::VERTEX_SHADER,
188 &format!(
189 "{}\n#define NEW_SHADER_INTERFACE {}\n{}\n{}",
190 shader_version_declaration,
191 shader_version.is_new_shader_interface() as i32,
192 shader_prefix,
193 VERT_SRC
194 ),
195 )?;
196 let frag = compile_shader(
197 &gl,
198 glow::FRAGMENT_SHADER,
199 &format!(
200 "{}\n#define NEW_SHADER_INTERFACE {}\n#define DITHERING {}\n{}\n{}",
201 shader_version_declaration,
202 shader_version.is_new_shader_interface() as i32,
203 dithering as i32,
204 shader_prefix,
205 FRAG_SRC
206 ),
207 )?;
208 let program = link_program(&gl, [vert, frag].iter())?;
209 gl.detach_shader(program, vert);
210 gl.detach_shader(program, frag);
211 gl.delete_shader(vert);
212 gl.delete_shader(frag);
213 let u_screen_size = gl.get_uniform_location(program, "u_screen_size").unwrap();
214 let u_sampler = gl.get_uniform_location(program, "u_sampler").unwrap();
215
216 let vbo = gl.create_buffer()?;
217
218 let a_pos_loc = gl.get_attrib_location(program, "a_pos").unwrap();
219 let a_tc_loc = gl.get_attrib_location(program, "a_tc").unwrap();
220 let a_srgba_loc = gl.get_attrib_location(program, "a_srgba").unwrap();
221
222 let stride = std::mem::size_of::<Vertex>() as i32;
223 let buffer_infos = vec![
224 vao::BufferInfo {
225 location: a_pos_loc,
226 vector_size: 2,
227 data_type: glow::FLOAT,
228 normalized: false,
229 stride,
230 offset: offset_of!(Vertex, pos) as i32,
231 },
232 vao::BufferInfo {
233 location: a_tc_loc,
234 vector_size: 2,
235 data_type: glow::FLOAT,
236 normalized: false,
237 stride,
238 offset: offset_of!(Vertex, uv) as i32,
239 },
240 vao::BufferInfo {
241 location: a_srgba_loc,
242 vector_size: 4,
243 data_type: glow::UNSIGNED_BYTE,
244 normalized: false,
245 stride,
246 offset: offset_of!(Vertex, color) as i32,
247 },
248 ];
249 let vao = crate::vao::VertexArrayObject::new(&gl, vbo, buffer_infos);
250
251 let element_array_buffer = gl.create_buffer()?;
252
253 crate::check_for_gl_error_even_in_release!(&gl, "after Painter::new");
254
255 Ok(Self {
256 gl,
257 max_texture_side,
258 program,
259 u_screen_size,
260 u_sampler,
261 is_webgl_1,
262 vao,
263 srgb_textures,
264 supports_srgb_framebuffer,
265 vbo,
266 element_array_buffer,
267 textures: Default::default(),
268 next_native_tex_id: 1 << 32,
269 textures_to_destroy: Vec::new(),
270 destroyed: false,
271 })
272 }
273 }
274
275 pub fn gl(&self) -> &Arc<glow::Context> {
277 &self.gl
278 }
279
280 pub fn max_texture_side(&self) -> usize {
281 self.max_texture_side
282 }
283
284 #[expect(clippy::unused_self)]
294 pub fn intermediate_fbo(&self) -> Option<glow::Framebuffer> {
295 None
298 }
299
300 unsafe fn prepare_painting(
301 &mut self,
302 [width_in_pixels, height_in_pixels]: [u32; 2],
303 pixels_per_point: f32,
304 ) {
305 unsafe {
306 self.gl.enable(glow::SCISSOR_TEST);
307 self.gl.disable(glow::CULL_FACE);
309 self.gl.disable(glow::DEPTH_TEST);
310
311 self.gl.color_mask(true, true, true, true);
312
313 self.gl.enable(glow::BLEND);
314 self.gl
315 .blend_equation_separate(glow::FUNC_ADD, glow::FUNC_ADD);
316 self.gl.blend_func_separate(
317 glow::ONE,
319 glow::ONE_MINUS_SRC_ALPHA,
320 glow::ONE_MINUS_DST_ALPHA,
323 glow::ONE,
324 );
325
326 if self.supports_srgb_framebuffer {
327 self.gl.disable(glow::FRAMEBUFFER_SRGB);
328 check_for_gl_error!(&self.gl, "FRAMEBUFFER_SRGB");
329 }
330
331 let width_in_points = width_in_pixels as f32 / pixels_per_point;
332 let height_in_points = height_in_pixels as f32 / pixels_per_point;
333
334 self.gl
335 .viewport(0, 0, width_in_pixels as i32, height_in_pixels as i32);
336 self.gl.use_program(Some(self.program));
337
338 self.gl
339 .uniform_2_f32(Some(&self.u_screen_size), width_in_points, height_in_points);
340 self.gl.uniform_1_i32(Some(&self.u_sampler), 0);
341 self.gl.active_texture(glow::TEXTURE0);
342
343 self.vao.bind(&self.gl);
344 self.gl
345 .bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(self.element_array_buffer));
346 }
347
348 check_for_gl_error!(&self.gl, "prepare_painting");
349 }
350
351 pub fn clear(&self, screen_size_in_pixels: [u32; 2], clear_color: [f32; 4]) {
352 clear(&self.gl, screen_size_in_pixels, clear_color);
353 }
354
355 pub fn paint_and_update_textures(
357 &mut self,
358 screen_size_px: [u32; 2],
359 pixels_per_point: f32,
360 clipped_primitives: &[egui::ClippedPrimitive],
361 textures_delta: &egui::TexturesDelta,
362 ) {
363 profiling::function_scope!();
364
365 for (id, image_delta) in &textures_delta.set {
366 self.set_texture(*id, image_delta);
367 }
368
369 self.paint_primitives(screen_size_px, pixels_per_point, clipped_primitives);
370
371 for &id in &textures_delta.free {
372 self.free_texture(id);
373 }
374 }
375
376 pub fn paint_primitives(
397 &mut self,
398 screen_size_px: [u32; 2],
399 pixels_per_point: f32,
400 clipped_primitives: &[egui::ClippedPrimitive],
401 ) {
402 profiling::function_scope!();
403 self.assert_not_destroyed();
404
405 unsafe { self.prepare_painting(screen_size_px, pixels_per_point) };
406
407 for egui::ClippedPrimitive {
408 clip_rect,
409 primitive,
410 } in clipped_primitives
411 {
412 set_clip_rect(&self.gl, screen_size_px, pixels_per_point, *clip_rect);
413
414 match primitive {
415 Primitive::Mesh(mesh) => {
416 self.paint_mesh(mesh);
417 }
418 Primitive::Callback(callback) => {
419 if callback.rect.is_positive() {
420 profiling::scope!("callback");
421
422 let info = egui::PaintCallbackInfo {
423 viewport: callback.rect,
424 clip_rect: *clip_rect,
425 pixels_per_point,
426 screen_size_px,
427 };
428
429 let viewport_px = info.viewport_in_pixels();
430 unsafe {
431 self.gl.viewport(
432 viewport_px.left_px,
433 viewport_px.from_bottom_px,
434 viewport_px.width_px,
435 viewport_px.height_px,
436 );
437 }
438
439 if let Some(callback) = callback.callback.downcast_ref::<CallbackFn>() {
440 (callback.f)(info, self);
441 } else {
442 log::warn!(
443 "Warning: Unsupported render callback. Expected egui_glow::CallbackFn"
444 );
445 }
446
447 check_for_gl_error!(&self.gl, "callback");
448
449 unsafe { self.prepare_painting(screen_size_px, pixels_per_point) };
451 }
452 }
453 }
454 }
455
456 unsafe {
457 self.vao.unbind(&self.gl);
458 self.gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, None);
459
460 self.gl.disable(glow::SCISSOR_TEST);
461
462 check_for_gl_error!(&self.gl, "painting");
463 }
464 }
465
466 #[inline(never)] fn paint_mesh(&mut self, mesh: &Mesh) {
468 debug_assert!(mesh.is_valid(), "Mesh is not valid");
469 if let Some(texture) = self.texture(mesh.texture_id) {
470 unsafe {
471 self.gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.vbo));
472 self.gl.buffer_data_u8_slice(
473 glow::ARRAY_BUFFER,
474 bytemuck::cast_slice(&mesh.vertices),
475 glow::STREAM_DRAW,
476 );
477
478 self.gl
479 .bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(self.element_array_buffer));
480 self.gl.buffer_data_u8_slice(
481 glow::ELEMENT_ARRAY_BUFFER,
482 bytemuck::cast_slice(&mesh.indices),
483 glow::STREAM_DRAW,
484 );
485
486 self.gl.bind_texture(glow::TEXTURE_2D, Some(texture));
487 }
488
489 unsafe {
490 self.gl.draw_elements(
491 glow::TRIANGLES,
492 mesh.indices.len() as i32,
493 glow::UNSIGNED_INT,
494 0,
495 );
496 }
497
498 check_for_gl_error!(&self.gl, "paint_mesh");
499 } else {
500 log::warn!("Failed to find texture {:?}", mesh.texture_id);
501 }
502 }
503
504 pub fn set_texture(&mut self, tex_id: egui::TextureId, delta: &egui::epaint::ImageDelta) {
507 profiling::function_scope!();
508
509 self.assert_not_destroyed();
510
511 let glow_texture = *self
512 .textures
513 .entry(tex_id)
514 .or_insert_with(|| unsafe { self.gl.create_texture().unwrap() });
515 unsafe {
516 self.gl.bind_texture(glow::TEXTURE_2D, Some(glow_texture));
517 }
518
519 match &delta.image {
520 egui::ImageData::Color(image) => {
521 assert_eq!(
522 image.width() * image.height(),
523 image.pixels.len(),
524 "Mismatch between texture size and texel count"
525 );
526
527 let data: &[u8] = bytemuck::cast_slice(image.pixels.as_ref());
528
529 self.upload_texture_srgb(delta.pos, image.size, delta.options, data);
530 }
531 }
532 }
533
534 fn upload_texture_srgb(
535 &mut self,
536 pos: Option<[usize; 2]>,
537 [w, h]: [usize; 2],
538 options: egui::TextureOptions,
539 data: &[u8],
540 ) {
541 profiling::function_scope!();
542 assert_eq!(
543 data.len(),
544 w * h * 4,
545 "Mismatch between texture size and texel count, by {}",
546 data.len() % (w * h * 4)
547 );
548 assert!(
549 w <= self.max_texture_side && h <= self.max_texture_side,
550 "Got a texture image of size {}x{}, but the maximum supported texture side is only {}",
551 w,
552 h,
553 self.max_texture_side
554 );
555
556 unsafe {
557 self.gl.tex_parameter_i32(
558 glow::TEXTURE_2D,
559 glow::TEXTURE_MAG_FILTER,
560 options.magnification.glow_code(None) as i32,
561 );
562 self.gl.tex_parameter_i32(
563 glow::TEXTURE_2D,
564 glow::TEXTURE_MIN_FILTER,
565 options.minification.glow_code(options.mipmap_mode) as i32,
566 );
567
568 self.gl.tex_parameter_i32(
569 glow::TEXTURE_2D,
570 glow::TEXTURE_WRAP_S,
571 options.wrap_mode.glow_code() as i32,
572 );
573 self.gl.tex_parameter_i32(
574 glow::TEXTURE_2D,
575 glow::TEXTURE_WRAP_T,
576 options.wrap_mode.glow_code() as i32,
577 );
578 check_for_gl_error!(&self.gl, "tex_parameter");
579
580 let (internal_format, src_format) = if self.is_webgl_1 {
581 let format = if self.srgb_textures {
582 glow::SRGB_ALPHA
583 } else {
584 glow::RGBA
585 };
586 (format, format)
587 } else if self.srgb_textures {
588 (glow::SRGB8_ALPHA8, glow::RGBA)
589 } else {
590 (glow::RGBA8, glow::RGBA)
591 };
592
593 self.gl.pixel_store_i32(glow::UNPACK_ALIGNMENT, 1);
594
595 let level = 0;
596 if let Some([x, y]) = pos {
597 profiling::scope!("gl.tex_sub_image_2d");
598 self.gl.tex_sub_image_2d(
599 glow::TEXTURE_2D,
600 level,
601 x as _,
602 y as _,
603 w as _,
604 h as _,
605 src_format,
606 glow::UNSIGNED_BYTE,
607 glow::PixelUnpackData::Slice(Some(data)),
608 );
609 check_for_gl_error!(&self.gl, "tex_sub_image_2d");
610 } else {
611 let border = 0;
612 profiling::scope!("gl.tex_image_2d");
613 self.gl.tex_image_2d(
614 glow::TEXTURE_2D,
615 level,
616 internal_format as _,
617 w as _,
618 h as _,
619 border,
620 src_format,
621 glow::UNSIGNED_BYTE,
622 glow::PixelUnpackData::Slice(Some(data)),
623 );
624 check_for_gl_error!(&self.gl, "tex_image_2d");
625 }
626
627 if options.mipmap_mode.is_some() {
628 self.gl.generate_mipmap(glow::TEXTURE_2D);
629 check_for_gl_error!(&self.gl, "generate_mipmap");
630 }
631 }
632 }
633
634 pub fn free_texture(&mut self, tex_id: egui::TextureId) {
635 if let Some(old_tex) = self.textures.remove(&tex_id) {
636 unsafe { self.gl.delete_texture(old_tex) };
637 }
638 }
639
640 pub fn texture(&self, texture_id: egui::TextureId) -> Option<glow::Texture> {
642 self.textures.get(&texture_id).copied()
643 }
644
645 pub fn register_native_texture(&mut self, native: glow::Texture) -> egui::TextureId {
646 self.assert_not_destroyed();
647 let id = egui::TextureId::User(self.next_native_tex_id);
648 self.next_native_tex_id += 1;
649 self.textures.insert(id, native);
650 id
651 }
652
653 pub fn replace_native_texture(&mut self, id: egui::TextureId, replacing: glow::Texture) {
654 if let Some(old_tex) = self.textures.insert(id, replacing) {
655 self.textures_to_destroy.push(old_tex);
656 }
657 }
658
659 pub fn read_screen_rgba(&self, [w, h]: [u32; 2]) -> egui::ColorImage {
660 profiling::function_scope!();
661
662 let mut pixels = vec![0_u8; (w * h * 4) as usize];
663 unsafe {
664 self.gl.read_pixels(
665 0,
666 0,
667 w as _,
668 h as _,
669 glow::RGBA,
670 glow::UNSIGNED_BYTE,
671 glow::PixelPackData::Slice(Some(&mut pixels)),
672 );
673 }
674 let mut flipped = Vec::with_capacity((w * h * 4) as usize);
675 for row in pixels.chunks_exact((w * 4) as usize).rev() {
676 flipped.extend_from_slice(bytemuck::cast_slice(row));
677 }
678 egui::ColorImage::new([w as usize, h as usize], flipped)
679 }
680
681 pub fn read_screen_rgb(&self, [w, h]: [u32; 2]) -> Vec<u8> {
682 profiling::function_scope!();
683 let mut pixels = vec![0_u8; (w * h * 3) as usize];
684 unsafe {
685 self.gl.read_pixels(
686 0,
687 0,
688 w as _,
689 h as _,
690 glow::RGB,
691 glow::UNSIGNED_BYTE,
692 glow::PixelPackData::Slice(Some(&mut pixels)),
693 );
694 }
695 pixels
696 }
697
698 unsafe fn destroy_gl(&self) {
699 unsafe {
700 self.gl.delete_program(self.program);
701 #[expect(clippy::iter_over_hash_type)]
702 for tex in self.textures.values() {
703 self.gl.delete_texture(*tex);
704 }
705 self.gl.delete_buffer(self.vbo);
706 self.gl.delete_buffer(self.element_array_buffer);
707 for t in &self.textures_to_destroy {
708 self.gl.delete_texture(*t);
709 }
710 }
711 }
712
713 pub fn destroy(&mut self) {
716 if !self.destroyed {
717 unsafe {
718 self.destroy_gl();
719 }
720 self.destroyed = true;
721 }
722 }
723
724 fn assert_not_destroyed(&self) {
725 assert!(!self.destroyed, "the egui glow has already been destroyed!");
726 }
727}
728
729pub fn clear(gl: &glow::Context, screen_size_in_pixels: [u32; 2], clear_color: [f32; 4]) {
730 profiling::function_scope!();
731 unsafe {
732 gl.disable(glow::SCISSOR_TEST);
733
734 gl.viewport(
735 0,
736 0,
737 screen_size_in_pixels[0] as i32,
738 screen_size_in_pixels[1] as i32,
739 );
740 gl.clear_color(
741 clear_color[0],
742 clear_color[1],
743 clear_color[2],
744 clear_color[3],
745 );
746 gl.clear(glow::COLOR_BUFFER_BIT);
747 }
748}
749
750impl Drop for Painter {
751 fn drop(&mut self) {
752 if !self.destroyed {
753 log::warn!(
754 "You forgot to call destroy() on the egui glow painter. Resources will leak!"
755 );
756 }
757 }
758}
759
760fn set_clip_rect(
761 gl: &glow::Context,
762 [width_px, height_px]: [u32; 2],
763 pixels_per_point: f32,
764 clip_rect: Rect,
765) {
766 let clip_min_x = pixels_per_point * clip_rect.min.x;
768 let clip_min_y = pixels_per_point * clip_rect.min.y;
769 let clip_max_x = pixels_per_point * clip_rect.max.x;
770 let clip_max_y = pixels_per_point * clip_rect.max.y;
771
772 let clip_min_x = clip_min_x.round() as i32;
774 let clip_min_y = clip_min_y.round() as i32;
775 let clip_max_x = clip_max_x.round() as i32;
776 let clip_max_y = clip_max_y.round() as i32;
777
778 let clip_min_x = clip_min_x.clamp(0, width_px as i32);
780 let clip_min_y = clip_min_y.clamp(0, height_px as i32);
781 let clip_max_x = clip_max_x.clamp(clip_min_x, width_px as i32);
782 let clip_max_y = clip_max_y.clamp(clip_min_y, height_px as i32);
783
784 unsafe {
785 gl.scissor(
786 clip_min_x,
787 height_px as i32 - clip_max_y,
788 clip_max_x - clip_min_x,
789 clip_max_y - clip_min_y,
790 );
791 }
792}