Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Managed texture loading #3297

Merged
merged 64 commits into from
Sep 6, 2023
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
f8cd5f4
add types from proposal
jprochazk Sep 4, 2023
aad9978
add load methods on `egui::Context`
jprochazk Sep 4, 2023
a94b17f
implement loaders from proposal in `egui_extras`
jprochazk Sep 4, 2023
ec79d88
impl `From<Vec2>` for `SizeHint`
jprochazk Sep 4, 2023
1934fba
re-export `SizeHint` from `egui` root
jprochazk Sep 4, 2023
5b113d5
rework `svg` example to use new managed `Image`
jprochazk Sep 4, 2023
c1443b9
split loaders into separate files + add logging
jprochazk Sep 4, 2023
f6efd05
add `log_trace`
jprochazk Sep 4, 2023
652432a
clean up `RetainedImage` from `svg` example
jprochazk Sep 4, 2023
015dc84
refactor ehttp loader response to bytes mapping
jprochazk Sep 4, 2023
80dd53c
remove spammy trace
jprochazk Sep 4, 2023
eceeda8
load images even without extension
jprochazk Sep 4, 2023
022d5a0
fix lints
jprochazk Sep 4, 2023
6819ea3
remove unused imports
jprochazk Sep 4, 2023
d954175
use `Image2` in `download_image`
jprochazk Sep 4, 2023
9ae51a2
use `visuals.error_fg_color` in `Image2` error state
jprochazk Sep 4, 2023
aea252d
update lockfile
jprochazk Sep 4, 2023
c2ecc34
use `Arc<ColorImage>` in `ImageData` + add `forget` API
jprochazk Sep 4, 2023
52f3529
add `ui.image2`
jprochazk Sep 4, 2023
08951f1
add byte size query api
jprochazk Sep 4, 2023
41b074c
use iterators to sum loader byte sizes
jprochazk Sep 4, 2023
6b0a653
add static image loading
jprochazk Sep 4, 2023
3fc03c3
use static image in `svg` example
jprochazk Sep 4, 2023
78f2d93
small refactor of `Image2::ui` texture loading code
jprochazk Sep 4, 2023
4080b8f
add `ImageFit` to size images properly
jprochazk Sep 4, 2023
2584fa8
remove println calls
jprochazk Sep 4, 2023
3182c9d
add bad image load to `download_image` example
jprochazk Sep 4, 2023
ae0c6c1
add loader file extension support tests
jprochazk Sep 4, 2023
4cfec50
fix lint errors in `loaders`
jprochazk Sep 4, 2023
a10a88c
remove unused `poll-promise` dependency
jprochazk Sep 4, 2023
a8ae600
add some docs to `Image2`
jprochazk Sep 4, 2023
08cb30b
add some docs to `egui_extras::loaders::install`
jprochazk Sep 4, 2023
2c8be3c
explain `loaders::install` in examples
jprochazk Sep 4, 2023
808e249
fix lint
jprochazk Sep 5, 2023
1d9a7ed
upgrade `ehttp` to `0.3` for some crates
jprochazk Sep 5, 2023
d109db4
Remove some unused dependencies
emilk Sep 5, 2023
9544b02
Remove unnecessary context clone
emilk Sep 5, 2023
3f194cc
Turn on the `log` create feature of egui_extras in all examples
emilk Sep 5, 2023
2962067
Merge branch 'master' into improved-texture-loading
emilk Sep 5, 2023
0dbaef0
rename `forget` and document it
jprochazk Sep 5, 2023
f6104b9
derive `Debug` on `SizeHint`
jprochazk Sep 5, 2023
1bfd26e
round when converting SizeHint from vec2
jprochazk Sep 5, 2023
2a687b7
add `load` module docs
jprochazk Sep 5, 2023
817b956
docstring `add_loader` methods
jprochazk Sep 5, 2023
b733a58
expose + document `load_include_bytes`
jprochazk Sep 5, 2023
1aacf42
cache texture handles in `DefaultTextureLoader`
jprochazk Sep 5, 2023
356d7ef
add `image2` doctest + further document `Image2`
jprochazk Sep 5, 2023
0d28132
use `Default` for default `Image2` options
jprochazk Sep 5, 2023
afa3ef3
update `image2` doc comment
jprochazk Sep 5, 2023
a3c890d
mention immediate-mode safety
jprochazk Sep 5, 2023
f975d83
more fit calculation into inherent impl
jprochazk Sep 5, 2023
98ddf02
add hover text on spinner
jprochazk Sep 5, 2023
2736cb5
add `all-loaders` feature
jprochazk Sep 5, 2023
ef71ea2
clarify `egui_extras::loaders::install` behavior
jprochazk Sep 5, 2023
76b5e74
explain how to enable image formats
jprochazk Sep 5, 2023
30f0e0f
properly format `uri`
jprochazk Sep 5, 2023
c84e9ab
use `thread::Builder` instead of `spawn`
jprochazk Sep 5, 2023
d21e74e
use eq op instead of `matches`
jprochazk Sep 5, 2023
59cd614
inline `From<Arc<ColorImage>>` for `ImageData`
jprochazk Sep 5, 2023
4206355
allow non-`'static` bytes + `forget` in `DefaultTextureLoader`
jprochazk Sep 6, 2023
f0e9733
sort features
jprochazk Sep 6, 2023
049122f
change `ehttp` feature to `http`
jprochazk Sep 6, 2023
c8a8074
update `Image2` docs
jprochazk Sep 6, 2023
ef7e946
refactor loader cache type
jprochazk Sep 6, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions crates/egui/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1905,24 +1905,38 @@ impl Context {

/// ## Image loading
impl Context {
pub(crate) fn load_include_bytes(&self, name: &'static str, bytes: &'static [u8]) {
self.read(|ctx| ctx.loaders.include.insert(name, bytes));
/// Associate some static bytes with a `uri`.
///
/// The same `uri` may be passed to [`Ui::image2`] later to load the bytes as an image.
pub fn load_include_bytes(&self, uri: &'static str, bytes: &'static [u8]) {
self.read(|ctx| ctx.loaders.include.insert(uri, bytes));
}

/// Append an entry onto the chain of bytes loaders.
///
/// See [`load`] for more information.
pub fn add_bytes_loader(&self, loader: Arc<dyn load::BytesLoader + Send + Sync + 'static>) {
self.write(|ctx| ctx.loaders.bytes.push(loader));
}

/// Append an entry onto the chain of image loaders.
///
/// See [`load`] for more information.
pub fn add_image_loader(&self, loader: Arc<dyn load::ImageLoader + Send + Sync + 'static>) {
self.write(|ctx| ctx.loaders.image.push(loader));
}

/// Append an entry onto the chain of texture loaders.
///
/// See [`load`] for more information.
pub fn add_texture_loader(&self, loader: Arc<dyn load::TextureLoader + Send + Sync + 'static>) {
self.write(|ctx| ctx.loaders.texture.push(loader));
}

// TODO: evict caches
pub fn forget(&self, uri: &str) {
/// Release all memory and textures related to the given image URI.
///
/// If you attempt to load the image again, it will be reloaded from scratch.
pub fn forget_image(&self, uri: &str) {
self.write(|ctx| {
use crate::load::BytesLoader as _;

Expand All @@ -1944,6 +1958,8 @@ impl Context {

/// Try loading the bytes from the given uri using any available bytes loaders.
///
/// Loaders are expected to cache results, so that this call is immediate-mode safe.
///
/// This calls the loaders one by one in the order in which they were registered.
/// If a loader returns [`LoadError::NotSupported`][not_supported],
/// then the next loader is called. This process repeats until all loaders have
Expand Down Expand Up @@ -1971,6 +1987,8 @@ impl Context {

/// Try loading the image from the given uri using any available image loaders.
///
/// Loaders are expected to cache results, so that this call is immediate-mode safe.
///
/// This calls the loaders one by one in the order in which they were registered.
/// If a loader returns [`LoadError::NotSupported`][not_supported],
/// then the next loader is called. This process repeats until all loaders have
Expand Down Expand Up @@ -1998,6 +2016,8 @@ impl Context {

/// Try loading the texture from the given uri using any available texture loaders.
///
/// Loaders are expected to cache results, so that this call is immediate-mode safe.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

We should consider how we can alert the user of methods that aren't immediate safe, most notably the old Context::load_texture. Adding a ⚠️ Not immediate-mode safe warning to them might be a start, but perhaps we could also add it to their name somehow. load_texture_sync or something.

///
/// This calls the loaders one by one in the order in which they were registered.
/// If a loader returns [`LoadError::NotSupported`][not_supported],
/// then the next loader is called. This process repeats until all loaders have
Expand Down
110 changes: 92 additions & 18 deletions crates/egui/src/load.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,57 @@
//! Types and traits related to image loading.
//!
//! If you just want to load some images, see [`egui_extras`](https://crates.io/crates/egui_extras/),
//! which contains reasonable default implementations of these traits. You can get started quickly
//! using [`egui_extras::loaders::install`](https://docs.rs/egui_extras/latest/egui_extras/loaders/fn.install.html).
//!
//! ## Loading process
//!
//! There are three kinds of loaders:
//! - [`BytesLoader`]: load the raw bytes of an image
//! - [`ImageLoader`]: decode the bytes into an array of colors
//! - [`TextureLoader`]: ask the backend to put an image onto the GPU
//!
//! The different kinds of loaders represent different layers in the loading process:
//!
//! ```text,ignore
//! ui.image2("file://image.png")
//! └► ctx.try_load_texture("file://image.png", ...)
//! └► TextureLoader::load("file://image.png", ...)
//! └► ctx.try_load_image("file://image.png", ...)
//! └► ImageLoader::load("file://image.png", ...)
//! └► ctx.try_load_bytes("file://image.png", ...)
//! └► BytesLoader::load("file://image.png", ...)
//! ```
//!
//! As each layer attempts to load the URI, it first asks the layer below it
//! for the data it needs to do its job. But this is not a strict requirement,
//! an implementation could instead generate the data it needs!
//!
//! Loader trait implementations may be registered on a context with:
//! - [`Context::add_bytes_loader`]
//! - [`Context::add_image_loader`]
//! - [`Context::add_texture_loader`]
//!
//! There may be multiple loaders of the same kind registered at the same time.
//! The `try_load` methods on [`Context`] will attempt to call each loader one by one,
//! until one of them returns something other than [`LoadError::NotSupported`].
//!
//! The loaders are stored in the context. This means they may hold state across frames,
//! which they can (and _should_) use to cache the results of the operations they perform.
//!
//! For example, a [`BytesLoader`] that loads file URIs (`file://image.png`)
//! would cache each file read. A [`TextureLoader`] would cache each combination
//! of `(URI, TextureOptions)`, and so on.
//!
//! Each URI will be passed through the loaders as a plain `&str`.
//! The loaders are free to derive as much meaning from the URI as they wish to.
//! For example, a loader may determine that it doesn't support loading a specific URI
//! if the protocol does not match what it expects.

use crate::Context;
use ahash::HashMap;
use epaint::mutex::Mutex;
use epaint::TextureHandle;
use epaint::{textures::TextureOptions, ColorImage, TextureId, Vec2};
use std::{error::Error as StdError, fmt::Display, sync::Arc};

Expand Down Expand Up @@ -33,9 +84,10 @@ pub type Result<T, E = LoadError> = std::result::Result<T, E>;
/// All variants will preserve the original aspect ratio.
///
/// Similar to `usvg::FitTo`.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum SizeHint {
/// Keep original size.
#[default]
Original,

/// Scale to width.
Expand All @@ -50,7 +102,7 @@ pub enum SizeHint {

impl From<Vec2> for SizeHint {
fn from(value: Vec2) -> Self {
Self::Size(value.x as u32, value.y as u32)
Self::Size(value.x.round() as u32, value.y.round() as u32)
}
}

Expand Down Expand Up @@ -84,6 +136,9 @@ pub trait BytesLoader {
/// Implementations should call `ctx.request_repaint` to wake up the ui
/// once the data is ready.
///
/// The implementation should cache any result, so that calling this
/// is immediate-mode safe.
///
/// # Errors
/// This may fail with:
/// - [`LoadError::NotSupported`] if the loader does not support loading `uri`.
Expand Down Expand Up @@ -126,6 +181,9 @@ pub trait ImageLoader {
/// Implementations should call `ctx.request_repaint` to wake up the ui
/// once the image is ready.
///
/// The implementation should cache any result, so that calling this
/// is immediate-mode safe.
///
/// # Errors
/// This may fail with:
/// - [`LoadError::NotSupported`] if the loader does not support loading `uri`.
Expand Down Expand Up @@ -155,6 +213,15 @@ pub struct SizedTexture {
pub size: Size,
}

impl SizedTexture {
pub fn from_handle(handle: &TextureHandle) -> Self {
Self {
id: handle.id(),
size: handle.size(),
}
}
}

#[derive(Clone)]
pub enum TexturePoll {
/// Texture is loading.
Expand All @@ -175,6 +242,9 @@ pub trait TextureLoader {
/// Implementations should call `ctx.request_repaint` to wake up the ui
/// once the texture is ready.
///
/// The implementation should cache any result, so that calling this
/// is immediate-mode safe.
///
/// # Errors
/// This may fail with:
/// - [`LoadError::NotSupported`] if the loader does not support loading `uri`.
Expand Down Expand Up @@ -209,11 +279,8 @@ pub(crate) struct IncludeBytesLoader {
}

impl IncludeBytesLoader {
pub(crate) fn insert(&self, name: &'static str, bytes: &'static [u8]) {
self.cache
.lock()
.entry(name)
.or_insert_with(|| bytes.into());
pub(crate) fn insert(&self, uri: &'static str, bytes: &'static [u8]) {
self.cache.lock().entry(uri).or_insert_with(|| bytes.into());
}
}

Expand All @@ -234,7 +301,10 @@ impl BytesLoader for IncludeBytesLoader {
}
}

struct DefaultTextureLoader;
#[derive(Default)]
struct DefaultTextureLoader {
cache: Mutex<HashMap<(String, TextureOptions), TextureHandle>>,
jprochazk marked this conversation as resolved.
Show resolved Hide resolved
}

impl TextureLoader for DefaultTextureLoader {
fn load(
Expand All @@ -244,15 +314,19 @@ impl TextureLoader for DefaultTextureLoader {
texture_options: TextureOptions,
size_hint: SizeHint,
) -> TextureLoadResult {
match ctx.try_load_image(uri, size_hint)? {
ImagePoll::Pending { size } => Ok(TexturePoll::Pending { size }),
ImagePoll::Ready { image } => {
let handle = ctx.load_texture(uri, image, texture_options);
let texture = SizedTexture {
id: handle.id(),
size: handle.size(),
};
Ok(TexturePoll::Ready { texture })
let mut cache = self.cache.lock();
if let Some(handle) = cache.get(&(uri.into(), texture_options)) {
let texture = SizedTexture::from_handle(handle);
Ok(TexturePoll::Ready { texture })
} else {
match ctx.try_load_image(uri, size_hint)? {
ImagePoll::Pending { size } => Ok(TexturePoll::Pending { size }),
ImagePoll::Ready { image } => {
let handle = ctx.load_texture(uri, image, texture_options);
let texture = SizedTexture::from_handle(&handle);
cache.insert((uri.into(), texture_options), handle);
Ok(TexturePoll::Ready { texture })
}
}
}
}
Expand Down Expand Up @@ -284,7 +358,7 @@ impl Default for Loaders {
bytes: vec![include.clone()],
image: Vec::new(),
// By default we only include `DefaultTextureLoader`.
texture: vec![Arc::new(DefaultTextureLoader)],
texture: vec![Arc::new(DefaultTextureLoader::default())],
include,
}
}
Expand Down
14 changes: 14 additions & 0 deletions crates/egui/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1591,6 +1591,20 @@ impl Ui {
Image::new(texture_id, size).ui(self)
}

/// Show an image available at the given `uri`.
///
/// ⚠ This will do nothing unless you install some image loaders first!
/// The easiest way to do this is via [`egui_extras::loaders::install`](https://docs.rs/egui_extras/latest/egui_extras/loaders/fn.install.html).
///
/// The loaders handle caching image data, sampled textures, etc. across frames, so calling this is immediate-mode safe.
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// ui.image2("file://ferris.svg");
/// # });
/// ```
///
/// See also [`crate::Image2`] and [`crate::ImageSource`].
#[inline]
pub fn image2<'a>(&mut self, source: impl Into<ImageSource<'a>>) -> Response {
jprochazk marked this conversation as resolved.
Show resolved Hide resolved
Image2::new(source.into()).ui(self)
Expand Down
Loading
Loading