1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-29 04:40:03 -04:00

Texture loading in egui (#1110)

* Move texture allocation into epaint/egui proper
* Add TextureHandle
* egui_glow: cast using bytemuck instead of unsafe code
* Optimize glium painter
* Optimize WebGL
* Add example of loading an image from file
This commit is contained in:
Emil Ernerfeldt
2022-01-15 13:59:52 +01:00
committed by GitHub
parent 6c616a1b69
commit 66d80e2519
59 changed files with 1297 additions and 860 deletions

View File

@@ -3,6 +3,8 @@ All notable changes to the `egui_glium` integration will be noted in this file.
## Unreleased
* `EguiGlium::run` no longer returns the shapes to paint, but stores them internally until you call `EguiGlium::paint` ([#1110](https://github.com/emilk/egui/pull/1110)).
* Optimize the painter and texture uploading ([#1110](https://github.com/emilk/egui/pull/1110)).
## 0.16.0 - 2021-12-29

View File

@@ -23,10 +23,15 @@ include = [
all-features = true
[dependencies]
egui = { version = "0.16.0", path = "../egui", default-features = false, features = ["single_threaded"] }
egui = { version = "0.16.0", path = "../egui", default-features = false, features = [
"convert_bytemuck",
"single_threaded",
] }
egui-winit = { version = "0.16.0", path = "../egui-winit", default-features = false, features = ["epi"] }
epi = { version = "0.16.0", path = "../epi", optional = true }
ahash = "0.7"
bytemuck = "1.7"
glium = "0.31"
[dev-dependencies]

View File

@@ -62,7 +62,7 @@ fn main() {
let mut redraw = || {
let mut quit = false;
let (needs_repaint, shapes) = egui_glium.run(&display, |egui_ctx| {
let needs_repaint = egui_glium.run(&display, |egui_ctx| {
egui::SidePanel::left("my_side_panel").show(egui_ctx, |ui| {
if ui
.add(egui::Button::image_and_text(
@@ -98,7 +98,7 @@ fn main() {
// draw things behind egui here
egui_glium.paint(&display, &mut target, shapes);
egui_glium.paint(&display, &mut target);
// draw things on top of egui here

View File

@@ -32,7 +32,7 @@ fn main() {
let mut redraw = || {
let mut quit = false;
let (needs_repaint, shapes) = egui_glium.run(&display, |egui_ctx| {
let needs_repaint = egui_glium.run(&display, |egui_ctx| {
egui::SidePanel::left("my_side_panel").show(egui_ctx, |ui| {
ui.heading("Hello World!");
if ui.button("Quit").clicked() {
@@ -59,7 +59,7 @@ fn main() {
// draw things behind egui here
egui_glium.paint(&display, &mut target, shapes);
egui_glium.paint(&display, &mut target);
// draw things on top of egui here

View File

@@ -66,11 +66,11 @@ pub fn run(app: Box<dyn epi::App>, native_options: &epi::NativeOptions) -> ! {
std::thread::sleep(std::time::Duration::from_millis(10));
}
let (needs_repaint, mut tex_allocation_data, shapes) =
let (needs_repaint, mut textures_delta, shapes) =
integration.update(display.gl_window().window());
let clipped_meshes = integration.egui_ctx.tessellate(shapes);
for (id, image) in tex_allocation_data.creations {
for (id, image) in textures_delta.set {
painter.set_texture(&display, id, &image);
}
@@ -86,13 +86,12 @@ pub fn run(app: Box<dyn epi::App>, native_options: &epi::NativeOptions) -> ! {
&mut target,
integration.egui_ctx.pixels_per_point(),
clipped_meshes,
&integration.egui_ctx.font_image(),
);
target.finish().unwrap();
}
for id in tex_allocation_data.destructions.drain(..) {
for id in textures_delta.free.drain(..) {
painter.free_texture(id);
}

View File

@@ -104,6 +104,9 @@ pub struct EguiGlium {
pub egui_ctx: egui::Context,
pub egui_winit: egui_winit::State,
pub painter: crate::Painter,
shapes: Vec<egui::epaint::ClippedShape>,
textures_delta: egui::TexturesDelta,
}
impl EguiGlium {
@@ -112,6 +115,8 @@ impl EguiGlium {
egui_ctx: Default::default(),
egui_winit: egui_winit::State::new(display.gl_window().window()),
painter: crate::Painter::new(display),
shapes: Default::default(),
textures_delta: Default::default(),
}
}
@@ -125,35 +130,45 @@ impl EguiGlium {
self.egui_winit.on_event(&self.egui_ctx, event)
}
/// Returns `needs_repaint` and shapes to draw.
pub fn run(
&mut self,
display: &glium::Display,
run_ui: impl FnMut(&egui::Context),
) -> (bool, Vec<egui::epaint::ClippedShape>) {
/// Returns `true` if egui requests a repaint.
///
/// Call [`Self::paint`] later to paint.
pub fn run(&mut self, display: &glium::Display, run_ui: impl FnMut(&egui::Context)) -> bool {
let raw_input = self
.egui_winit
.take_egui_input(display.gl_window().window());
let (egui_output, shapes) = self.egui_ctx.run(raw_input, run_ui);
let needs_repaint = egui_output.needs_repaint;
self.egui_winit
.handle_output(display.gl_window().window(), &self.egui_ctx, egui_output);
(needs_repaint, shapes)
let textures_delta = self.egui_winit.handle_output(
display.gl_window().window(),
&self.egui_ctx,
egui_output,
);
self.shapes = shapes;
self.textures_delta.append(textures_delta);
needs_repaint
}
pub fn paint<T: glium::Surface>(
&mut self,
display: &glium::Display,
target: &mut T,
shapes: Vec<egui::epaint::ClippedShape>,
) {
/// Paint the results of the last call to [`Self::run`].
pub fn paint<T: glium::Surface>(&mut self, display: &glium::Display, target: &mut T) {
let shapes = std::mem::take(&mut self.shapes);
let mut textures_delta = std::mem::take(&mut self.textures_delta);
for (id, image) in textures_delta.set {
self.painter.set_texture(display, id, &image);
}
let clipped_meshes = self.egui_ctx.tessellate(shapes);
self.painter.paint_meshes(
display,
target,
self.egui_ctx.pixels_per_point(),
clipped_meshes,
&self.egui_ctx.font_image(),
);
for id in textures_delta.free.drain(..) {
self.painter.free_texture(id);
}
}
}

View File

@@ -2,10 +2,8 @@
#![allow(semicolon_in_expressions_from_macros)] // glium::program! macro
use {
egui::{
emath::Rect,
epaint::{Color32, Mesh},
},
ahash::AHashMap,
egui::{emath::Rect, epaint::Mesh},
glium::{
implement_vertex,
index::PrimitiveType,
@@ -14,19 +12,17 @@ use {
uniform,
uniforms::{MagnifySamplerFilter, SamplerWrapFunction},
},
std::{collections::HashMap, rc::Rc},
std::rc::Rc,
};
pub struct Painter {
program: glium::Program,
egui_texture: Option<SrgbTexture2d>,
egui_texture_version: Option<u64>,
/// Index is the same as in [`egui::TextureId::User`].
user_textures: HashMap<u64, Rc<SrgbTexture2d>>,
textures: AHashMap<egui::TextureId, Rc<SrgbTexture2d>>,
#[cfg(feature = "epi")]
next_native_tex_id: u64, // TODO: 128-bit texture space?
/// [`egui::TextureId::User`] index
next_native_tex_id: u64,
}
impl Painter {
@@ -54,40 +50,12 @@ impl Painter {
Painter {
program,
egui_texture: None,
egui_texture_version: None,
user_textures: Default::default(),
textures: Default::default(),
#[cfg(feature = "epi")]
next_native_tex_id: 1 << 32,
next_native_tex_id: 0,
}
}
pub fn upload_egui_texture(
&mut self,
facade: &dyn glium::backend::Facade,
font_image: &egui::FontImage,
) {
if self.egui_texture_version == Some(font_image.version) {
return; // No change
}
let pixels: Vec<Vec<(u8, u8, u8, u8)>> = font_image
.pixels
.chunks(font_image.width as usize)
.map(|row| {
row.iter()
.map(|&a| Color32::from_white_alpha(a).to_tuple())
.collect()
})
.collect();
let format = texture::SrgbFormat::U8U8U8U8;
let mipmaps = texture::MipmapsOption::NoMipmap;
self.egui_texture =
Some(SrgbTexture2d::with_format(facade, pixels, format, mipmaps).unwrap());
self.egui_texture_version = Some(font_image.version);
}
/// Main entry-point for painting a frame.
/// You should call `target.clear_color(..)` before
/// and `target.finish()` after this.
@@ -97,10 +65,7 @@ impl Painter {
target: &mut T,
pixels_per_point: f32,
cipped_meshes: Vec<egui::ClippedMesh>,
font_image: &egui::FontImage,
) {
self.upload_egui_texture(display, font_image);
for egui::ClippedMesh(clip_rect, mesh) in cipped_meshes {
self.paint_mesh(target, display, pixels_per_point, clip_rect, &mesh);
}
@@ -118,7 +83,8 @@ impl Painter {
debug_assert!(mesh.is_valid());
let vertex_buffer = {
#[derive(Copy, Clone)]
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct Vertex {
a_pos: [f32; 2],
a_tc: [f32; 2],
@@ -126,18 +92,10 @@ impl Painter {
}
implement_vertex!(Vertex, a_pos, a_tc, a_srgba);
let vertices: Vec<Vertex> = mesh
.vertices
.iter()
.map(|v| Vertex {
a_pos: [v.pos.x, v.pos.y],
a_tc: [v.uv.x, v.uv.y],
a_srgba: v.color.to_array(),
})
.collect();
let vertices: &[Vertex] = bytemuck::cast_slice(&mesh.vertices);
// TODO: we should probably reuse the `VertexBuffer` instead of allocating a new one each frame.
glium::VertexBuffer::new(display, &vertices).unwrap()
glium::VertexBuffer::new(display, vertices).unwrap()
};
// TODO: we should probably reuse the `IndexBuffer` instead of allocating a new one each frame.
@@ -223,41 +181,48 @@ impl Painter {
// ------------------------------------------------------------------------
#[cfg(feature = "epi")]
pub fn set_texture(
&mut self,
facade: &dyn glium::backend::Facade,
tex_id: u64,
image: &epi::Image,
tex_id: egui::TextureId,
image: &egui::ImageData,
) {
assert_eq!(
image.size[0] * image.size[1],
image.pixels.len(),
"Mismatch between texture size and texel count"
);
let pixels: Vec<Vec<(u8, u8, u8, u8)>> = image
.pixels
.chunks(image.size[0] as usize)
.map(|row| row.iter().map(|srgba| srgba.to_tuple()).collect())
.collect();
let pixels: Vec<(u8, u8, u8, u8)> = match image {
egui::ImageData::Color(image) => {
assert_eq!(
image.width() * image.height(),
image.pixels.len(),
"Mismatch between texture size and texel count"
);
image.pixels.iter().map(|color| color.to_tuple()).collect()
}
egui::ImageData::Alpha(image) => {
let gamma = 1.0;
image
.srgba_pixels(gamma)
.map(|color| color.to_tuple())
.collect()
}
};
let glium_image = glium::texture::RawImage2d {
data: std::borrow::Cow::Owned(pixels),
width: image.width() as _,
height: image.height() as _,
format: glium::texture::ClientFormat::U8U8U8U8,
};
let format = texture::SrgbFormat::U8U8U8U8;
let mipmaps = texture::MipmapsOption::NoMipmap;
let gl_texture = SrgbTexture2d::with_format(facade, pixels, format, mipmaps).unwrap();
let gl_texture = SrgbTexture2d::with_format(facade, glium_image, format, mipmaps).unwrap();
self.user_textures.insert(tex_id, gl_texture.into());
self.textures.insert(tex_id, gl_texture.into());
}
pub fn free_texture(&mut self, tex_id: u64) {
self.user_textures.remove(&tex_id);
pub fn free_texture(&mut self, tex_id: egui::TextureId) {
self.textures.remove(&tex_id);
}
fn get_texture(&self, texture_id: egui::TextureId) -> Option<&SrgbTexture2d> {
match texture_id {
egui::TextureId::Egui => self.egui_texture.as_ref(),
egui::TextureId::User(id) => self.user_textures.get(&id).map(|rc| rc.as_ref()),
}
self.textures.get(&texture_id).map(|rc| rc.as_ref())
}
}
@@ -266,15 +231,13 @@ impl epi::NativeTexture for Painter {
type Texture = Rc<SrgbTexture2d>;
fn register_native_texture(&mut self, native: Self::Texture) -> egui::TextureId {
let id = self.next_native_tex_id;
let id = egui::TextureId::User(self.next_native_tex_id);
self.next_native_tex_id += 1;
self.user_textures.insert(id, native);
egui::TextureId::User(id as u64)
self.textures.insert(id, native);
id
}
fn replace_native_texture(&mut self, id: egui::TextureId, replacing: Self::Texture) {
if let egui::TextureId::User(id) = id {
self.user_textures.insert(id, replacing);
}
self.textures.insert(id, replacing);
}
}