perf: pre scale generated images (#553)

Before this change only filesystem images were scaled when transitioning
between slides. This PR does that for generated images as well, and
polls other slides while doing transitions so any images generated
asynchronously are already ASCII'd before we transition into them.

This PR has shown that the async render code has reached proper 💩
level and requires changing it, more so since #537 will be implemented
soon-ish.
This commit is contained in:
Matias Fontanini 2025-04-17 15:38:58 -07:00 committed by GitHub
commit f59c19af36
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 121 additions and 109 deletions

View File

@ -97,13 +97,14 @@ impl ContentManager {
ImageSource::Filesystem(path) => Ok(path),
ImageSource::Generated => {
let mut buffer = Vec::new();
let dimensions = image.dimensions();
let TerminalImage::Ascii(resource) = image.image.as_ref() else { panic!("not in ascii mode") };
let dimensions = image.image().dimensions();
let TerminalImage::Ascii(image) = image.image() else { panic!("not in ascii mode") };
let image = image.image();
PngEncoder::new(&mut buffer).write_image(
resource.as_bytes(),
image.as_bytes(),
dimensions.0,
dimensions.1,
resource.color().into(),
image.color().into(),
)?;
let name = format!("img-{}.png", self.image_count);
let path = self.output_directory.path().join(name);

View File

@ -173,8 +173,8 @@ impl Presentation {
}
/// Poll every async render operation in the current slide and check whether they're completed.
pub(crate) fn poll_slide_async_renders(&mut self) -> RenderAsyncState {
let slide = self.current_slide_mut();
pub(crate) fn poll_slide_async_renders(&mut self, slide: usize) -> RenderAsyncState {
let slide = &mut self.slides[slide];
let mut slide_state = RenderAsyncState::Rendered;
for operation in slide.iter_operations_mut() {
if let RenderOperation::RenderAsync(operation) = operation {

View File

@ -156,12 +156,12 @@ impl<'a> Presenter<'a> {
self.try_scale_transition_images()?;
break;
}
CommandSideEffect::NextSlide => {
self.next_slide(&mut drawer)?;
CommandSideEffect::AnimateNextSlide => {
self.animate_next_slide(&mut drawer)?;
break;
}
CommandSideEffect::PreviousSlide => {
self.previous_slide(&mut drawer)?;
CommandSideEffect::AnimatePreviousSlide => {
self.animate_previous_slide(&mut drawer)?;
break;
}
CommandSideEffect::None => (),
@ -203,15 +203,19 @@ impl<'a> Presenter<'a> {
return Ok(false);
}
let current_index = self.state.presentation().current_slide_index();
if self.slides_with_pending_async_renders.contains(&current_index) {
let state = self.state.presentation_mut().poll_slide_async_renders();
self.poll_slide_async_renders(current_index)
}
fn poll_slide_async_renders(&mut self, slide: usize) -> Result<bool, RenderError> {
if self.slides_with_pending_async_renders.contains(&slide) {
let state = self.state.presentation_mut().poll_slide_async_renders(slide);
match state {
RenderAsyncState::NotStarted | RenderAsyncState::Rendering { modified: false } => (),
RenderAsyncState::Rendering { modified: true } => {
return Ok(true);
}
RenderAsyncState::Rendered | RenderAsyncState::JustFinishedRendering => {
self.slides_with_pending_async_renders.remove(&current_index);
self.slides_with_pending_async_renders.remove(&slide);
return Ok(true);
}
};
@ -279,7 +283,7 @@ impl<'a> Presenter<'a> {
if !presentation.jump_next() {
false
} else if presentation.current_slide_index() != current_slide {
return CommandSideEffect::NextSlide;
return CommandSideEffect::AnimateNextSlide;
} else {
true
}
@ -290,7 +294,7 @@ impl<'a> Presenter<'a> {
if !presentation.jump_previous() {
false
} else if presentation.current_slide_index() != current_slide {
return CommandSideEffect::PreviousSlide;
return CommandSideEffect::AnimatePreviousSlide;
} else {
true
}
@ -361,7 +365,7 @@ impl<'a> Presenter<'a> {
return Ok(());
}
let options = RenderEngineOptions { max_size: self.options.max_size.clone(), ..Default::default() };
let scaler = AsciiScaler::new(options, self.resources.image_registry());
let scaler = AsciiScaler::new(options);
let dimensions = WindowSize::current(self.options.font_size_fallback)?;
scaler.process(self.state.presentation(), &dimensions)?;
Ok(())
@ -436,34 +440,40 @@ impl<'a> Presenter<'a> {
}
}
fn next_slide(&mut self, drawer: &mut TerminalDrawer) -> RenderResult {
fn animate_next_slide(&mut self, drawer: &mut TerminalDrawer) -> RenderResult {
let Some(config) = self.options.transition.clone() else {
return Ok(());
};
let registry = self.resources.image_registry();
self.poll_and_scale_images()?;
let options = drawer.render_engine_options();
let presentation = self.state.presentation_mut();
let dimensions = WindowSize::current(self.options.font_size_fallback)?;
presentation.jump_previous();
let left = Self::virtual_render(presentation.current_slide(), dimensions.clone(), &options, registry.clone())?;
let left = Self::virtual_render(presentation.current_slide(), dimensions.clone(), &options)?;
presentation.jump_next();
let right = Self::virtual_render(presentation.current_slide(), dimensions.clone(), &options, registry)?;
let right = Self::virtual_render(presentation.current_slide(), dimensions.clone(), &options)?;
let direction = TransitionDirection::Next;
self.animate_transition(drawer, left, right, direction, dimensions, config)
}
fn previous_slide(&mut self, drawer: &mut TerminalDrawer) -> RenderResult {
fn animate_previous_slide(&mut self, drawer: &mut TerminalDrawer) -> RenderResult {
let Some(config) = self.options.transition.clone() else {
return Ok(());
};
let registry = self.resources.image_registry();
self.poll_and_scale_images()?;
let options = drawer.render_engine_options();
let presentation = self.state.presentation_mut();
let dimensions = WindowSize::current(self.options.font_size_fallback)?;
presentation.jump_next();
let right = Self::virtual_render(presentation.current_slide(), dimensions.clone(), &options, registry.clone())?;
// Re-borrow to avoid calling fns above while mutably borrowing
let presentation = self.state.presentation_mut();
let right = Self::virtual_render(presentation.current_slide(), dimensions.clone(), &options)?;
presentation.jump_previous();
let left = Self::virtual_render(presentation.current_slide(), dimensions.clone(), &options, registry)?;
let left = Self::virtual_render(presentation.current_slide(), dimensions.clone(), &options)?;
let direction = TransitionDirection::Previous;
self.animate_transition(drawer, left, right, direction, dimensions, config)
}
@ -543,13 +553,23 @@ impl<'a> Presenter<'a> {
slide: &Slide,
dimensions: WindowSize,
options: &RenderEngineOptions,
registry: ImageRegistry,
) -> Result<TerminalGrid, RenderError> {
let mut term = VirtualTerminal::new(dimensions.clone(), ImageBehavior::PrintAscii(registry));
let mut term = VirtualTerminal::new(dimensions.clone(), ImageBehavior::PrintAscii);
let engine = RenderEngine::new(&mut term, dimensions.clone(), options.clone());
engine.render(slide.iter_visible_operations())?;
Ok(term.into_contents())
}
fn poll_and_scale_images(&mut self) -> RenderResult {
let mut needs_scaling = false;
for index in 0..self.state.presentation().iter_slides().count() {
needs_scaling = self.poll_slide_async_renders(index)? || needs_scaling;
}
if needs_scaling {
self.try_scale_transition_images()?;
}
Ok(())
}
}
enum CommandSideEffect {
@ -557,8 +577,8 @@ enum CommandSideEffect {
Suspend,
Redraw,
Reload,
NextSlide,
PreviousSlide,
AnimateNextSlide,
AnimatePreviousSlide,
None,
}

View File

@ -3,10 +3,10 @@ use super::{
engine::{RenderEngine, RenderEngineOptions},
};
use crate::{
ImageRegistry, WindowSize,
WindowSize,
presentation::Presentation,
terminal::{
image::{Image, ImageSource},
image::Image,
printer::{TerminalCommand, TerminalError, TerminalIo},
},
};
@ -15,12 +15,11 @@ use unicode_width::UnicodeWidthStr;
pub(crate) struct AsciiScaler {
options: RenderEngineOptions,
registry: ImageRegistry,
}
impl AsciiScaler {
pub(crate) fn new(options: RenderEngineOptions, registry: ImageRegistry) -> Self {
Self { options, registry }
pub(crate) fn new(options: RenderEngineOptions) -> Self {
Self { options }
}
pub(crate) fn process(self, presentation: &Presentation, dimensions: &WindowSize) -> Result<(), RenderError> {
@ -29,13 +28,13 @@ impl AsciiScaler {
let engine = RenderEngine::new(&mut collector, dimensions.clone(), self.options.clone());
engine.render(slide.iter_operations())?;
}
thread::spawn(move || Self::scale(collector.images, self.registry));
thread::spawn(move || Self::scale(collector.images));
Ok(())
}
fn scale(images: Vec<ScalableImage>, registry: ImageRegistry) {
fn scale(images: Vec<ScalableImage>) {
for image in images {
let ascii_image = registry.as_ascii(&image.image);
let ascii_image = image.image.to_ascii();
ascii_image.cache_scaling(image.columns, image.rows);
}
}
@ -84,11 +83,8 @@ impl TerminalIo for ImageCollector {
}
PrintImage { image, options } => {
// we can only really cache filesystem images for now
if matches!(image.source, ImageSource::Filesystem(_)) {
let image =
ScalableImage { image: image.clone(), rows: options.rows * 2, columns: options.columns };
self.images.push(image);
}
let image = ScalableImage { image: image.clone(), rows: options.rows * 2, columns: options.columns };
self.images.push(image);
}
ClearScreen => {
self.current_column = 0;

View File

@ -258,7 +258,7 @@ where
let starting_cursor =
CursorPosition { row: starting_row.saturating_sub(rect.start_row), column: rect.start_column };
let (width, height) = image.dimensions();
let (width, height) = image.image().dimensions();
let (columns, rows) = match properties.size {
ImageSize::ShrinkIfNeeded => {
let image_scale =

View File

@ -133,10 +133,6 @@ impl Resources {
inner.image_registry.clear();
inner.themes.clear();
}
pub(crate) fn image_registry(&self) -> ImageRegistry {
self.inner.borrow().image_registry.clone()
}
}
/// Watches for file changes.

View File

@ -1,23 +1,59 @@
use image::DynamicImage;
use protocols::ascii::AsciiImage;
use self::printer::{ImageProperties, TerminalImage};
use std::{fmt::Debug, ops::Deref, path::PathBuf, sync::Arc};
use std::{
fmt::Debug,
ops::Deref,
path::PathBuf,
sync::{Arc, Mutex},
};
pub(crate) mod printer;
pub(crate) mod protocols;
pub(crate) mod scale;
struct Inner {
image: TerminalImage,
ascii_image: Mutex<Option<AsciiImage>>,
}
/// An image.
///
/// This stores the image in an [std::sync::Arc] so it's cheap to clone.
#[derive(Clone)]
pub(crate) struct Image {
pub(crate) image: Arc<TerminalImage>,
inner: Arc<Inner>,
pub(crate) source: ImageSource,
}
impl Image {
/// Constructs a new image.
pub(crate) fn new(image: TerminalImage, source: ImageSource) -> Self {
Self { image: Arc::new(image), source }
let inner = Inner { image, ascii_image: Default::default() };
Self { inner: Arc::new(inner), source }
}
pub(crate) fn to_ascii(&self) -> AsciiImage {
let mut ascii_image = self.inner.ascii_image.lock().unwrap();
match ascii_image.deref() {
Some(image) => image.clone(),
None => {
let image = match &self.inner.image {
TerminalImage::Ascii(image) => image.clone(),
TerminalImage::Kitty(image) => DynamicImage::from(image.as_rgba8()).into(),
TerminalImage::Iterm(image) => DynamicImage::from(image.as_rgba8()).into(),
#[cfg(feature = "sixel")]
TerminalImage::Sixel(image) => DynamicImage::from(image.as_rgba8()).into(),
};
*ascii_image = Some(image.clone());
image
}
}
}
pub(crate) fn image(&self) -> &TerminalImage {
&self.inner.image
}
}
@ -29,19 +65,11 @@ impl PartialEq for Image {
impl Debug for Image {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let (width, height) = self.image.dimensions();
let (width, height) = self.inner.image.dimensions();
write!(f, "Image<{width}x{height}>")
}
}
impl Deref for Image {
type Target = TerminalImage;
fn deref(&self) -> &Self::Target {
&self.image
}
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum ImageSource {
Filesystem(PathBuf),

View File

@ -18,7 +18,6 @@ use std::{
borrow::Cow,
collections::HashMap,
fmt, io,
ops::Deref,
path::PathBuf,
sync::{Arc, Mutex},
};
@ -151,12 +150,11 @@ impl PrintImage for ImagePrinter {
pub(crate) struct ImageRegistry {
printer: Arc<ImagePrinter>,
images: Arc<Mutex<HashMap<PathBuf, Image>>>,
ascii_images: Arc<Mutex<HashMap<PathBuf, AsciiImage>>>,
}
impl ImageRegistry {
pub fn new(printer: Arc<ImagePrinter>) -> Self {
Self { printer, images: Default::default(), ascii_images: Default::default() }
Self { printer, images: Default::default() }
}
}
@ -191,39 +189,12 @@ impl ImageRegistry {
let image = Image::new(resource, source);
if let Some(key) = cache_key {
images.insert(key.clone(), image.clone());
drop(images);
if let TerminalImage::Ascii(image) = image.image.as_ref() {
self.ascii_images.lock().unwrap().insert(key, image.clone());
}
}
Ok(image)
}
pub(crate) fn clear(&self) {
self.images.lock().unwrap().clear();
self.ascii_images.lock().unwrap().clear();
}
pub(crate) fn as_ascii(&self, image: &Image) -> AsciiImage {
if let ImageSource::Filesystem(path) = &image.source {
if let Some(image) = self.ascii_images.lock().unwrap().get(path) {
return image.clone();
}
if let Some(TerminalImage::Ascii(image)) = self.images.lock().unwrap().get(path).map(|i| i.image.as_ref()) {
return image.clone();
}
}
let ascii_image = match image.image.deref() {
TerminalImage::Ascii(image) => image.clone(),
TerminalImage::Kitty(image) => DynamicImage::from(image.as_rgba8()).into(),
TerminalImage::Iterm(image) => DynamicImage::from(image.as_rgba8()).into(),
#[cfg(feature = "sixel")]
TerminalImage::Sixel(image) => DynamicImage::from(image.as_rgba8()).into(),
};
if let ImageSource::Filesystem(path) = &image.source {
self.ascii_images.lock().unwrap().insert(path.clone(), ascii_image.clone());
}
ascii_image
}
}

View File

@ -10,49 +10,49 @@ use itertools::Itertools;
use std::{
collections::HashMap,
fs,
ops::Deref,
sync::{Arc, Mutex},
};
const TOP_CHAR: &str = "";
const BOTTOM_CHAR: &str = "";
struct Inner {
image: DynamicImage,
cached_sizes: Mutex<HashMap<(u16, u16), RgbaImage>>,
}
#[derive(Clone)]
pub(crate) struct AsciiImage {
image: Arc<DynamicImage>,
cached_sizes: Arc<Mutex<HashMap<(u16, u16), RgbaImage>>>,
inner: Arc<Inner>,
}
impl AsciiImage {
pub(crate) fn cache_scaling(&self, columns: u16, rows: u16) {
let mut cached_sizes = self.cached_sizes.lock().unwrap();
let mut cached_sizes = self.inner.cached_sizes.lock().unwrap();
// lookup on cache/resize the image and store it in cache
let cache_key = (columns, rows);
if cached_sizes.get(&cache_key).is_none() {
let image = self.image.resize_exact(columns as u32, rows as u32, FilterType::Triangle);
let image = self.inner.image.resize_exact(columns as u32, rows as u32, FilterType::Triangle);
cached_sizes.insert(cache_key, image.into_rgba8());
}
}
pub(crate) fn image(&self) -> &DynamicImage {
&self.inner.image
}
}
impl ImageProperties for AsciiImage {
fn dimensions(&self) -> (u32, u32) {
self.image.dimensions()
self.inner.image.dimensions()
}
}
impl From<DynamicImage> for AsciiImage {
fn from(image: DynamicImage) -> Self {
let image = image.into_rgba8();
Self { image: Arc::new(image.into()), cached_sizes: Default::default() }
}
}
impl Deref for AsciiImage {
type Target = DynamicImage;
fn deref(&self) -> &Self::Target {
&self.image
let inner = Inner { image: image.into(), cached_sizes: Default::default() };
Self { inner: Arc::new(inner) }
}
}
@ -108,7 +108,7 @@ impl PrintImage for AsciiPrinter {
// lookup on cache/resize the image and store it in cache
let cache_key = (columns, rows);
let cached_sizes = image.cached_sizes.lock().unwrap();
let cached_sizes = image.inner.cached_sizes.lock().unwrap();
let image = cached_sizes.get(&cache_key).expect("scaled image no longer there");
let default_background = options.background_color.map(Color::from);

View File

@ -146,7 +146,7 @@ impl<I: TerminalWrite> Terminal<I> {
fn print_image(&mut self, image: &Image, options: &PrintOptions) -> Result<(), PrintImageError> {
let image_printer = self.image_printer.clone();
image_printer.print(&image.image, options, self)?;
image_printer.print(image.image(), options, self)?;
self.cursor_row += options.rows;
Ok(())
}

View File

@ -7,7 +7,7 @@ use super::{
printer::{TerminalError, TerminalIo},
};
use crate::{
ImageRegistry, WindowSize,
WindowSize,
markdown::{
elements::Text,
text_style::{Color, Colors, TextStyle},
@ -187,8 +187,8 @@ impl VirtualTerminal {
let image = PrintedImage { image: image.clone(), width_columns: options.columns };
self.images.insert(key, image);
}
ImageBehavior::PrintAscii(registry) => {
let image = registry.as_ascii(image);
ImageBehavior::PrintAscii => {
let image = image.to_ascii();
let image_printer = AsciiPrinter;
image_printer.print(&image, options, self)?
}
@ -228,7 +228,7 @@ impl TerminalIo for VirtualTerminal {
pub(crate) enum ImageBehavior {
#[default]
Store,
PrintAscii(ImageRegistry),
PrintAscii,
}
#[derive(Clone, Copy, Debug, PartialEq)]