Skip to content

Commit

Permalink
feat: avoid re-rendering code output and auto rendered blocks (#280)
Browse files Browse the repository at this point in the history
This changes the logic that processes the async rendered blocks (code
execution output and typst/mermaid/latex `+render` snippets) to avoid
re-rendering. Before this was okay as it only affected code execution
but now that conversion to images also happens asynchronously this can
cause some unnecessary flickering.
  • Loading branch information
mfontanini authored Jul 7, 2024
2 parents 7168afe + 9d91a83 commit 139885b
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 29 deletions.
29 changes: 22 additions & 7 deletions src/presentation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ impl Presentation {
for (index, slide) in self.slides.iter().enumerate() {
for operation in slide.iter_operations() {
if let RenderOperation::RenderAsync(operation) = operation {
if matches!(operation.poll_state(), RenderAsyncState::Rendering) {
if matches!(operation.poll_state(), RenderAsyncState::Rendering { .. }) {
indexes.insert(index);
break;
}
Expand All @@ -170,16 +170,28 @@ impl Presentation {
}

/// Poll every async render operation in the current slide and check whether they're completed.
pub(crate) fn async_renders_completed(&mut self) -> bool {
pub(crate) fn poll_slide_async_renders(&mut self) -> RenderAsyncState {
let slide = self.current_slide_mut();
let mut all_rendered = true;
let mut slide_state = RenderAsyncState::Rendered;
for operation in slide.iter_operations_mut() {
if let RenderOperation::RenderAsync(operation) = operation {
let is_rendered = matches!(operation.poll_state(), RenderAsyncState::Rendered);
all_rendered = all_rendered && is_rendered;
let state = operation.poll_state();
slide_state = match (&slide_state, &state) {
// If one finished rendering and another one still is rendering, claim that we
// are still rendering and there's modifications.
(RenderAsyncState::JustFinishedRendering, RenderAsyncState::Rendering { modified: false })
| (RenderAsyncState::Rendering { modified: false }, RenderAsyncState::JustFinishedRendering) => {
RenderAsyncState::Rendering { modified: true }
}
// Render + modified overrides anything, rendering overrides only "rendered".
(_, RenderAsyncState::Rendering { modified: true })
| (RenderAsyncState::Rendered, RenderAsyncState::Rendering { .. })
| (_, RenderAsyncState::JustFinishedRendering) => state,
_ => slide_state,
};
}
}
all_rendered
slide_state
}
/// Run a callback through every operation and let it mutate it in place.
///
Expand Down Expand Up @@ -623,8 +635,11 @@ pub(crate) trait RenderAsync: AsRenderOperations {
pub(crate) enum RenderAsyncState {
#[default]
NotStarted,
Rendering,
Rendering {
modified: bool,
},
Rendered,
JustFinishedRendering,
}

#[cfg(test)]
Expand Down
35 changes: 20 additions & 15 deletions src/presenter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::{
input::source::{Command, CommandSource},
markdown::parse::{MarkdownParser, ParseError},
media::{printer::ImagePrinter, register::ImageRegistry},
presentation::Presentation,
presentation::{Presentation, RenderAsyncState},
processing::builder::{BuildError, PresentationBuilder, PresentationBuilderOptions, Themes},
render::{
draw::{ErrorSource, RenderError, RenderResult, TerminalDrawer},
Expand Down Expand Up @@ -91,10 +91,14 @@ impl<'a> Presenter<'a> {
let mut drawer =
TerminalDrawer::new(io::stdout(), self.image_printer.clone(), self.options.font_size_fallback)?;
loop {
// Poll async renders once before we draw just in case.
self.poll_async_renders()?;
self.render(&mut drawer)?;

loop {
self.update_async_renders(&mut drawer)?;
if self.poll_async_renders()? {
self.render(&mut drawer)?;
}
let Some(command) = self.commands.try_next_command()? else {
if self.check_async_error() {
break;
Expand All @@ -110,9 +114,6 @@ impl<'a> Presenter<'a> {
CommandSideEffect::Redraw => {
break;
}
CommandSideEffect::PollAsyncRenders => {
self.slides_with_pending_async_renders.insert(self.state.presentation().current_slide_index());
}
CommandSideEffect::None => (),
};
}
Expand All @@ -132,17 +133,22 @@ impl<'a> Presenter<'a> {
}
}

fn update_async_renders(&mut self, drawer: &mut TerminalDrawer<Stdout>) -> RenderResult {
fn poll_async_renders(&mut self) -> Result<bool, RenderError> {
let current_index = self.state.presentation().current_slide_index();
if self.slides_with_pending_async_renders.contains(&current_index) {
self.render(drawer)?;
if self.state.presentation_mut().async_renders_completed() {
// Render one last time just in case it _just_ rendered
self.render(drawer)?;
self.slides_with_pending_async_renders.remove(&current_index);
}
let state = self.state.presentation_mut().poll_slide_async_renders();
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);
return Ok(true);
}
};
}
Ok(())
Ok(false)
}

fn render(&mut self, drawer: &mut TerminalDrawer<Stdout>) -> RenderResult {
Expand Down Expand Up @@ -205,7 +211,7 @@ impl<'a> Presenter<'a> {
Command::RenderAsyncOperations => {
if presentation.trigger_slide_async_renders() {
self.slides_with_pending_async_renders.insert(self.state.presentation().current_slide_index());
return CommandSideEffect::PollAsyncRenders;
return CommandSideEffect::Redraw;
} else {
return CommandSideEffect::None;
}
Expand Down Expand Up @@ -319,7 +325,6 @@ impl<'a> Presenter<'a> {
enum CommandSideEffect {
Exit,
Redraw,
PollAsyncRenders,
Reload,
None,
}
Expand Down
10 changes: 6 additions & 4 deletions src/processing/execution.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use super::separator::RenderSeparator;
use crate::{
execute::{CodeExecutor, ExecutionHandle, ExecutionState, ProcessStatus},
markdown::elements::{Code, Text, TextBlock},
Expand All @@ -10,8 +11,6 @@ use itertools::Itertools;
use std::{cell::RefCell, rc::Rc};
use unicode_width::UnicodeWidthStr;

use super::separator::RenderSeparator;

#[derive(Debug)]
struct RunCodeOperationInner {
handle: Option<ExecutionHandle>,
Expand Down Expand Up @@ -116,9 +115,12 @@ impl RenderAsync for RunCodeOperation {
Text::new("finished with error", TextStyle::default().colors(self.status_colors.failure.clone()))
}
};
let modified = inner.output_lines.len() != output.len();
if status.is_finished() {
inner.handle.take();
inner.state = RenderAsyncState::Rendered;
inner.state = RenderAsyncState::JustFinishedRendering;
} else {
inner.state = RenderAsyncState::Rendering { modified };
}
inner.output_lines = output;
}
Expand All @@ -133,7 +135,7 @@ impl RenderAsync for RunCodeOperation {
match self.executor.execute(&self.code) {
Ok(handle) => {
inner.handle = Some(handle);
inner.state = RenderAsyncState::Rendering;
inner.state = RenderAsyncState::Rendering { modified: false };
true
}
Err(e) => {
Expand Down
6 changes: 3 additions & 3 deletions src/third_party.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,13 +345,13 @@ impl RenderAsync for RenderThirdParty {
match mem::take(&mut *self.pending_result.lock().unwrap()) {
RenderResult::Success(image) => {
*contents = Some(image);
RenderAsyncState::Rendered
RenderAsyncState::JustFinishedRendering
}
RenderResult::Failure(error) => {
*self.error_holder.lock().unwrap() = Some(AsyncPresentationError { slide: self.slide, error });
RenderAsyncState::Rendered
RenderAsyncState::JustFinishedRendering
}
RenderResult::Pending => RenderAsyncState::Rendering,
RenderResult::Pending => RenderAsyncState::Rendering { modified: false },
}
}
}
Expand Down

0 comments on commit 139885b

Please sign in to comment.