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::Filesystem(path) => Ok(path),
ImageSource::Generated => { ImageSource::Generated => {
let mut buffer = Vec::new(); let mut buffer = Vec::new();
let dimensions = image.dimensions(); let dimensions = image.image().dimensions();
let TerminalImage::Ascii(resource) = image.image.as_ref() else { panic!("not in ascii mode") }; let TerminalImage::Ascii(image) = image.image() else { panic!("not in ascii mode") };
let image = image.image();
PngEncoder::new(&mut buffer).write_image( PngEncoder::new(&mut buffer).write_image(
resource.as_bytes(), image.as_bytes(),
dimensions.0, dimensions.0,
dimensions.1, dimensions.1,
resource.color().into(), image.color().into(),
)?; )?;
let name = format!("img-{}.png", self.image_count); let name = format!("img-{}.png", self.image_count);
let path = self.output_directory.path().join(name); 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. /// 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 { pub(crate) fn poll_slide_async_renders(&mut self, slide: usize) -> RenderAsyncState {
let slide = self.current_slide_mut(); let slide = &mut self.slides[slide];
let mut slide_state = RenderAsyncState::Rendered; let mut slide_state = RenderAsyncState::Rendered;
for operation in slide.iter_operations_mut() { for operation in slide.iter_operations_mut() {
if let RenderOperation::RenderAsync(operation) = operation { if let RenderOperation::RenderAsync(operation) = operation {

View File

@ -156,12 +156,12 @@ impl<'a> Presenter<'a> {
self.try_scale_transition_images()?; self.try_scale_transition_images()?;
break; break;
} }
CommandSideEffect::NextSlide => { CommandSideEffect::AnimateNextSlide => {
self.next_slide(&mut drawer)?; self.animate_next_slide(&mut drawer)?;
break; break;
} }
CommandSideEffect::PreviousSlide => { CommandSideEffect::AnimatePreviousSlide => {
self.previous_slide(&mut drawer)?; self.animate_previous_slide(&mut drawer)?;
break; break;
} }
CommandSideEffect::None => (), CommandSideEffect::None => (),
@ -203,15 +203,19 @@ impl<'a> Presenter<'a> {
return Ok(false); return Ok(false);
} }
let current_index = self.state.presentation().current_slide_index(); let current_index = self.state.presentation().current_slide_index();
if self.slides_with_pending_async_renders.contains(&current_index) { self.poll_slide_async_renders(current_index)
let state = self.state.presentation_mut().poll_slide_async_renders(); }
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 { match state {
RenderAsyncState::NotStarted | RenderAsyncState::Rendering { modified: false } => (), RenderAsyncState::NotStarted | RenderAsyncState::Rendering { modified: false } => (),
RenderAsyncState::Rendering { modified: true } => { RenderAsyncState::Rendering { modified: true } => {
return Ok(true); return Ok(true);
} }
RenderAsyncState::Rendered | RenderAsyncState::JustFinishedRendering => { RenderAsyncState::Rendered | RenderAsyncState::JustFinishedRendering => {
self.slides_with_pending_async_renders.remove(&current_index); self.slides_with_pending_async_renders.remove(&slide);
return Ok(true); return Ok(true);
} }
}; };
@ -279,7 +283,7 @@ impl<'a> Presenter<'a> {
if !presentation.jump_next() { if !presentation.jump_next() {
false false
} else if presentation.current_slide_index() != current_slide { } else if presentation.current_slide_index() != current_slide {
return CommandSideEffect::NextSlide; return CommandSideEffect::AnimateNextSlide;
} else { } else {
true true
} }
@ -290,7 +294,7 @@ impl<'a> Presenter<'a> {
if !presentation.jump_previous() { if !presentation.jump_previous() {
false false
} else if presentation.current_slide_index() != current_slide { } else if presentation.current_slide_index() != current_slide {
return CommandSideEffect::PreviousSlide; return CommandSideEffect::AnimatePreviousSlide;
} else { } else {
true true
} }
@ -361,7 +365,7 @@ impl<'a> Presenter<'a> {
return Ok(()); return Ok(());
} }
let options = RenderEngineOptions { max_size: self.options.max_size.clone(), ..Default::default() }; 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)?; let dimensions = WindowSize::current(self.options.font_size_fallback)?;
scaler.process(self.state.presentation(), &dimensions)?; scaler.process(self.state.presentation(), &dimensions)?;
Ok(()) 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 { let Some(config) = self.options.transition.clone() else {
return Ok(()); return Ok(());
}; };
let registry = self.resources.image_registry(); self.poll_and_scale_images()?;
let options = drawer.render_engine_options(); let options = drawer.render_engine_options();
let presentation = self.state.presentation_mut(); let presentation = self.state.presentation_mut();
let dimensions = WindowSize::current(self.options.font_size_fallback)?; let dimensions = WindowSize::current(self.options.font_size_fallback)?;
presentation.jump_previous(); 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(); 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; let direction = TransitionDirection::Next;
self.animate_transition(drawer, left, right, direction, dimensions, config) 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 { let Some(config) = self.options.transition.clone() else {
return Ok(()); return Ok(());
}; };
let registry = self.resources.image_registry(); self.poll_and_scale_images()?;
let options = drawer.render_engine_options(); let options = drawer.render_engine_options();
let presentation = self.state.presentation_mut(); let presentation = self.state.presentation_mut();
let dimensions = WindowSize::current(self.options.font_size_fallback)?; let dimensions = WindowSize::current(self.options.font_size_fallback)?;
presentation.jump_next(); 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(); 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; let direction = TransitionDirection::Previous;
self.animate_transition(drawer, left, right, direction, dimensions, config) self.animate_transition(drawer, left, right, direction, dimensions, config)
} }
@ -543,13 +553,23 @@ impl<'a> Presenter<'a> {
slide: &Slide, slide: &Slide,
dimensions: WindowSize, dimensions: WindowSize,
options: &RenderEngineOptions, options: &RenderEngineOptions,
registry: ImageRegistry,
) -> Result<TerminalGrid, RenderError> { ) -> 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()); let engine = RenderEngine::new(&mut term, dimensions.clone(), options.clone());
engine.render(slide.iter_visible_operations())?; engine.render(slide.iter_visible_operations())?;
Ok(term.into_contents()) 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 { enum CommandSideEffect {
@ -557,8 +577,8 @@ enum CommandSideEffect {
Suspend, Suspend,
Redraw, Redraw,
Reload, Reload,
NextSlide, AnimateNextSlide,
PreviousSlide, AnimatePreviousSlide,
None, None,
} }

View File

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

View File

@ -258,7 +258,7 @@ where
let starting_cursor = let starting_cursor =
CursorPosition { row: starting_row.saturating_sub(rect.start_row), column: rect.start_column }; 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 { let (columns, rows) = match properties.size {
ImageSize::ShrinkIfNeeded => { ImageSize::ShrinkIfNeeded => {
let image_scale = let image_scale =

View File

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

View File

@ -1,23 +1,59 @@
use image::DynamicImage;
use protocols::ascii::AsciiImage;
use self::printer::{ImageProperties, TerminalImage}; 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 printer;
pub(crate) mod protocols; pub(crate) mod protocols;
pub(crate) mod scale; pub(crate) mod scale;
struct Inner {
image: TerminalImage,
ascii_image: Mutex<Option<AsciiImage>>,
}
/// An image. /// An image.
/// ///
/// This stores the image in an [std::sync::Arc] so it's cheap to clone. /// This stores the image in an [std::sync::Arc] so it's cheap to clone.
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct Image { pub(crate) struct Image {
pub(crate) image: Arc<TerminalImage>, inner: Arc<Inner>,
pub(crate) source: ImageSource, pub(crate) source: ImageSource,
} }
impl Image { impl Image {
/// Constructs a new image. /// Constructs a new image.
pub(crate) fn new(image: TerminalImage, source: ImageSource) -> Self { 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 { impl Debug for Image {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 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}>") write!(f, "Image<{width}x{height}>")
} }
} }
impl Deref for Image {
type Target = TerminalImage;
fn deref(&self) -> &Self::Target {
&self.image
}
}
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub(crate) enum ImageSource { pub(crate) enum ImageSource {
Filesystem(PathBuf), Filesystem(PathBuf),

View File

@ -18,7 +18,6 @@ use std::{
borrow::Cow, borrow::Cow,
collections::HashMap, collections::HashMap,
fmt, io, fmt, io,
ops::Deref,
path::PathBuf, path::PathBuf,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
}; };
@ -151,12 +150,11 @@ impl PrintImage for ImagePrinter {
pub(crate) struct ImageRegistry { pub(crate) struct ImageRegistry {
printer: Arc<ImagePrinter>, printer: Arc<ImagePrinter>,
images: Arc<Mutex<HashMap<PathBuf, Image>>>, images: Arc<Mutex<HashMap<PathBuf, Image>>>,
ascii_images: Arc<Mutex<HashMap<PathBuf, AsciiImage>>>,
} }
impl ImageRegistry { impl ImageRegistry {
pub fn new(printer: Arc<ImagePrinter>) -> Self { 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); let image = Image::new(resource, source);
if let Some(key) = cache_key { if let Some(key) = cache_key {
images.insert(key.clone(), image.clone()); 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) Ok(image)
} }
pub(crate) fn clear(&self) { pub(crate) fn clear(&self) {
self.images.lock().unwrap().clear(); 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::{ use std::{
collections::HashMap, collections::HashMap,
fs, fs,
ops::Deref,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
}; };
const TOP_CHAR: &str = ""; const TOP_CHAR: &str = "";
const BOTTOM_CHAR: &str = ""; const BOTTOM_CHAR: &str = "";
struct Inner {
image: DynamicImage,
cached_sizes: Mutex<HashMap<(u16, u16), RgbaImage>>,
}
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct AsciiImage { pub(crate) struct AsciiImage {
image: Arc<DynamicImage>, inner: Arc<Inner>,
cached_sizes: Arc<Mutex<HashMap<(u16, u16), RgbaImage>>>,
} }
impl AsciiImage { impl AsciiImage {
pub(crate) fn cache_scaling(&self, columns: u16, rows: u16) { 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 // lookup on cache/resize the image and store it in cache
let cache_key = (columns, rows); let cache_key = (columns, rows);
if cached_sizes.get(&cache_key).is_none() { 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()); cached_sizes.insert(cache_key, image.into_rgba8());
} }
} }
pub(crate) fn image(&self) -> &DynamicImage {
&self.inner.image
}
} }
impl ImageProperties for AsciiImage { impl ImageProperties for AsciiImage {
fn dimensions(&self) -> (u32, u32) { fn dimensions(&self) -> (u32, u32) {
self.image.dimensions() self.inner.image.dimensions()
} }
} }
impl From<DynamicImage> for AsciiImage { impl From<DynamicImage> for AsciiImage {
fn from(image: DynamicImage) -> Self { fn from(image: DynamicImage) -> Self {
let image = image.into_rgba8(); let image = image.into_rgba8();
Self { image: Arc::new(image.into()), cached_sizes: Default::default() } let inner = Inner { image: image.into(), cached_sizes: Default::default() };
} Self { inner: Arc::new(inner) }
}
impl Deref for AsciiImage {
type Target = DynamicImage;
fn deref(&self) -> &Self::Target {
&self.image
} }
} }
@ -108,7 +108,7 @@ impl PrintImage for AsciiPrinter {
// lookup on cache/resize the image and store it in cache // lookup on cache/resize the image and store it in cache
let cache_key = (columns, rows); 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 image = cached_sizes.get(&cache_key).expect("scaled image no longer there");
let default_background = options.background_color.map(Color::from); 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> { fn print_image(&mut self, image: &Image, options: &PrintOptions) -> Result<(), PrintImageError> {
let image_printer = self.image_printer.clone(); 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; self.cursor_row += options.rows;
Ok(()) Ok(())
} }

View File

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