1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-31 05: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

@@ -1,5 +1,6 @@
use crate::*;
use egui::TexturesDelta;
pub use egui::{pos2, Color32};
// ----------------------------------------------------------------------------
@@ -92,7 +93,7 @@ pub struct AppRunner {
screen_reader: crate::screen_reader::ScreenReader,
pub(crate) text_cursor_pos: Option<egui::Pos2>,
pub(crate) mutable_text_under_cursor: bool,
pending_texture_destructions: Vec<u64>,
textures_delta: TexturesDelta,
}
impl AppRunner {
@@ -139,7 +140,7 @@ impl AppRunner {
screen_reader: Default::default(),
text_cursor_pos: None,
mutable_text_under_cursor: false,
pending_texture_destructions: Default::default(),
textures_delta: Default::default(),
};
{
@@ -183,7 +184,10 @@ impl AppRunner {
Ok(())
}
pub fn logic(&mut self) -> Result<(egui::Output, Vec<egui::ClippedMesh>), JsValue> {
/// Returns `true` if egui requests a repaint.
///
/// Call [`Self::paint`] later to paint
pub fn logic(&mut self) -> Result<(bool, Vec<egui::ClippedMesh>), JsValue> {
let frame_start = now_sec();
resize_canvas_to_screen_size(self.canvas_id(), self.app.max_size_points());
@@ -195,7 +199,9 @@ impl AppRunner {
});
let clipped_meshes = self.egui_ctx.tessellate(shapes);
self.handle_egui_output(&egui_output);
let needs_repaint = egui_output.needs_repaint;
let textures_delta = self.handle_egui_output(egui_output);
self.textures_delta.append(textures_delta);
{
let app_output = self.frame.take_app_output();
@@ -205,32 +211,32 @@ impl AppRunner {
window_title: _, // TODO: change title of window
decorated: _, // Can't toggle decorations
drag_window: _, // Can't be dragged
tex_allocation_data,
} = app_output;
for (id, image) in tex_allocation_data.creations {
self.painter.set_texture(id, image);
}
self.pending_texture_destructions = tex_allocation_data.destructions;
}
self.frame.lock().info.cpu_usage = Some((now_sec() - frame_start) as f32);
Ok((egui_output, clipped_meshes))
Ok((needs_repaint, clipped_meshes))
}
/// Paint the results of the last call to [`Self::logic`].
pub fn paint(&mut self, clipped_meshes: Vec<egui::ClippedMesh>) -> Result<(), JsValue> {
self.painter
.upload_egui_texture(&self.egui_ctx.font_image());
let textures_delta = std::mem::take(&mut self.textures_delta);
for (id, image) in textures_delta.set {
self.painter.set_texture(id, image);
}
self.painter.clear(self.app.clear_color());
self.painter
.paint_meshes(clipped_meshes, self.egui_ctx.pixels_per_point())?;
for id in self.pending_texture_destructions.drain(..) {
for id in textures_delta.free {
self.painter.free_texture(id);
}
Ok(())
}
fn handle_egui_output(&mut self, output: &egui::Output) {
fn handle_egui_output(&mut self, output: egui::Output) -> egui::TexturesDelta {
if self.egui_ctx.memory().options.screen_reader {
self.screen_reader.speak(&output.events_description());
}
@@ -243,27 +249,30 @@ impl AppRunner {
events: _, // already handled
mutable_text_under_cursor,
text_cursor_pos,
textures_delta,
} = output;
set_cursor_icon(*cursor_icon);
set_cursor_icon(cursor_icon);
if let Some(open) = open_url {
crate::open_url(&open.url, open.new_tab);
}
#[cfg(web_sys_unstable_apis)]
if !copied_text.is_empty() {
set_clipboard_text(copied_text);
set_clipboard_text(&copied_text);
}
#[cfg(not(web_sys_unstable_apis))]
let _ = copied_text;
self.mutable_text_under_cursor = *mutable_text_under_cursor;
self.mutable_text_under_cursor = mutable_text_under_cursor;
if &self.text_cursor_pos != text_cursor_pos {
if self.text_cursor_pos != text_cursor_pos {
move_text_cursor(text_cursor_pos, self.canvas_id());
self.text_cursor_pos = *text_cursor_pos;
self.text_cursor_pos = text_cursor_pos;
}
textures_delta
}
}

View File

@@ -1,5 +1,5 @@
use crate::{canvas_element_or_die, console_error};
use egui::{ClippedMesh, FontImage, Rgba};
use egui::{ClippedMesh, Rgba};
use egui_glow::glow;
use wasm_bindgen::JsCast;
use wasm_bindgen::JsValue;
@@ -40,12 +40,12 @@ impl WrappedGlowPainter {
}
impl crate::Painter for WrappedGlowPainter {
fn set_texture(&mut self, tex_id: u64, image: epi::Image) {
fn set_texture(&mut self, tex_id: egui::TextureId, image: egui::ImageData) {
self.painter.set_texture(&self.glow_ctx, tex_id, &image);
}
fn free_texture(&mut self, tex_id: u64) {
self.painter.free_texture(tex_id);
fn free_texture(&mut self, tex_id: egui::TextureId) {
self.painter.free_texture(&self.glow_ctx, tex_id);
}
fn debug_info(&self) -> String {
@@ -60,10 +60,6 @@ impl crate::Painter for WrappedGlowPainter {
&self.canvas_id
}
fn upload_egui_texture(&mut self, font_image: &FontImage) {
self.painter.upload_egui_texture(&self.glow_ctx, font_image)
}
fn clear(&mut self, clear_color: Rgba) {
let canvas_dimension = [self.canvas.width(), self.canvas.height()];
egui_glow::painter::clear(&self.glow_ctx, canvas_dimension, clear_color)

View File

@@ -482,9 +482,9 @@ fn paint_and_schedule(runner_ref: AppRunnerRef) -> Result<(), JsValue> {
fn paint_if_needed(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
let mut runner_lock = runner_ref.0.lock();
if runner_lock.needs_repaint.fetch_and_clear() {
let (output, clipped_meshes) = runner_lock.logic()?;
let (needs_repaint, clipped_meshes) = runner_lock.logic()?;
runner_lock.paint(clipped_meshes)?;
if output.needs_repaint {
if needs_repaint {
runner_lock.needs_repaint.set_true();
}
runner_lock.auto_save();
@@ -1214,7 +1214,7 @@ fn is_mobile() -> Option<bool> {
// candidate window moves following text element (agent),
// so it appears that the IME candidate window moves with text cursor.
// On mobile devices, there is no need to do that.
fn move_text_cursor(cursor: &Option<egui::Pos2>, canvas_id: &str) -> Option<()> {
fn move_text_cursor(cursor: Option<egui::Pos2>, canvas_id: &str) -> Option<()> {
let style = text_agent().style();
// Note: movint agent on mobile devices will lead to unpredictable scroll.
if is_mobile() == Some(false) {

View File

@@ -1,17 +1,15 @@
use wasm_bindgen::prelude::JsValue;
pub trait Painter {
fn set_texture(&mut self, tex_id: u64, image: epi::Image);
fn set_texture(&mut self, tex_id: egui::TextureId, image: egui::ImageData);
fn free_texture(&mut self, tex_id: u64);
fn free_texture(&mut self, tex_id: egui::TextureId);
fn debug_info(&self) -> String;
/// id of the canvas html element containing the rendering
fn canvas_id(&self) -> &str;
fn upload_egui_texture(&mut self, font_image: &egui::FontImage);
fn clear(&mut self, clear_color: egui::Rgba);
fn paint_meshes(

View File

@@ -9,10 +9,7 @@ use {
},
};
use egui::{
emath::vec2,
epaint::{Color32, FontImage},
};
use egui::{emath::vec2, epaint::Color32};
type Gl = WebGlRenderingContext;
@@ -28,13 +25,8 @@ pub struct WebGlPainter {
texture_format: u32,
post_process: Option<PostProcess>,
egui_texture: WebGlTexture,
egui_texture_version: Option<u64>,
/// Index is the same as in [`egui::TextureId::User`].
user_textures: HashMap<u64, WebGlTexture>,
next_native_tex_id: u64, // TODO: 128-bit texture space?
textures: HashMap<egui::TextureId, WebGlTexture>,
next_native_tex_id: u64,
}
impl WebGlPainter {
@@ -101,18 +93,13 @@ impl WebGlPainter {
color_buffer,
texture_format,
post_process,
egui_texture,
egui_texture_version: None,
user_textures: Default::default(),
textures: Default::default(),
next_native_tex_id: 1 << 32,
})
}
fn get_texture(&self, texture_id: egui::TextureId) -> Option<&WebGlTexture> {
match texture_id {
egui::TextureId::Egui => Some(&self.egui_texture),
egui::TextureId::User(id) => self.user_textures.get(&id),
}
self.textures.get(&texture_id)
}
fn paint_mesh(&self, mesh: &egui::epaint::Mesh16) -> Result<(), JsValue> {
@@ -234,44 +221,8 @@ impl WebGlPainter {
Ok(())
}
}
impl epi::NativeTexture for WebGlPainter {
type Texture = WebGlTexture;
fn register_native_texture(&mut self, native: Self::Texture) -> egui::TextureId {
let id = self.next_native_tex_id;
self.next_native_tex_id += 1;
self.user_textures.insert(id, native);
egui::TextureId::User(id as u64)
}
fn replace_native_texture(&mut self, id: egui::TextureId, replacing: Self::Texture) {
if let egui::TextureId::User(id) = id {
if let Some(user_texture) = self.user_textures.get_mut(&id) {
*user_texture = replacing;
}
}
}
}
impl crate::Painter for WebGlPainter {
fn set_texture(&mut self, tex_id: u64, image: epi::Image) {
assert_eq!(
image.size[0] * image.size[1],
image.pixels.len(),
"Mismatch between texture size and texel count"
);
// TODO: optimize
let mut pixels: Vec<u8> = Vec::with_capacity(image.pixels.len() * 4);
for srgba in image.pixels {
pixels.push(srgba.r());
pixels.push(srgba.g());
pixels.push(srgba.b());
pixels.push(srgba.a());
}
fn set_texture_rgba(&mut self, tex_id: egui::TextureId, size: [usize; 2], pixels: &[u8]) {
let gl = &self.gl;
let gl_texture = gl.create_texture().unwrap();
gl.bind_texture(Gl::TEXTURE_2D, Some(&gl_texture));
@@ -291,20 +242,64 @@ impl crate::Painter for WebGlPainter {
Gl::TEXTURE_2D,
level,
internal_format as _,
image.size[0] as _,
image.size[1] as _,
size[0] as _,
size[1] as _,
border,
src_format,
src_type,
Some(&pixels),
Some(pixels),
)
.unwrap();
self.user_textures.insert(tex_id, gl_texture);
self.textures.insert(tex_id, gl_texture);
}
}
impl epi::NativeTexture for WebGlPainter {
type Texture = WebGlTexture;
fn register_native_texture(&mut self, texture: Self::Texture) -> egui::TextureId {
let id = egui::TextureId::User(self.next_native_tex_id);
self.next_native_tex_id += 1;
self.textures.insert(id, texture);
id
}
fn free_texture(&mut self, tex_id: u64) {
self.user_textures.remove(&tex_id);
fn replace_native_texture(&mut self, id: egui::TextureId, texture: Self::Texture) {
self.textures.insert(id, texture);
}
}
impl crate::Painter for WebGlPainter {
fn set_texture(&mut self, tex_id: egui::TextureId, image: egui::ImageData) {
match image {
egui::ImageData::Color(image) => {
assert_eq!(
image.width() * image.height(),
image.pixels.len(),
"Mismatch between texture size and texel count"
);
let data: &[u8] = bytemuck::cast_slice(image.pixels.as_ref());
self.set_texture_rgba(tex_id, image.size, data);
}
egui::ImageData::Alpha(image) => {
let gamma = if self.post_process.is_none() {
1.0 / 2.2 // HACK due to non-linear framebuffer blending.
} else {
1.0 // post process enables linear blending
};
let data: Vec<u8> = image
.srgba_pixels(gamma)
.flat_map(|a| a.to_array())
.collect();
self.set_texture_rgba(tex_id, image.size, &data);
}
};
}
fn free_texture(&mut self, tex_id: egui::TextureId) {
self.textures.remove(&tex_id);
}
fn debug_info(&self) -> String {
@@ -323,48 +318,6 @@ impl crate::Painter for WebGlPainter {
&self.canvas_id
}
fn upload_egui_texture(&mut self, font_image: &FontImage) {
if self.egui_texture_version == Some(font_image.version) {
return; // No change
}
let gamma = if self.post_process.is_none() {
1.0 / 2.2 // HACK due to non-linear framebuffer blending.
} else {
1.0 // post process enables linear blending
};
let mut pixels: Vec<u8> = Vec::with_capacity(font_image.pixels.len() * 4);
for srgba in font_image.srgba_pixels(gamma) {
pixels.push(srgba.r());
pixels.push(srgba.g());
pixels.push(srgba.b());
pixels.push(srgba.a());
}
let gl = &self.gl;
gl.bind_texture(Gl::TEXTURE_2D, Some(&self.egui_texture));
let level = 0;
let internal_format = self.texture_format;
let border = 0;
let src_format = self.texture_format;
let src_type = Gl::UNSIGNED_BYTE;
gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array(
Gl::TEXTURE_2D,
level,
internal_format as i32,
font_image.width as i32,
font_image.height as i32,
border,
src_format,
src_type,
Some(&pixels),
)
.unwrap();
self.egui_texture_version = Some(font_image.version);
}
fn clear(&mut self, clear_color: egui::Rgba) {
let gl = &self.gl;

View File

@@ -10,10 +10,7 @@ use {
},
};
use egui::{
emath::vec2,
epaint::{Color32, FontImage},
};
use egui::{emath::vec2, epaint::Color32};
type Gl = WebGl2RenderingContext;
@@ -28,13 +25,8 @@ pub struct WebGl2Painter {
color_buffer: WebGlBuffer,
post_process: PostProcess,
egui_texture: WebGlTexture,
egui_texture_version: Option<u64>,
/// Index is the same as in [`egui::TextureId::User`].
user_textures: HashMap<u64, WebGlTexture>,
next_native_tex_id: u64, // TODO: 128-bit texture space?
textures: HashMap<egui::TextureId, WebGlTexture>,
next_native_tex_id: u64,
}
impl WebGl2Painter {
@@ -85,18 +77,13 @@ impl WebGl2Painter {
tc_buffer,
color_buffer,
post_process,
egui_texture,
egui_texture_version: None,
user_textures: Default::default(),
textures: Default::default(),
next_native_tex_id: 1 << 32,
})
}
fn get_texture(&self, texture_id: egui::TextureId) -> Option<&WebGlTexture> {
match texture_id {
egui::TextureId::Egui => Some(&self.egui_texture),
egui::TextureId::User(id) => self.user_textures.get(&id),
}
self.textures.get(&texture_id)
}
fn paint_mesh(&self, mesh: &egui::epaint::Mesh16) -> Result<(), JsValue> {
@@ -218,44 +205,8 @@ impl WebGl2Painter {
Ok(())
}
}
impl epi::NativeTexture for WebGl2Painter {
type Texture = WebGlTexture;
fn register_native_texture(&mut self, native: Self::Texture) -> egui::TextureId {
let id = self.next_native_tex_id;
self.next_native_tex_id += 1;
self.user_textures.insert(id, native);
egui::TextureId::User(id as u64)
}
fn replace_native_texture(&mut self, id: egui::TextureId, replacing: Self::Texture) {
if let egui::TextureId::User(id) = id {
if let Some(user_texture) = self.user_textures.get_mut(&id) {
*user_texture = replacing;
}
}
}
}
impl crate::Painter for WebGl2Painter {
fn set_texture(&mut self, tex_id: u64, image: epi::Image) {
assert_eq!(
image.size[0] * image.size[1],
image.pixels.len(),
"Mismatch between texture size and texel count"
);
// TODO: optimize
let mut pixels: Vec<u8> = Vec::with_capacity(image.pixels.len() * 4);
for srgba in image.pixels {
pixels.push(srgba.r());
pixels.push(srgba.g());
pixels.push(srgba.b());
pixels.push(srgba.a());
}
fn set_texture_rgba(&mut self, tex_id: egui::TextureId, size: [usize; 2], pixels: &[u8]) {
let gl = &self.gl;
let gl_texture = gl.create_texture().unwrap();
gl.bind_texture(Gl::TEXTURE_2D, Some(&gl_texture));
@@ -271,24 +222,65 @@ impl crate::Painter for WebGl2Painter {
let border = 0;
let src_format = Gl::RGBA;
let src_type = Gl::UNSIGNED_BYTE;
gl.pixel_storei(Gl::UNPACK_ALIGNMENT, 1);
gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array(
Gl::TEXTURE_2D,
level,
internal_format as _,
image.size[0] as _,
image.size[1] as _,
internal_format as i32,
size[0] as i32,
size[1] as i32,
border,
src_format,
src_type,
Some(&pixels),
Some(pixels),
)
.unwrap();
self.user_textures.insert(tex_id, gl_texture);
self.textures.insert(tex_id, gl_texture);
}
}
impl epi::NativeTexture for WebGl2Painter {
type Texture = WebGlTexture;
fn register_native_texture(&mut self, native: Self::Texture) -> egui::TextureId {
let id = egui::TextureId::User(self.next_native_tex_id);
self.next_native_tex_id += 1;
self.textures.insert(id, native);
id
}
fn free_texture(&mut self, tex_id: u64) {
self.user_textures.remove(&tex_id);
fn replace_native_texture(&mut self, id: egui::TextureId, native: Self::Texture) {
self.textures.insert(id, native);
}
}
impl crate::Painter for WebGl2Painter {
fn set_texture(&mut self, tex_id: egui::TextureId, image: egui::ImageData) {
match image {
egui::ImageData::Color(image) => {
assert_eq!(
image.width() * image.height(),
image.pixels.len(),
"Mismatch between texture size and texel count"
);
let data: &[u8] = bytemuck::cast_slice(image.pixels.as_ref());
self.set_texture_rgba(tex_id, image.size, data);
}
egui::ImageData::Alpha(image) => {
let gamma = 1.0;
let data: Vec<u8> = image
.srgba_pixels(gamma)
.flat_map(|a| a.to_array())
.collect();
self.set_texture_rgba(tex_id, image.size, &data);
}
};
}
fn free_texture(&mut self, tex_id: egui::TextureId) {
self.textures.remove(&tex_id);
}
fn debug_info(&self) -> String {
@@ -307,44 +299,6 @@ impl crate::Painter for WebGl2Painter {
&self.canvas_id
}
fn upload_egui_texture(&mut self, font_image: &FontImage) {
if self.egui_texture_version == Some(font_image.version) {
return; // No change
}
let mut pixels: Vec<u8> = Vec::with_capacity(font_image.pixels.len() * 4);
for srgba in font_image.srgba_pixels(1.0) {
pixels.push(srgba.r());
pixels.push(srgba.g());
pixels.push(srgba.b());
pixels.push(srgba.a());
}
let gl = &self.gl;
gl.bind_texture(Gl::TEXTURE_2D, Some(&self.egui_texture));
let level = 0;
let internal_format = Gl::SRGB8_ALPHA8;
let border = 0;
let src_format = Gl::RGBA;
let src_type = Gl::UNSIGNED_BYTE;
gl.pixel_storei(Gl::UNPACK_ALIGNMENT, 1);
gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array(
Gl::TEXTURE_2D,
level,
internal_format as i32,
font_image.width as i32,
font_image.height as i32,
border,
src_format,
src_type,
Some(&pixels),
)
.unwrap();
self.egui_texture_version = Some(font_image.version);
}
fn clear(&mut self, clear_color: egui::Rgba) {
let gl = &self.gl;