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:
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user