perf: pre-scale ascii images when transitions are enabled

This commit is contained in:
Matias Fontanini 2025-04-14 17:52:34 -07:00
parent 9e1f2beca2
commit eca6ce91bf
4 changed files with 144 additions and 16 deletions

View File

@ -13,6 +13,7 @@ use crate::{
},
render::{
ErrorSource, RenderError, RenderResult, TerminalDrawer, TerminalDrawerOptions,
ascii_scaler::AsciiScaler,
engine::{MaxSize, RenderEngine, RenderEngineOptions},
operation::RenderAsyncState,
properties::WindowSize,
@ -109,7 +110,7 @@ impl<'a> Presenter<'a> {
self.resources.watch_presentation_file(path.to_path_buf());
}
self.state = PresenterState::Presenting(Presentation::from(vec![]));
self.try_reload(path, true);
self.try_reload(path, true)?;
let drawer_options = TerminalDrawerOptions {
font_size_fallback: self.options.font_size_fallback,
@ -148,10 +149,11 @@ impl<'a> Presenter<'a> {
break;
}
CommandSideEffect::Reload => {
self.try_reload(path, false);
self.try_reload(path, false)?;
break;
}
CommandSideEffect::Redraw => {
self.try_scale_transition_images()?;
break;
}
CommandSideEffect::NextSlide => {
@ -326,9 +328,9 @@ impl<'a> Presenter<'a> {
if needs_redraw { CommandSideEffect::Redraw } else { CommandSideEffect::None }
}
fn try_reload(&mut self, path: &Path, force: bool) {
fn try_reload(&mut self, path: &Path, force: bool) -> RenderResult {
if matches!(self.options.mode, PresentMode::Presentation) && !force {
return;
return Ok(());
}
self.slides_with_pending_async_renders.clear();
self.resources.clear_watches();
@ -344,12 +346,25 @@ impl<'a> Presenter<'a> {
}
self.slides_with_pending_async_renders = presentation.slides_with_async_renders().into_iter().collect();
self.state = self.validate_overflows(presentation);
self.try_scale_transition_images()?;
}
Err(e) => {
let presentation = mem::take(&mut self.state).into_presentation();
self.state = PresenterState::failure(e, presentation, ErrorSource::Presentation, FailureMode::Other);
}
};
Ok(())
}
fn try_scale_transition_images(&self) -> RenderResult {
if self.options.transition.is_none() {
return Ok(());
}
let options = RenderEngineOptions { max_size: self.options.max_size.clone(), ..Default::default() };
let scaler = AsciiScaler::new(options, self.resources.image_registry());
let dimensions = WindowSize::current(self.options.font_size_fallback)?;
scaler.process(self.state.presentation(), &dimensions)?;
Ok(())
}
fn is_displaying_other_error(&self) -> bool {

106
src/render/ascii_scaler.rs Normal file
View File

@ -0,0 +1,106 @@
use super::{
RenderError,
engine::{RenderEngine, RenderEngineOptions},
};
use crate::{
ImageRegistry, WindowSize,
presentation::Presentation,
terminal::{
image::{Image, ImageSource},
printer::{TerminalCommand, TerminalError, TerminalIo},
},
};
use std::thread;
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 process(self, presentation: &Presentation, dimensions: &WindowSize) -> Result<(), RenderError> {
let mut collector = ImageCollector::default();
for slide in presentation.iter_slides() {
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));
Ok(())
}
fn scale(images: Vec<ScalableImage>, registry: ImageRegistry) {
for image in images {
let ascii_image = registry.as_ascii(&image.image);
ascii_image.cache_scaling(image.columns, image.rows);
}
}
}
struct ScalableImage {
image: Image,
rows: u16,
columns: u16,
}
struct ImageCollector {
current_column: u16,
current_row: u16,
current_row_height: u16,
images: Vec<ScalableImage>,
}
impl Default for ImageCollector {
fn default() -> Self {
Self { current_row: 0, current_column: 0, current_row_height: 1, images: Default::default() }
}
}
impl TerminalIo for ImageCollector {
fn execute(&mut self, command: &TerminalCommand<'_>) -> Result<(), TerminalError> {
use TerminalCommand::*;
match command {
MoveTo { column, row } => {
self.current_column = *column;
self.current_row = *row;
}
MoveToRow(row) => self.current_row = *row,
MoveToColumn(column) => self.current_column = *column,
MoveDown(amount) => self.current_row = self.current_row.saturating_add(*amount),
MoveRight(amount) => self.current_column = self.current_column.saturating_add(*amount),
MoveLeft(amount) => self.current_column = self.current_column.saturating_sub(*amount),
MoveToNextLine => {
self.current_row = self.current_row.saturating_add(1);
self.current_column = 0;
self.current_row_height = 1;
}
PrintText { content, style } => {
self.current_column = self.current_column.saturating_add(content.width() as u16);
self.current_row_height = self.current_row_height.max(style.size as u16);
}
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);
}
}
ClearScreen => {
self.current_column = 0;
self.current_row = 0;
self.current_row_height = 1;
}
BeginUpdate | EndUpdate | Flush | SetColors(_) | SetBackgroundColor(_) => (),
};
Ok(())
}
fn cursor_row(&self) -> u16 {
self.current_row
}
}

View File

@ -1,3 +1,4 @@
pub(crate) mod ascii_scaler;
pub(crate) mod engine;
pub(crate) mod layout;
pub(crate) mod operation;

View File

@ -23,6 +23,18 @@ pub(crate) struct AsciiImage {
cached_sizes: Arc<Mutex<HashMap<(u16, u16), RgbaImage>>>,
}
impl AsciiImage {
pub(crate) fn cache_scaling(&self, columns: u16, rows: u16) {
let mut cached_sizes = self.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);
cached_sizes.insert(cache_key, image.into_rgba8());
}
}
}
impl ImageProperties for AsciiImage {
fn dimensions(&self) -> (u32, u32) {
self.image.dimensions()
@ -91,20 +103,14 @@ impl PrintImage for AsciiPrinter {
{
let columns = options.columns;
let rows = options.rows * 2;
let mut cached_sizes = image.cached_sizes.lock().unwrap();
// Scale it first
image.cache_scaling(columns, rows);
// lookup on cache/resize the image and store it in cache
let cache_key = (columns, rows);
let image = match cached_sizes.get(&cache_key) {
Some(image) => image,
None => {
let image = image.image.resize_exact(columns as u32, rows as u32, FilterType::Triangle);
cached_sizes.insert(cache_key, image.into_rgba8());
cached_sizes.get(&cache_key).unwrap()
}
};
// The strategy here is taken from viuer: use half vertical ascii blocks in combination
// with foreground/background colors to fit 2 vertical pixels per cell. That is, cell (x, y)
// will contain the pixels at (x, y) and (x, y + 1) combined.
let cached_sizes = image.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);
// Iterate pixel rows in pairs to be able to merge both pixels in a single iteration.