diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0d38edb9..0cd48a56 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -103,7 +103,7 @@ jobs: - NetBSD x86_64 # Solaris - - Solaris x86_64 + # - Solaris x86_64 # weird error # https://travis-ci.org/LiveSplit/livesplit-core/jobs/327011754 # - env: TARGET=sparcv9-sun-solaris @@ -533,11 +533,13 @@ jobs: tests: skip # Solaris - - label: Solaris x86_64 - target: x86_64-sun-solaris - os: ubuntu-latest - tests: skip - dylib: skip + # - label: Solaris x86_64 + # target: x86_64-sun-solaris + # os: ubuntu-latest + # tests: skip + # dylib: skip + # FIXME: Solaris stopped working. core isn't available: + # https://github.com/LiveSplit/livesplit-core/runs/2777745289?check_suite_focus=true # Testing other channels - label: Windows Beta diff --git a/Cargo.toml b/Cargo.toml index 13ed02a8..23747b86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ serde_json = { version = "1.0.8", optional = true } utf-8 = { version = "0.7.4", optional = true } # Rendering +ahash = { version = "0.7.0", default-features = false, optional = true } euclid = { version = "0.22.1", default-features = false, optional = true } rustybuzz = { version = "0.3.0", optional = true } ttf-parser = { version = "0.12.0", optional = true } @@ -62,7 +63,7 @@ ttf-parser = { version = "0.12.0", optional = true } font-kit = { version = "0.10.0", optional = true } # Software Rendering -tiny-skia = { version = "0.4.2", optional = true } +tiny-skia = { version = "0.5.1", optional = true } # Networking splits-io-api = { version = "0.2.0", optional = true } @@ -84,7 +85,7 @@ doesnt-have-atomics = [] std = ["byteorder", "chrono/std", "chrono/clock", "image", "indexmap", "livesplit-hotkey/std", "parking_lot", "quick-xml", "serde_json", "serde/std", "snafu/std", "utf-8"] more-image-formats = ["image/webp", "image/pnm", "image/ico", "image/jpeg", "image/tiff", "image/tga", "image/bmp", "image/hdr"] image-shrinking = ["std", "bytemuck", "more-image-formats"] -rendering = ["std", "more-image-formats", "euclid", "ttf-parser", "rustybuzz", "bytemuck/derive"] +rendering = ["std", "more-image-formats", "euclid", "ttf-parser", "rustybuzz", "bytemuck/derive", "ahash"] font-loading = ["std", "rendering", "font-kit"] software-rendering = ["rendering", "tiny-skia"] wasm-web = ["std", "web-sys", "chrono/wasmbind", "livesplit-hotkey/wasm-web", "parking_lot/wasm-bindgen"] @@ -108,15 +109,15 @@ name = "balanced_pb" harness = false [[bench]] -name = "dummy_rendering" +name = "layout_state" harness = false [[bench]] -name = "layout_state" +name = "parsing" harness = false [[bench]] -name = "parsing" +name = "scene_management" harness = false [[bench]] diff --git a/benches/dummy_rendering.rs b/benches/scene_management.rs similarity index 78% rename from benches/dummy_rendering.rs rename to benches/scene_management.rs index 9a9854d6..cc019246 100644 --- a/benches/dummy_rendering.rs +++ b/benches/scene_management.rs @@ -3,7 +3,7 @@ cfg_if::cfg_if! { use criterion::{criterion_group, criterion_main, Criterion}; use livesplit_core::{ layout::{self, Layout}, - rendering::{Backend, FillShader, PathBuilder, Renderer, Rgba, Transform}, + rendering::{PathBuilder, ResourceAllocator, SceneManager}, run::parser::livesplit, Run, Segment, TimeSpan, Timer, TimingMethod, }; @@ -25,7 +25,7 @@ cfg_if::cfg_if! { fn finish(self, _: &mut Dummy) -> Self::Path {} } - impl Backend for Dummy { + impl ResourceAllocator for Dummy { type PathBuilder = Dummy; type Path = (); type Image = (); @@ -33,12 +33,7 @@ cfg_if::cfg_if! { fn path_builder(&mut self) -> Self::PathBuilder { Dummy } - fn render_fill_path(&mut self, _: &Self::Path, _: FillShader, _: Transform) {} - fn render_stroke_path(&mut self, _: &Self::Path, _: f32, _: Rgba, _: Transform) {} - fn render_image(&mut self, _: &Self::Image, _: &Self::Path, _: Transform) {} - fn free_path(&mut self, _: Self::Path) {} fn create_image(&mut self, _: u32, _: u32, _: &[u8]) -> Self::Image {} - fn free_image(&mut self, _: Self::Image) {} } fn default(c: &mut Criterion) { @@ -54,10 +49,10 @@ cfg_if::cfg_if! { let state = layout.state(&timer.snapshot()); - let mut renderer = Renderer::new(); + let mut manager = SceneManager::new(Dummy); - c.bench_function("Dummy Rendering (Default)", move |b| { - b.iter(|| renderer.render(&mut Dummy, (300.0, 500.0), &state)) + c.bench_function("Scene Management (Default)", move |b| { + b.iter(|| manager.update_scene(Dummy, (300.0, 500.0), &state)) }); } @@ -67,16 +62,19 @@ cfg_if::cfg_if! { let mut layout = lsl("tests/layout_files/subsplits.lsl"); start_run(&mut timer); - make_progress_run_with_splits_opt(&mut timer, &[Some(10.0), None, Some(20.0), Some(55.0)]); + make_progress_run_with_splits_opt( + &mut timer, + &[Some(10.0), None, Some(20.0), Some(55.0)], + ); let snapshot = timer.snapshot(); let mut state = layout.state(&snapshot); layout.update_state(&mut state, &snapshot); - let mut renderer = Renderer::new(); + let mut manager = SceneManager::new(Dummy); - c.bench_function("Dummy Rendering (Subsplits Layout)", move |b| { - b.iter(|| renderer.render(&mut Dummy, (300.0, 800.0), &state)) + c.bench_function("Scene Management (Subsplits Layout)", move |b| { + b.iter(|| manager.update_scene(Dummy, (300.0, 800.0), &state)) }); } diff --git a/benches/software_rendering.rs b/benches/software_rendering.rs index 711e47d3..25afb202 100644 --- a/benches/software_rendering.rs +++ b/benches/software_rendering.rs @@ -4,7 +4,7 @@ cfg_if::cfg_if! { criterion::{criterion_group, criterion_main, Criterion}, livesplit_core::{ layout::{self, Layout}, - rendering::software::SoftwareRenderer, + rendering::software::Renderer, run::parser::livesplit, Run, Segment, TimeSpan, Timer, TimingMethod, }, @@ -27,7 +27,7 @@ cfg_if::cfg_if! { let snapshot = timer.snapshot(); let state = layout.state(&snapshot); - let mut renderer = SoftwareRenderer::new(); + let mut renderer = Renderer::new(); c.bench_function("Software Rendering (Default)", move |b| { b.iter(|| renderer.render(&state, [300, 500])) @@ -44,7 +44,7 @@ cfg_if::cfg_if! { let snapshot = timer.snapshot(); let state = layout.state(&snapshot); - let mut renderer = SoftwareRenderer::new(); + let mut renderer = Renderer::new(); c.bench_function("Software Rendering (Subsplits Layout)", move |b| { b.iter(|| renderer.render(&state, [300, 800])) diff --git a/capi/bind_gen/src/typescript.ts b/capi/bind_gen/src/typescript.ts index ed1a9b0d..f4644559 100644 --- a/capi/bind_gen/src/typescript.ts +++ b/capi/bind_gen/src/typescript.ts @@ -13,7 +13,7 @@ export type ComponentStateJson = /** * Colors can be used to describe what color to use for visualizing backgrounds, * texts, lines and various other elements that are being shown. They are stored - * as RGBA colors with float point numbers ranging from 0.0 to 1.0 per channel. + * as RGBA colors with floating point numbers ranging from 0.0 to 1.0 per channel. */ export type Color = number[]; @@ -198,6 +198,11 @@ export interface TimerComponentStateJson { bottom_color: Color, /** The height of the timer. */ height: number, + /** + * This value indicates whether the timer is currently frequently being + * updated. This can be used for rendering optimizations. + */ + updates_frequently: boolean, } /** The state object describes the information to visualize for this component. */ @@ -348,6 +353,11 @@ export interface SplitColumnState { semantic_color: SemanticColor, /** The visual color of the value. */ visual_color: Color, + /** + * This value indicates whether the column is currently frequently being + * updated. This can be used for rendering optimizations. + */ + updates_frequently: boolean, } /** @@ -383,6 +393,11 @@ export interface KeyValueComponentStateJson { * two separate rows. */ display_two_rows: boolean, + /** + * This value indicates whether the value is currently frequently being + * updated. This can be used for rendering optimizations. + */ + updates_frequently: boolean, } /** @@ -598,7 +613,6 @@ export type SettingsDescriptionValueJson = { Int: number } | { String: string } | { OptionalString: string | null } | - { Float: number } | { Accuracy: AccuracyJson } | { DigitsFormat: DigitsFormatJson } | { OptionalTimingMethod: TimingMethodJson | null } | diff --git a/capi/src/layout_editor.rs b/capi/src/layout_editor.rs index 157bd4a1..89585266 100644 --- a/capi/src/layout_editor.rs +++ b/capi/src/layout_editor.rs @@ -4,11 +4,11 @@ //! state objects that can be visualized by any kind of User Interface. use super::{output_vec, Json}; -use crate::component::OwnedComponent; -use crate::layout::OwnedLayout; -use crate::layout_editor_state::OwnedLayoutEditorState; -use crate::setting_value::OwnedSettingValue; -use livesplit_core::{LayoutEditor, Timer}; +use crate::{ + component::OwnedComponent, layout::OwnedLayout, layout_editor_state::OwnedLayoutEditorState, + setting_value::OwnedSettingValue, +}; +use livesplit_core::{layout::LayoutState, LayoutEditor, Timer}; /// type pub type OwnedLayoutEditor = Box; @@ -58,6 +58,30 @@ pub extern "C" fn LayoutEditor_layout_state_as_json( }) } +/// Updates the layout's state based on the timer provided. +#[no_mangle] +pub extern "C" fn LayoutEditor_update_layout_state( + this: &mut LayoutEditor, + state: &mut LayoutState, + timer: &Timer, +) { + this.update_layout_state(state, &timer.snapshot()) +} + +/// Updates the layout's state based on the timer provided and encodes it as +/// JSON. +#[no_mangle] +pub extern "C" fn LayoutEditor_update_layout_state_as_json( + this: &mut LayoutEditor, + state: &mut LayoutState, + timer: &Timer, +) -> Json { + this.update_layout_state(state, &timer.snapshot()); + output_vec(|o| { + state.write_json(o).unwrap(); + }) +} + /// Selects the component with the given index in order to modify its /// settings. Only a single component is selected at any given time. You may /// not provide an invalid index. diff --git a/capi/src/setting_value.rs b/capi/src/setting_value.rs index 94ba6aea..1201babe 100644 --- a/capi/src/setting_value.rs +++ b/capi/src/setting_value.rs @@ -76,12 +76,6 @@ pub extern "C" fn SettingValue_from_optional_empty_string() -> OwnedSettingValue Box::new(None::.into()) } -/// Creates a new setting value from a floating point number. -#[no_mangle] -pub extern "C" fn SettingValue_from_float(value: f64) -> OwnedSettingValue { - Box::new(value.into()) -} - /// Creates a new setting value from an accuracy name. If it doesn't match a /// known accuracy, is returned. #[no_mangle] diff --git a/capi/src/software_renderer.rs b/capi/src/software_renderer.rs index 78959c8d..483cf043 100644 --- a/capi/src/software_renderer.rs +++ b/capi/src/software_renderer.rs @@ -4,7 +4,7 @@ use livesplit_core::layout::LayoutState; #[cfg(feature = "software-rendering")] -use livesplit_core::rendering::software::BorrowedSoftwareRenderer as SoftwareRenderer; +use livesplit_core::rendering::software::BorrowedRenderer as SoftwareRenderer; #[cfg(not(feature = "software-rendering"))] /// dummy @@ -15,7 +15,7 @@ impl SoftwareRenderer { panic!("The software renderer is not compiled in.") } - fn render(&mut self, _: &LayoutState, _: &mut [u8], _: [u32; 2], _: u32) {} + fn render(&mut self, _: &LayoutState, _: &mut [u8], _: [u32; 2], _: u32, _: bool) {} } /// type @@ -33,13 +33,15 @@ pub extern "C" fn SoftwareRenderer_drop(this: OwnedSoftwareRenderer) { drop(this); } -/// Renders the layout state provided into the image buffer provided. The -/// image has to be an array of RGBA8 encoded pixels (red, green, blue, -/// alpha with each channel being an u8). Some frameworks may over allocate -/// an image's dimensions. So an image with dimensions 100x50 may be over -/// allocated as 128x64. In that case you provide the real dimensions of -/// 100 and 50 as the width and height, but a stride of 128 pixels as that -/// correlates with the real width of the underlying buffer. +/// Renders the layout state provided into the image buffer provided. The image +/// has to be an array of RGBA8 encoded pixels (red, green, blue, alpha with +/// each channel being an u8). Some frameworks may over allocate an image's +/// dimensions. So an image with dimensions 100x50 may be over allocated as +/// 128x64. In that case you provide the real dimensions of 100x50 as the width +/// and height, but a stride of 128 pixels as that correlates with the real +/// width of the underlying buffer. By default the renderer will try not to +/// redraw parts of the image that haven't changed. You can force a redraw in +/// case the image provided or its contents have changed. #[no_mangle] pub unsafe extern "C" fn SoftwareRenderer_render( this: &mut SoftwareRenderer, @@ -48,11 +50,13 @@ pub unsafe extern "C" fn SoftwareRenderer_render( width: u32, height: u32, stride: u32, + force_redraw: bool, ) { this.render( layout_state, std::slice::from_raw_parts_mut(data, stride as usize * height as usize * 4), [width, height], stride, + force_redraw, ); } diff --git a/src/analysis/current_pace.rs b/src/analysis/current_pace.rs index fdf23195..8e21a0e0 100644 --- a/src/analysis/current_pace.rs +++ b/src/analysis/current_pace.rs @@ -7,7 +7,7 @@ use crate::{analysis, timing::Snapshot, TimeSpan, TimerPhase}; /// Calculates the current pace of the active attempt based on the comparison /// provided. If there's no active attempt, the final time of the comparison is /// returned instead. -pub fn calculate(timer: &Snapshot<'_>, comparison: &str) -> Option { +pub fn calculate(timer: &Snapshot<'_>, comparison: &str) -> (Option, bool) { let timing_method = timer.current_timing_method(); let last_segment = timer.run().segments().last().unwrap(); @@ -21,20 +21,28 @@ pub fn calculate(timer: &Snapshot<'_>, comparison: &str) -> Option { ) .unwrap_or_default(); + let mut is_live = false; + catch! { let live_delta = timer.current_time()[timing_method]? - timer.current_split().unwrap().comparison(comparison)[timing_method]?; if live_delta > delta { delta = live_delta; + is_live = true; } }; - catch! { + let value = catch! { last_segment.comparison(comparison)[timing_method]? + delta - } + }; + + ( + value, + is_live && timer.current_phase().is_running() && value.is_some(), + ) } - TimerPhase::Ended => last_segment.split_time()[timing_method], - TimerPhase::NotRunning => last_segment.comparison(comparison)[timing_method], + TimerPhase::Ended => (last_segment.split_time()[timing_method], false), + TimerPhase::NotRunning => (last_segment.comparison(comparison)[timing_method], false), } } diff --git a/src/analysis/pb_chance/mod.rs b/src/analysis/pb_chance/mod.rs index 73ac6003..35363a5e 100644 --- a/src/analysis/pb_chance/mod.rs +++ b/src/analysis/pb_chance/mod.rs @@ -39,14 +39,17 @@ pub fn for_run(run: &Run, method: TimingMethod) -> f64 { /// Calculates the PB chance for a timer. The chance is calculated in terms of /// the current attempt. If there is no attempt in progress it yields the same /// result as the PB chance for the run. The value is being reported as a -/// floating point number in the range from 0 (0%) to 1 (100%). -pub fn for_timer(timer: &Snapshot<'_>) -> f64 { +/// floating point number in the range from 0 (0%) to 1 (100%). Additionally a +/// boolean is returned that indicates if the value is currently actively +/// changing as time is being lost. +pub fn for_timer(timer: &Snapshot<'_>) -> (f64, bool) { let method = timer.current_timing_method(); let all_segments = timer.run().segments(); - let live_delta = super::check_live_delta(timer, false, comparison::personal_best::NAME, method); + let is_live = + super::check_live_delta(timer, false, comparison::personal_best::NAME, method).is_some(); - let (segments, current_time) = if live_delta.is_some() { + let (segments, current_time) = if is_live { // If there is a live delta, act as if we did just split. ( &all_segments[timer.current_split_index().unwrap() + 1..], @@ -71,7 +74,7 @@ pub fn for_timer(timer: &Snapshot<'_>) -> f64 { // final split, then we want to simply compare the current time to the PB // time and then either return 100% or 0% based on whether our new time is a // PB or not. - if segments.is_empty() { + let chance = if segments.is_empty() { let beat_pb = all_segments .last() .and_then(|s| s.personal_best_split_time()[method]) @@ -83,5 +86,7 @@ pub fn for_timer(timer: &Snapshot<'_>) -> f64 { } } else { calculate(segments, method, current_time) - } + }; + + (chance, is_live) } diff --git a/src/analysis/pb_chance/tests.rs b/src/analysis/pb_chance/tests.rs index 9121f1b7..8668a8d4 100644 --- a/src/analysis/pb_chance/tests.rs +++ b/src/analysis/pb_chance/tests.rs @@ -1,11 +1,13 @@ use super::for_timer; -use crate::tests_helper::{ - create_timer, make_progress_run_with_splits_opt, run_with_splits, span, start_run, +use crate::{ + tests_helper::{ + create_timer, make_progress_run_with_splits_opt, run_with_splits, span, start_run, + }, + Timer, TimerPhase, }; -use crate::{Timer, TimerPhase}; fn chance(timer: &Timer) -> u32 { - (for_timer(&timer.snapshot()) * 100.0).round() as _ + (for_timer(&timer.snapshot()).0 * 100.0).round() as _ } #[test] diff --git a/src/component/current_comparison.rs b/src/component/current_comparison.rs index b58a4815..5c62c05d 100644 --- a/src/component/current_comparison.rs +++ b/src/component/current_comparison.rs @@ -3,9 +3,11 @@ //! comparison that is currently selected to be compared against. use super::key_value; -use crate::platform::prelude::*; -use crate::settings::{Color, Field, Gradient, SettingsDescription, Value}; -use crate::Timer; +use crate::{ + platform::prelude::*, + settings::{Color, Field, Gradient, SettingsDescription, Value}, + Timer, +}; use serde::{Deserialize, Serialize}; /// The Current Comparison Component is a component that shows the name of the @@ -86,6 +88,7 @@ impl Component { state.key_abbreviations.push("Comparison".into()); state.display_two_rows = self.settings.display_two_rows; + state.updates_frequently = false; } /// Calculates the component's state based on the timer provided. diff --git a/src/component/current_pace.rs b/src/component/current_pace.rs index 7ed52a1f..962211b9 100644 --- a/src/component/current_pace.rs +++ b/src/component/current_pace.rs @@ -4,14 +4,17 @@ //! comparison for the remainder of the run. use super::key_value; -use crate::analysis::current_pace; -use crate::platform::prelude::*; -use crate::settings::{Color, Field, Gradient, SettingsDescription, Value}; -use crate::timing::{ - formatter::{Accuracy, Regular, TimeFormatter}, - Snapshot, +use crate::{ + analysis::current_pace, + comparison, + platform::prelude::*, + settings::{Color, Field, Gradient, SettingsDescription, Value}, + timing::{ + formatter::{Accuracy, Regular, TimeFormatter}, + Snapshot, + }, + TimerPhase, }; -use crate::{comparison, TimerPhase}; use alloc::borrow::Cow; use core::fmt::Write; use serde::{Deserialize, Serialize}; @@ -105,9 +108,9 @@ impl Component { let comparison = comparison::or_current(comparison, timer); let key = self.text(Some(comparison)); - let current_pace = + let (current_pace, updates_frequently) = if timer.current_phase() == TimerPhase::NotRunning && key.starts_with("Current Pace") { - None + (None, false) } else { current_pace::calculate(timer, comparison) }; @@ -154,6 +157,7 @@ impl Component { } state.display_two_rows = self.settings.display_two_rows; + state.updates_frequently = updates_frequently; } /// Calculates the component's state based on the timer provided. diff --git a/src/component/delta/mod.rs b/src/component/delta/mod.rs index bb96eb47..d4148ed3 100644 --- a/src/component/delta/mod.rs +++ b/src/component/delta/mod.rs @@ -3,14 +3,17 @@ //! attempt is compared to the chosen comparison. use super::key_value; -use crate::analysis::{delta, state_helper}; -use crate::platform::prelude::*; -use crate::settings::{Color, Field, Gradient, SemanticColor, SettingsDescription, Value}; -use crate::timing::{ - formatter::{Accuracy, Delta, TimeFormatter}, - Snapshot, +use crate::{ + analysis::{delta, state_helper}, + comparison, + platform::prelude::*, + settings::{Color, Field, Gradient, SemanticColor, SettingsDescription, Value}, + timing::{ + formatter::{Accuracy, Delta, TimeFormatter}, + Snapshot, + }, + GeneralLayoutSettings, }; -use crate::{comparison, GeneralLayoutSettings}; use alloc::borrow::Cow; use core::fmt::Write; use serde::{Deserialize, Serialize}; @@ -145,6 +148,7 @@ impl Component { } state.display_two_rows = self.settings.display_two_rows; + state.updates_frequently = use_live_delta; } /// Calculates the component's state based on the timer and the layout diff --git a/src/component/key_value.rs b/src/component/key_value.rs index ff834aff..b5d9f9fe 100644 --- a/src/component/key_value.rs +++ b/src/component/key_value.rs @@ -33,6 +33,9 @@ pub struct State { pub key_abbreviations: Vec>, /// Specifies whether to display the key and the value in two separate rows. pub display_two_rows: bool, + /// This value indicates whether the value is currently frequently being + /// updated. This can be used for rendering optimizations. + pub updates_frequently: bool, } #[cfg(feature = "std")] diff --git a/src/component/pb_chance.rs b/src/component/pb_chance.rs index c76754c9..e7020cc5 100644 --- a/src/component/pb_chance.rs +++ b/src/component/pb_chance.rs @@ -5,9 +5,12 @@ //! how well the attempt is going. use super::key_value; -use crate::platform::prelude::*; -use crate::settings::{Color, Field, Gradient, SettingsDescription, Value}; -use crate::{analysis::pb_chance, timing::Snapshot}; +use crate::{ + analysis::pb_chance, + platform::prelude::*, + settings::{Color, Field, Gradient, SettingsDescription, Value}, + timing::Snapshot, +}; use core::fmt::Write; use serde::{Deserialize, Serialize}; @@ -76,7 +79,7 @@ impl Component { /// Updates the component's state based on the timer provided. pub fn update_state(&self, state: &mut key_value::State, timer: &Snapshot<'_>) { - let chance = pb_chance::for_timer(timer); + let (chance, is_live) = pb_chance::for_timer(timer); state.background = self.settings.background; state.key_color = self.settings.label_color; @@ -91,6 +94,7 @@ impl Component { state.key_abbreviations.clear(); state.display_two_rows = self.settings.display_two_rows; + state.updates_frequently = is_live; } /// Calculates the component's state based on the timer provided. diff --git a/src/component/possible_time_save.rs b/src/component/possible_time_save.rs index 09342dfe..a27fba17 100644 --- a/src/component/possible_time_save.rs +++ b/src/component/possible_time_save.rs @@ -5,14 +5,17 @@ //! for the remainder of the current attempt. use super::key_value; -use crate::analysis::possible_time_save; -use crate::platform::prelude::*; -use crate::settings::{Color, Field, Gradient, SettingsDescription, Value}; -use crate::timing::{ - formatter::{Accuracy, SegmentTime, TimeFormatter}, - Snapshot, +use crate::{ + analysis::possible_time_save, + comparison, + platform::prelude::*, + settings::{Color, Field, Gradient, SettingsDescription, Value}, + timing::{ + formatter::{Accuracy, SegmentTime, TimeFormatter}, + Snapshot, + }, + TimerPhase, }; -use crate::{comparison, TimerPhase}; use alloc::borrow::Cow; use core::fmt::Write as FmtWrite; use serde::{Deserialize, Serialize}; @@ -160,6 +163,7 @@ impl Component { } state.display_two_rows = self.settings.display_two_rows; + state.updates_frequently = false; } /// Calculates the component's state based on the timer provided. diff --git a/src/component/previous_segment.rs b/src/component/previous_segment.rs index 852ed0c3..c7a73d01 100644 --- a/src/component/previous_segment.rs +++ b/src/component/previous_segment.rs @@ -6,13 +6,16 @@ //! active time loss whenever the runner is losing time on the current segment. use super::key_value; -use crate::platform::prelude::*; -use crate::settings::{Color, Field, Gradient, SemanticColor, SettingsDescription, Value}; -use crate::timing::{ - formatter::{Accuracy, Delta, SegmentTime, TimeFormatter}, - Snapshot, +use crate::{ + analysis, comparison, + platform::prelude::*, + settings::{Color, Field, Gradient, SemanticColor, SettingsDescription, Value}, + timing::{ + formatter::{Accuracy, Delta, SegmentTime, TimeFormatter}, + Snapshot, + }, + GeneralLayoutSettings, TimerPhase, }; -use crate::{analysis, comparison, GeneralLayoutSettings, TimerPhase}; use alloc::borrow::Cow; use core::fmt::Write as FmtWrite; use serde::{Deserialize, Serialize}; @@ -232,6 +235,7 @@ impl Component { } state.display_two_rows = self.settings.display_two_rows; + state.updates_frequently = live_segment.is_some(); } /// Calculates the component's state based on the timer and the layout diff --git a/src/component/segment_time/mod.rs b/src/component/segment_time/mod.rs index 24e0ccc8..86208fb3 100644 --- a/src/component/segment_time/mod.rs +++ b/src/component/segment_time/mod.rs @@ -4,11 +4,14 @@ //! comparison. use super::key_value; -use crate::analysis::state_helper::comparison_single_segment_time; -use crate::platform::prelude::*; -use crate::settings::{Color, Field, Gradient, SettingsDescription, Value}; -use crate::timing::formatter::{Accuracy, SegmentTime, TimeFormatter}; -use crate::{comparison, Timer, TimerPhase}; +use crate::{ + analysis::state_helper::comparison_single_segment_time, + comparison, + platform::prelude::*, + settings::{Color, Field, Gradient, SettingsDescription, Value}, + timing::formatter::{Accuracy, SegmentTime, TimeFormatter}, + Timer, TimerPhase, +}; use alloc::borrow::Cow; use core::fmt::Write; use serde::{Deserialize, Serialize}; @@ -165,6 +168,7 @@ impl Component { }; state.display_two_rows = self.settings.display_two_rows; + state.updates_frequently = false; } /// Calculates the component's state based on the timer provided. diff --git a/src/component/splits/column.rs b/src/component/splits/column.rs index b3201b23..66f4003a 100644 --- a/src/component/splits/column.rs +++ b/src/component/splits/column.rs @@ -124,6 +124,9 @@ pub struct ColumnState { pub semantic_color: SemanticColor, /// The visual color of the value. pub visual_color: Color, + /// This value indicates whether the column is currently frequently being + /// updated. This can be used for rendering optimizations. + pub updates_frequently: bool, } impl Clear for ColumnState { @@ -164,33 +167,39 @@ pub fn update_state( let updated = update_value.is_some(); - let (column_value, semantic_color, formatter) = - update_value.unwrap_or_else(|| match column.start_with { - ColumnStartWith::Empty => (None, SemanticColor::Default, ColumnFormatter::Time), - ColumnStartWith::ComparisonTime => ( - segment.comparison(comparison)[method], - SemanticColor::Default, - ColumnFormatter::Time, - ), - ColumnStartWith::ComparisonSegmentTime => ( - analysis::comparison_combined_segment_time( - timer.run(), - segment_index, - comparison, - method, + let ((column_value, semantic_color, formatter), is_live) = update_value.unwrap_or_else(|| { + ( + match column.start_with { + ColumnStartWith::Empty => (None, SemanticColor::Default, ColumnFormatter::Time), + ColumnStartWith::ComparisonTime => ( + segment.comparison(comparison)[method], + SemanticColor::Default, + ColumnFormatter::Time, ), - SemanticColor::Default, - ColumnFormatter::Time, - ), - ColumnStartWith::PossibleTimeSave => ( - possible_time_save::calculate(timer, segment_index, comparison, false), - SemanticColor::Default, - ColumnFormatter::PossibleTimeSave, - ), - }); + ColumnStartWith::ComparisonSegmentTime => ( + analysis::comparison_combined_segment_time( + timer.run(), + segment_index, + comparison, + method, + ), + SemanticColor::Default, + ColumnFormatter::Time, + ), + ColumnStartWith::PossibleTimeSave => ( + possible_time_save::calculate(timer, segment_index, comparison, false), + SemanticColor::Default, + ColumnFormatter::PossibleTimeSave, + ), + }, + false, + ) + }); let is_empty = column.start_with == ColumnStartWith::Empty && !updated; + state.updates_frequently = is_live && column_value.is_some(); + state.value.clear(); if !is_empty { let _ = match formatter { @@ -218,7 +227,7 @@ fn column_update_value( current_split: Option, method: TimingMethod, comparison: &str, -) -> Option<(Option, SemanticColor, ColumnFormatter)> { +) -> Option<((Option, SemanticColor, ColumnFormatter), bool)> { use self::{ColumnUpdateTrigger::*, ColumnUpdateWith::*}; if current_split < Some(segment_index) { @@ -250,19 +259,19 @@ fn column_update_value( let is_live = is_current_split; - match (column.update_with, is_live) { - (DontUpdate, _) => None, + let value = match (column.update_with, is_live) { + (DontUpdate, _) => return None, - (SplitTime, false) => Some(( + (SplitTime, false) => ( segment.split_time()[method], SemanticColor::Default, ColumnFormatter::Time, - )), - (SplitTime, true) => Some(( + ), + (SplitTime, true) => ( timer.current_time()[method], SemanticColor::Default, ColumnFormatter::Time, - )), + ), (Delta, false) | (DeltaWithFallback, false) => { let split_time = segment.split_time()[method]; @@ -275,31 +284,31 @@ fn column_update_value( } else { (delta, ColumnFormatter::Delta) }; - Some(( + ( value, split_color(timer, delta, segment_index, true, true, comparison, method), formatter, - )) + ) } - (Delta, true) | (DeltaWithFallback, true) => Some(( + (Delta, true) | (DeltaWithFallback, true) => ( catch! { timer.current_time()[method]? - segment.comparison(comparison)[method]? }, SemanticColor::Default, ColumnFormatter::Delta, - )), + ), - (SegmentTime, false) => Some(( + (SegmentTime, false) => ( analysis::previous_segment_time(timer, segment_index, method), SemanticColor::Default, ColumnFormatter::Time, - )), - (SegmentTime, true) => Some(( + ), + (SegmentTime, true) => ( analysis::live_segment_time(timer, segment_index, method), SemanticColor::Default, ColumnFormatter::Time, - )), + ), (SegmentDelta, false) | (SegmentDeltaWithFallback, false) => { let delta = analysis::previous_segment_delta(timer, segment_index, comparison, method); @@ -311,18 +320,20 @@ fn column_update_value( } else { (delta, ColumnFormatter::Delta) }; - Some(( + ( value, split_color(timer, delta, segment_index, false, true, comparison, method), formatter, - )) + ) } - (SegmentDelta, true) | (SegmentDeltaWithFallback, true) => Some(( + (SegmentDelta, true) | (SegmentDeltaWithFallback, true) => ( analysis::live_segment_delta(timer, segment_index, comparison, method), SemanticColor::Default, ColumnFormatter::Delta, - )), - } + ), + }; + + Some((value, is_live)) } impl ColumnUpdateWith { diff --git a/src/component/splits/mod.rs b/src/component/splits/mod.rs index 58f71618..41d955c7 100644 --- a/src/component/splits/mod.rs +++ b/src/component/splits/mod.rs @@ -5,9 +5,9 @@ //! list provides scrolling functionality, so not every segment needs to be //! shown all the time. -use crate::platform::prelude::*; use crate::{ clear_vec::{Clear, ClearVec}, + platform::prelude::*, settings::{ CachedImageId, Color, Field, Gradient, ImageData, ListGradient, SettingsDescription, Value, }, @@ -380,6 +380,7 @@ impl Component { value: String::new(), semantic_color: Default::default(), visual_color: Color::transparent(), + updates_frequently: false, }), column, timer, diff --git a/src/component/sum_of_best.rs b/src/component/sum_of_best.rs index 0593db00..a132911f 100644 --- a/src/component/sum_of_best.rs +++ b/src/component/sum_of_best.rs @@ -114,6 +114,7 @@ impl Component { state.key_abbreviations.push("SoB".into()); state.display_two_rows = self.settings.display_two_rows; + state.updates_frequently = false; } /// Calculates the component's state based on the timer provided. diff --git a/src/component/timer.rs b/src/component/timer.rs index 6726cff2..145a6971 100644 --- a/src/component/timer.rs +++ b/src/component/timer.rs @@ -87,6 +87,9 @@ pub struct State { pub bottom_color: Color, /// The height of the timer. pub height: u32, + /// This value indicates whether the timer is currently frequently being + /// updated. This can be used for rendering optimizations. + pub updates_frequently: bool, } #[cfg(feature = "std")] @@ -143,8 +146,10 @@ impl Component { .timing_method .unwrap_or_else(|| timer.current_timing_method()); + let phase = timer.current_phase(); + let (time, semantic_color) = if self.settings.is_segment_timer { - let last_split_index = if timer.current_phase() == TimerPhase::Ended { + let last_split_index = if phase == TimerPhase::Ended { timer.run().len() - 1 } else { timer.current_split_index().unwrap_or_default() @@ -162,7 +167,7 @@ impl Component { let time = time[method].or(time.real_time).unwrap_or_default(); let current_comparison = timer.current_comparison(); - let semantic_color = match timer.current_phase() { + let semantic_color = match phase { TimerPhase::Running if time >= TimeSpan::zero() => { let pb_split_time = timer .current_split() @@ -233,6 +238,7 @@ impl Component { formatter::Fraction::with_accuracy(self.settings.accuracy).format(time), ); + state.updates_frequently = phase.is_running() && time.is_some(); state.semantic_color = semantic_color; state.top_color = top_color; state.bottom_color = bottom_color; diff --git a/src/component/total_playtime.rs b/src/component/total_playtime.rs index 14a4e84d..e83a0944 100644 --- a/src/component/total_playtime.rs +++ b/src/component/total_playtime.rs @@ -3,11 +3,13 @@ //! current category has been played for. use super::key_value; -use crate::analysis::total_playtime; -use crate::platform::prelude::*; -use crate::settings::{Color, Field, Gradient, SettingsDescription, Value}; -use crate::timing::formatter::{Days, Regular, TimeFormatter}; -use crate::Timer; +use crate::{ + analysis::total_playtime, + platform::prelude::*, + settings::{Color, Field, Gradient, SettingsDescription, Value}, + timing::formatter::{Days, Regular, TimeFormatter}, + Timer, +}; use core::fmt::Write; use serde::{Deserialize, Serialize}; @@ -99,6 +101,7 @@ impl Component { state.key_abbreviations.push("Playtime".into()); state.display_two_rows = self.settings.display_two_rows; + state.updates_frequently = timer.current_phase().is_running(); } /// Calculates the component's state based on the timer provided. diff --git a/src/layout/editor/mod.rs b/src/layout/editor/mod.rs index fbb0507d..381e5346 100644 --- a/src/layout/editor/mod.rs +++ b/src/layout/editor/mod.rs @@ -5,8 +5,7 @@ //! Interface. use super::{Component, Layout, LayoutState}; -use crate::settings::Value; -use crate::timing::Snapshot; +use crate::{settings::Value, timing::Snapshot}; use core::result::Result as StdResult; mod state; @@ -64,6 +63,13 @@ impl Editor { self.layout.state(timer) } + /// Updates the layout's state based on the timer provided. You can use this + /// to visualize all of the components of a layout, while it is still being + /// edited by the Layout Editor. + pub fn update_layout_state(&mut self, state: &mut LayoutState, timer: &Snapshot<'_>) { + self.layout.update_state(state, timer) + } + /// Selects the component with the given index in order to modify its /// settings. Only a single component is selected at any given time. You may /// not provide an invalid index. diff --git a/src/rendering/component/blank_space.rs b/src/rendering/component/blank_space.rs index af4d6538..0c820e90 100644 --- a/src/rendering/component/blank_space.rs +++ b/src/rendering/component/blank_space.rs @@ -1,10 +1,10 @@ use crate::{ component::blank_space::State, - rendering::{Backend, RenderContext}, + rendering::{resource::ResourceAllocator, RenderContext}, }; pub(in crate::rendering) fn render( - context: &mut RenderContext<'_, impl Backend>, + context: &mut RenderContext<'_, impl ResourceAllocator>, dim: [f32; 2], component: &State, ) { diff --git a/src/rendering/component/detailed_timer.rs b/src/rendering/component/detailed_timer.rs index 2a54637e..35bead6e 100644 --- a/src/rendering/component/detailed_timer.rs +++ b/src/rendering/component/detailed_timer.rs @@ -2,12 +2,16 @@ use crate::{ component::detailed_timer::State, layout::LayoutState, rendering::{ - component::timer, icon::Icon, solid, vertical_padding, Backend, RenderContext, - BOTH_PADDINGS, PADDING, + component::timer, + consts::{vertical_padding, BOTH_PADDINGS, PADDING}, + icon::Icon, + resource::ResourceAllocator, + scene::Layer, + solid, RenderContext, }, }; -pub(in crate::rendering) fn render( +pub(in crate::rendering) fn render( context: &mut RenderContext<'_, B>, [width, height]: [f32; 2], component: &State, @@ -22,9 +26,6 @@ pub(in crate::rendering) fn render( let icon_size = height - 2.0 * vertical_padding; if let Some(icon) = &component.icon_change { - if let Some(old_icon) = detailed_timer_icon.take() { - context.backend.free_image(old_icon.image); - } *detailed_timer_icon = context.create_icon(&icon); } @@ -103,6 +104,7 @@ pub(in crate::rendering) fn render( if let Some(comparison) = &component.comparison2 { context.render_numbers( &comparison.time, + Layer::Bottom, [time_x, comparison2_y], comparison_text_scale, text_color, @@ -111,6 +113,7 @@ pub(in crate::rendering) fn render( if let Some(comparison) = &component.comparison1 { context.render_numbers( &comparison.time, + Layer::Bottom, [time_x, comparison1_y], comparison_text_scale, text_color, diff --git a/src/rendering/component/graph.rs b/src/rendering/component/graph.rs index 3d788b25..4a0f05f6 100644 --- a/src/rendering/component/graph.rs +++ b/src/rendering/component/graph.rs @@ -1,12 +1,12 @@ use crate::{ component::graph::State, layout::LayoutState, - rendering::{Backend, PathBuilder, RenderContext}, + rendering::{PathBuilder, RenderContext, ResourceAllocator}, settings::Gradient, }; pub(in crate::rendering) fn render( - context: &mut RenderContext<'_, impl Backend>, + context: &mut RenderContext<'_, impl ResourceAllocator>, [width, height]: [f32; 2], component: &State, _layout_state: &LayoutState, @@ -19,19 +19,19 @@ pub(in crate::rendering) fn render( const LINE_WIDTH: f32 = 0.025; const CIRCLE_RADIUS: f32 = 0.035; - context.render_rectangle( + context.render_top_rectangle( [0.0, 0.0], [width, component.middle], &Gradient::Plain(component.top_background_color), ); - context.render_rectangle( + context.render_top_rectangle( [0.0, component.middle], [width, 1.0], &Gradient::Plain(component.bottom_background_color), ); for &y in &component.horizontal_grid_lines { - context.render_rectangle( + context.render_top_rectangle( [0.0, y - GRID_LINE_WIDTH], [width, y + GRID_LINE_WIDTH], &Gradient::Plain(component.grid_lines_color), @@ -39,7 +39,7 @@ pub(in crate::rendering) fn render( } for &x in &component.vertical_grid_lines { - context.render_rectangle( + context.render_top_rectangle( [width * x - GRID_LINE_WIDTH, 0.0], [width * x + GRID_LINE_WIDTH, 1.0], &Gradient::Plain(component.grid_lines_color), @@ -50,34 +50,32 @@ pub(in crate::rendering) fn render( let p1 = &component.points[component.points.len() - 2]; let p2 = &component.points[component.points.len() - 1]; - let mut builder = context.backend.path_builder(); + let mut builder = context.handles.path_builder(); builder.move_to(width * p1.x, component.middle); builder.line_to(width * p1.x, p1.y); builder.line_to(width * p2.x, p2.y); builder.line_to(width * p2.x, component.middle); builder.close(); - let partial_fill_path = builder.finish(context.backend); - context.render_path(&partial_fill_path, component.partial_fill_color); - context.free_path(partial_fill_path); + let partial_fill_path = builder.finish(&mut context.handles); + context.top_layer_path(partial_fill_path, component.partial_fill_color); component.points.len() - 1 } else { component.points.len() }; - let mut builder = context.backend.path_builder(); + let mut builder = context.handles.path_builder(); builder.move_to(0.0, component.middle); for p in &component.points[..len] { builder.line_to(width * p.x, p.y); } builder.line_to(width * component.points[len - 1].x, component.middle); builder.close(); - let fill_path = builder.finish(context.backend); - context.render_path(&fill_path, component.complete_fill_color); - context.free_path(fill_path); + let fill_path = builder.finish(&mut context.handles); + context.top_layer_path(fill_path, component.complete_fill_color); for points in component.points.windows(2) { - let mut builder = context.backend.path_builder(); + let mut builder = context.handles.path_builder(); builder.move_to(width * points[0].x, points[0].y); builder.line_to(width * points[1].x, points[1].y); @@ -87,9 +85,8 @@ pub(in crate::rendering) fn render( component.graph_lines_color }; - let line_path = builder.finish(context.backend); - context.render_stroke_path(&line_path, color, LINE_WIDTH); - context.free_path(line_path); + let line_path = builder.finish(&mut context.handles); + context.top_layer_stroke_path(line_path, color, LINE_WIDTH); } for (i, point) in component.points.iter().enumerate().skip(1) { @@ -101,10 +98,9 @@ pub(in crate::rendering) fn render( }; let circle_path = context - .backend + .handles .build_circle(width * point.x, point.y, CIRCLE_RADIUS); - context.render_path(&circle_path, color); - context.free_path(circle_path); + context.top_layer_path(circle_path, color); } } diff --git a/src/rendering/component/key_value.rs b/src/rendering/component/key_value.rs index b21ac1db..7bada161 100644 --- a/src/rendering/component/key_value.rs +++ b/src/rendering/component/key_value.rs @@ -1,11 +1,11 @@ use crate::{ component::key_value::State, layout::{LayoutDirection, LayoutState}, - rendering::{Backend, RenderContext}, + rendering::{resource::ResourceAllocator, RenderContext}, }; pub(in crate::rendering) fn render( - context: &mut RenderContext<'_, impl Backend>, + context: &mut RenderContext<'_, impl ResourceAllocator>, dim: [f32; 2], component: &State, layout_state: &LayoutState, @@ -15,6 +15,7 @@ pub(in crate::rendering) fn render( &component.key, &component.key_abbreviations, &component.value, + component.updates_frequently, dim, component.key_color.unwrap_or(layout_state.text_color), component.value_color.unwrap_or(layout_state.text_color), diff --git a/src/rendering/component/mod.rs b/src/rendering/component/mod.rs index b8574af8..ca69fda9 100644 --- a/src/rendering/component/mod.rs +++ b/src/rendering/component/mod.rs @@ -1,3 +1,11 @@ +use crate::layout::{ComponentState, LayoutState}; + +use super::{ + consts::{DEFAULT_COMPONENT_HEIGHT, PSEUDO_PIXELS, SEPARATOR_THICKNESS, TWO_ROW_HEIGHT}, + resource::ResourceAllocator, + IconCache, RenderContext, +}; + pub mod blank_space; pub mod detailed_timer; pub mod graph; @@ -7,3 +15,99 @@ pub mod splits; pub mod text; pub mod timer; pub mod title; + +pub fn layout_width(layout: &LayoutState) -> f32 { + layout.components.iter().map(width).sum() +} + +pub fn layout_height(layout: &LayoutState) -> f32 { + layout.components.iter().map(height).sum() +} + +pub fn width(component: &ComponentState) -> f32 { + match component { + ComponentState::BlankSpace(state) => state.size as f32 * PSEUDO_PIXELS, + ComponentState::DetailedTimer(_) => 7.0, + ComponentState::Graph(_) => 7.0, + ComponentState::KeyValue(_) => 6.0, + ComponentState::Separator(_) => SEPARATOR_THICKNESS, + ComponentState::Splits(state) => { + let column_count = 2.0; // FIXME: Not always 2. + let split_width = 2.0 + column_count * splits::COLUMN_WIDTH; + state.splits.len() as f32 * split_width + } + ComponentState::Text(_) => 6.0, + ComponentState::Timer(_) => 8.25, + ComponentState::Title(_) => 8.0, + } +} + +pub fn height(component: &ComponentState) -> f32 { + match component { + ComponentState::BlankSpace(state) => state.size as f32 * PSEUDO_PIXELS, + ComponentState::DetailedTimer(_) => 2.5, + ComponentState::Graph(state) => state.height as f32 * PSEUDO_PIXELS, + ComponentState::KeyValue(state) => { + if state.display_two_rows { + TWO_ROW_HEIGHT + } else { + DEFAULT_COMPONENT_HEIGHT + } + } + ComponentState::Separator(_) => SEPARATOR_THICKNESS, + ComponentState::Splits(state) => { + state.splits.len() as f32 + * if state.display_two_rows { + TWO_ROW_HEIGHT + } else { + DEFAULT_COMPONENT_HEIGHT + } + + if state.column_labels.is_some() { + DEFAULT_COMPONENT_HEIGHT + } else { + 0.0 + } + } + ComponentState::Text(state) => { + if state.display_two_rows { + TWO_ROW_HEIGHT + } else { + DEFAULT_COMPONENT_HEIGHT + } + } + ComponentState::Timer(state) => state.height as f32 * PSEUDO_PIXELS, + ComponentState::Title(_) => TWO_ROW_HEIGHT, + } +} + +pub(super) fn render( + context: &mut RenderContext<'_, A>, + icons: &mut IconCache, + component: &ComponentState, + state: &LayoutState, + dim: [f32; 2], +) { + match component { + ComponentState::BlankSpace(state) => blank_space::render(context, dim, state), + ComponentState::DetailedTimer(component) => detailed_timer::render( + context, + dim, + component, + state, + &mut icons.detailed_timer_icon, + ), + ComponentState::Graph(component) => graph::render(context, dim, component, state), + ComponentState::KeyValue(component) => key_value::render(context, dim, component, state), + ComponentState::Separator(component) => separator::render(context, dim, component, state), + ComponentState::Splits(component) => { + splits::render(context, dim, component, state, &mut icons.split_icons) + } + ComponentState::Text(component) => text::render(context, dim, component, state), + ComponentState::Timer(component) => { + timer::render(context, dim, component); + } + ComponentState::Title(component) => { + title::render(context, dim, component, state, &mut icons.game_icon) + } + } +} diff --git a/src/rendering/component/separator.rs b/src/rendering/component/separator.rs index 782a1758..aef82f3c 100644 --- a/src/rendering/component/separator.rs +++ b/src/rendering/component/separator.rs @@ -1,12 +1,12 @@ use crate::{ component::separator::State, layout::LayoutState, - rendering::{Backend, RenderContext}, + rendering::{resource::ResourceAllocator, RenderContext}, settings::Gradient, }; pub(in crate::rendering) fn render( - context: &mut RenderContext<'_, impl Backend>, + context: &mut RenderContext<'_, impl ResourceAllocator>, dim: [f32; 2], _component: &State, layout_state: &LayoutState, diff --git a/src/rendering/component/splits.rs b/src/rendering/component/splits.rs index 90c4ed3c..5a3988ba 100644 --- a/src/rendering/component/splits.rs +++ b/src/rendering/component/splits.rs @@ -2,16 +2,21 @@ use crate::{ component::splits::State, layout::{LayoutDirection, LayoutState}, rendering::{ - icon::Icon, solid, vertical_padding, Backend, RenderContext, BOTH_PADDINGS, - DEFAULT_COMPONENT_HEIGHT, DEFAULT_TEXT_SIZE, PADDING, TEXT_ALIGN_BOTTOM, TEXT_ALIGN_TOP, - THIN_SEPARATOR_THICKNESS, TWO_ROW_HEIGHT, + consts::{ + vertical_padding, BOTH_PADDINGS, DEFAULT_COMPONENT_HEIGHT, DEFAULT_TEXT_SIZE, PADDING, + TEXT_ALIGN_BOTTOM, TEXT_ALIGN_TOP, THIN_SEPARATOR_THICKNESS, TWO_ROW_HEIGHT, + }, + icon::Icon, + resource::ResourceAllocator, + scene::Layer, + solid, RenderContext, }, settings::{Gradient, ListGradient}, }; pub const COLUMN_WIDTH: f32 = 2.75; -pub(in crate::rendering) fn render( +pub(in crate::rendering) fn render( context: &mut RenderContext<'_, B>, [width, height]: [f32; 2], component: &State, @@ -65,11 +70,7 @@ pub(in crate::rendering) fn render( if icon_change.segment_index >= split_icons.len() { split_icons.extend((0..=icon_change.segment_index - split_icons.len()).map(|_| None)); } - let icon = &mut split_icons[icon_change.segment_index]; - if let Some(old_icon) = icon.take() { - context.backend.free_image(old_icon.image); - } - *icon = context.create_icon(&icon_change.icon); + split_icons[icon_change.segment_index] = context.create_icon(&icon_change.icon); } if let Some(column_labels) = &component.column_labels { @@ -79,6 +80,7 @@ pub(in crate::rendering) fn render( let left_x = right_x - COLUMN_WIDTH; context.render_text_right_align( label, + Layer::Bottom, [right_x, TEXT_ALIGN_TOP], DEFAULT_TEXT_SIZE, text_color, @@ -133,6 +135,7 @@ pub(in crate::rendering) fn render( if !column.value.is_empty() { left_x = context.render_numbers( &column.value, + Layer::from_updates_frequently(column.updates_frequently), [right_x, split_height + TEXT_ALIGN_BOTTOM], DEFAULT_TEXT_SIZE, solid(&column.visual_color), diff --git a/src/rendering/component/text.rs b/src/rendering/component/text.rs index 160184aa..5dcfeac8 100644 --- a/src/rendering/component/text.rs +++ b/src/rendering/component/text.rs @@ -1,11 +1,15 @@ use crate::{ component::text::{State, TextState}, layout::{LayoutDirection, LayoutState}, - rendering::{solid, Backend, RenderContext, DEFAULT_TEXT_SIZE, PADDING, TEXT_ALIGN_TOP}, + rendering::{ + consts::{DEFAULT_TEXT_SIZE, PADDING, TEXT_ALIGN_TOP}, + resource::ResourceAllocator, + solid, RenderContext, + }, }; pub(in crate::rendering) fn render( - context: &mut RenderContext<'_, impl Backend>, + context: &mut RenderContext<'_, impl ResourceAllocator>, [width, height]: [f32; 2], component: &State, layout_state: &LayoutState, @@ -28,6 +32,7 @@ pub(in crate::rendering) fn render( &left, &[], &right, + false, [width, height], component .left_center_color diff --git a/src/rendering/component/timer.rs b/src/rendering/component/timer.rs index e370523f..549a2960 100644 --- a/src/rendering/component/timer.rs +++ b/src/rendering/component/timer.rs @@ -1,10 +1,12 @@ use crate::{ component::timer::State, - rendering::{Backend, FillShader, RenderContext, PADDING}, + rendering::{ + consts::PADDING, resource::ResourceAllocator, scene::Layer, FillShader, RenderContext, + }, }; pub(in crate::rendering) fn render( - context: &mut RenderContext<'_, impl Backend>, + context: &mut RenderContext<'_, impl ResourceAllocator>, [width, height]: [f32; 2], component: &State, ) -> f32 { @@ -13,11 +15,19 @@ pub(in crate::rendering) fn render( component.top_color.to_array(), component.bottom_color.to_array(), ); + let render_target = Layer::from_updates_frequently(component.updates_frequently); let x = context.render_timer( &component.fraction, + render_target, [width - PADDING, 0.85 * height], 0.8 * height, shader, ); - context.render_timer(&component.time, [x, 0.85 * height], 1.2 * height, shader) + context.render_timer( + &component.time, + render_target, + [x, 0.85 * height], + 1.2 * height, + shader, + ) } diff --git a/src/rendering/component/title.rs b/src/rendering/component/title.rs index 474a3e4b..5a55b767 100644 --- a/src/rendering/component/title.rs +++ b/src/rendering/component/title.rs @@ -2,12 +2,17 @@ use crate::{ component::title::State, layout::LayoutState, rendering::{ - icon::Icon, solid, vertical_padding, Backend, RenderContext, BOTH_PADDINGS, - DEFAULT_TEXT_SIZE, PADDING, TEXT_ALIGN_BOTTOM, TEXT_ALIGN_CENTER, TEXT_ALIGN_TOP, + consts::{ + vertical_padding, BOTH_PADDINGS, DEFAULT_TEXT_SIZE, PADDING, TEXT_ALIGN_BOTTOM, + TEXT_ALIGN_CENTER, TEXT_ALIGN_TOP, + }, + icon::Icon, + resource::ResourceAllocator, + solid, Layer, RenderContext, }, }; -pub(in crate::rendering) fn render( +pub(in crate::rendering) fn render( context: &mut RenderContext<'_, B>, [width, height]: [f32; 2], component: &State, @@ -19,9 +24,6 @@ pub(in crate::rendering) fn render( let text_color = solid(&text_color); if let Some(icon) = &component.icon_change { - if let Some(old_icon) = game_icon.take() { - context.backend.free_image(old_icon.image); - } *game_icon = context.create_icon(icon); } @@ -47,6 +49,7 @@ pub(in crate::rendering) fn render( }; let line2_end_x = context.render_numbers( &attempts, + Layer::Bottom, [width - PADDING, height + TEXT_ALIGN_BOTTOM], DEFAULT_TEXT_SIZE, text_color, diff --git a/src/rendering/consts.rs b/src/rendering/consts.rs new file mode 100644 index 00000000..851fef4a --- /dev/null +++ b/src/rendering/consts.rs @@ -0,0 +1,21 @@ +pub const PADDING: f32 = 0.35; +pub const BOTH_PADDINGS: f32 = 2.0 * PADDING; +pub const BOTH_VERTICAL_PADDINGS: f32 = DEFAULT_COMPONENT_HEIGHT - DEFAULT_TEXT_SIZE; +pub const VERTICAL_PADDING: f32 = BOTH_VERTICAL_PADDINGS / 2.0; +pub const ICON_MIN_VERTICAL_PADDING: f32 = 0.1; +pub const DEFAULT_COMPONENT_HEIGHT: f32 = 1.0; +pub const TWO_ROW_HEIGHT: f32 = 2.0 * DEFAULT_TEXT_SIZE + BOTH_VERTICAL_PADDINGS; +pub const DEFAULT_TEXT_SIZE: f32 = 0.725; +pub const DEFAULT_TEXT_ASCENT: f32 = 0.55; +pub const DEFAULT_TEXT_DESCENT: f32 = DEFAULT_TEXT_SIZE - DEFAULT_TEXT_ASCENT; +pub const TEXT_ALIGN_TOP: f32 = VERTICAL_PADDING + DEFAULT_TEXT_ASCENT; +pub const TEXT_ALIGN_BOTTOM: f32 = -(VERTICAL_PADDING + DEFAULT_TEXT_DESCENT); +pub const TEXT_ALIGN_CENTER: f32 = DEFAULT_TEXT_ASCENT - DEFAULT_TEXT_SIZE / 2.0; +pub const SEPARATOR_THICKNESS: f32 = 0.1; +pub const THIN_SEPARATOR_THICKNESS: f32 = SEPARATOR_THICKNESS / 2.0; +pub const PSEUDO_PIXELS: f32 = 1.0 / 24.0; +pub const DEFAULT_VERTICAL_WIDTH: f32 = 11.5; + +pub fn vertical_padding(height: f32) -> f32 { + (ICON_MIN_VERTICAL_PADDING * height).min(PADDING) +} diff --git a/src/rendering/entity.rs b/src/rendering/entity.rs new file mode 100644 index 00000000..086a9e67 --- /dev/null +++ b/src/rendering/entity.rs @@ -0,0 +1,82 @@ +use std::{ + hash::{Hash, Hasher}, + mem, +}; + +use ahash::AHasher; + +use super::{resource::Handle, FillShader, Rgba, Transform}; + +/// An entity describes an element positioned on a [`Scene's`](super::Scene) +/// [`Layer`](super::Layer) that is meant to be visualized. +pub enum Entity { + /// A path where the inside is filled with the [`FillShader`]. For + /// determining what's inside the [non-zero fill + /// rule](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill-rule#nonzero) + /// is supposed to be used. + FillPath(Handle

, FillShader, Transform), + /// A path where only the path lines themselves are supposed to be drawn. + /// There is no notion of an inside region. The floating point number + /// determines the thickness of the path. + StrokePath(Handle

, f32, Rgba, Transform), + /// An image. + Image(Handle, Transform), +} + +pub fn calculate_hash(background: &Option, entities: &[Entity]) -> u64 { + let mut hasher = AHasher::new_with_keys(1234, 5678); + mem::discriminant(background).hash(&mut hasher); + if let Some(background) = background { + hash_shader(background, &mut hasher); + } + entities.hash(&mut hasher); + hasher.finish() +} + +fn hash_float(x: f32, state: &mut impl Hasher) { + u32::from_ne_bytes(x.to_ne_bytes()).hash(state); +} + +fn hash_floats(f: &[f32], state: &mut impl Hasher) { + for &f in f { + hash_float(f, state); + } +} + +fn hash_shader(shader: &FillShader, state: &mut impl Hasher) { + mem::discriminant(shader).hash(state); + match shader { + FillShader::SolidColor(c) => hash_floats(c, state), + FillShader::VerticalGradient(t, b) => { + hash_floats(t, state); + hash_floats(b, state); + } + FillShader::HorizontalGradient(l, r) => { + hash_floats(l, state); + hash_floats(r, state); + } + } +} + +impl Hash for Entity { + fn hash(&self, state: &mut H) { + mem::discriminant(self).hash(state); + match self { + Entity::FillPath(path, shader, transform) => { + path.hash(state); + hash_shader(shader, state); + hash_floats(&transform.to_array(), state); + } + Entity::StrokePath(path, stroke_width, color, transform) => { + path.hash(state); + hash_float(*stroke_width, state); + hash_floats(color, state); + hash_floats(&transform.to_array(), state); + } + Entity::Image(image, transform) => { + image.hash(state); + hash_floats(&transform.to_array(), state); + } + } + } +} diff --git a/src/rendering/font/cache.rs b/src/rendering/font/cache.rs new file mode 100644 index 00000000..23201258 --- /dev/null +++ b/src/rendering/font/cache.rs @@ -0,0 +1,110 @@ +use rustybuzz::UnicodeBuffer; + +use super::{Font, GlyphCache, SharedOwnership, TEXT_FONT, TIMER_FONT}; +use crate::settings::{FontStretch, FontStyle, FontWeight}; + +pub struct CachedFont

{ + #[cfg(feature = "font-loading")] + setting: Option, + pub font: Font<'static>, + pub glyph_cache: GlyphCache

, +} + +impl CachedFont

{ + fn new(font: Font<'static>) -> Self { + Self { + #[cfg(feature = "font-loading")] + setting: None, + font, + glyph_cache: GlyphCache::new(), + } + } + + #[cfg(feature = "font-loading")] + fn maybe_reload( + &mut self, + font_to_use: &Option, + default_font: impl FnOnce() -> Font<'static>, + ) { + if &self.setting != font_to_use { + self.font = font_to_use + .as_ref() + .and_then(Font::load) + .unwrap_or_else(default_font); + self.glyph_cache.clear(); + self.setting.clone_from(font_to_use); + } + } +} + +pub struct FontCache

{ + pub buffer: Option, + pub timer: CachedFont

, + pub times: CachedFont

, + pub text: CachedFont

, +} + +impl FontCache

{ + pub fn new() -> Option { + Some(Self { + buffer: None, + timer: CachedFont::new(Font::from_slice( + TIMER_FONT, + 0, + FontStyle::Normal, + FontWeight::Bold, + FontStretch::Normal, + )?), + times: CachedFont::new(Font::from_slice( + TEXT_FONT, + 0, + FontStyle::Normal, + FontWeight::Bold, + FontStretch::Normal, + )?), + text: CachedFont::new(Font::from_slice( + TEXT_FONT, + 0, + FontStyle::Normal, + FontWeight::Normal, + FontStretch::Normal, + )?), + }) + } + + #[cfg(feature = "font-loading")] + pub fn maybe_reload(&mut self, state: &crate::layout::LayoutState) { + self.timer.maybe_reload(&state.timer_font, || { + Font::from_slice( + TIMER_FONT, + 0, + FontStyle::Normal, + FontWeight::Bold, + FontStretch::Normal, + ) + .unwrap() + }); + + self.times.maybe_reload(&state.times_font, || { + Font::from_slice( + TEXT_FONT, + 0, + FontStyle::Normal, + FontWeight::Bold, + FontStretch::Normal, + ) + .unwrap() + }); + + self.text.maybe_reload(&state.text_font, || { + Font::from_slice( + TEXT_FONT, + 0, + FontStyle::Normal, + FontWeight::Normal, + FontStretch::Normal, + ) + .unwrap() + }); + } +} diff --git a/src/rendering/font/glyph_cache.rs b/src/rendering/font/glyph_cache.rs index ffa05451..c96a23e2 100644 --- a/src/rendering/font/glyph_cache.rs +++ b/src/rendering/font/glyph_cache.rs @@ -1,14 +1,17 @@ use std::marker::PhantomData; -use crate::settings::Color; +use crate::{ + rendering::resource::{Handle, Handles, PathBuilder, ResourceAllocator, SharedOwnership}, + settings::Color, +}; -use super::{color_font::iter_colored_glyphs, Backend, Font}; +use super::{color_font::iter_colored_glyphs, Font}; use hashbrown::HashMap; use ttf_parser::OutlineBuilder; -struct PathBuilder(PB, PhantomData); +struct GlyphBuilder(PB, PhantomData); -impl> OutlineBuilder for PathBuilder { +impl> OutlineBuilder for GlyphBuilder { fn move_to(&mut self, x: f32, y: f32) { self.0.move_to(x, -y); } @@ -27,7 +30,7 @@ impl> OutlineBuilder for PathBuilder } pub struct GlyphCache

{ - glyphs: HashMap, P)>>, + glyphs: HashMap, Handle

)>>, } impl

Default for GlyphCache

{ @@ -38,32 +41,28 @@ impl

Default for GlyphCache

{ } } -impl

GlyphCache

{ +impl GlyphCache

{ pub fn new() -> Self { Default::default() } #[cfg(feature = "font-loading")] - pub fn clear(&mut self, backend: &mut impl Backend) { - for (_, glyph) in self.glyphs.drain() { - for (_, layer) in glyph { - backend.free_path(layer); - } - } + pub fn clear(&mut self) { + self.glyphs.clear(); } pub fn lookup_or_insert( &mut self, font: &Font<'_>, glyph: u32, - backend: &mut impl Backend, - ) -> &[(Option, P)] { + handles: &mut Handles>, + ) -> &[(Option, Handle

)] { self.glyphs.entry(glyph).or_insert_with(|| { let mut glyphs = Vec::new(); iter_colored_glyphs(&font.color_tables, 0, glyph as _, |glyph, color| { - let mut builder = PathBuilder(backend.path_builder(), PhantomData); + let mut builder = GlyphBuilder(handles.path_builder(), PhantomData); font.outline_glyph(glyph, &mut builder); - let path = super::super::PathBuilder::finish(builder.0, backend); + let path = builder.0.finish(handles); glyphs.push((color, path)); }); glyphs diff --git a/src/rendering/font/mod.rs b/src/rendering/font/mod.rs index e1592ce1..4d7ec50d 100644 --- a/src/rendering/font/mod.rs +++ b/src/rendering/font/mod.rs @@ -1,13 +1,18 @@ +mod cache; mod color_font; mod glyph_cache; use self::color_font::ColorTables; -use super::{solid, Backend, FillShader, Pos, Transform}; +use super::{ + entity::Entity, + resource::{Handles, ResourceAllocator, SharedOwnership}, + solid, FillShader, Pos, Transform, +}; use crate::settings::{FontStretch, FontStyle, FontWeight}; use rustybuzz::{Face, Feature, GlyphBuffer, Tag, UnicodeBuffer, Variation}; use ttf_parser::{GlyphId, OutlineBuilder, Tag as ParserTag}; -pub use self::glyph_cache::GlyphCache; +pub use self::{cache::FontCache, glyph_cache::GlyphCache}; #[cfg(feature = "font-loading")] use { @@ -381,23 +386,28 @@ impl<'f> Glyphs<'f> { } } -pub fn render( +pub fn render( layout: impl IntoIterator, shader: FillShader, font: &ScaledFont<'_>, - glyph_cache: &mut GlyphCache, + glyph_cache: &mut GlyphCache, transform: &Transform, - backend: &mut B, + handles: &mut Handles, + entities: &mut Vec>, ) { for glyph in layout { - let layers = glyph_cache.lookup_or_insert(font.font, glyph.id, backend); + let layers = glyph_cache.lookup_or_insert(font.font, glyph.id, handles); let transform = transform .pre_translate([glyph.x, glyph.y].into()) .pre_scale(font.scale, font.scale); for (color, layer) in layers { - backend.render_fill_path(layer, color.as_ref().map_or(shader, solid), transform); + entities.push(Entity::FillPath( + layer.share(), + color.as_ref().map_or(shader, solid), + transform, + )); } } } diff --git a/src/rendering/icon.rs b/src/rendering/icon.rs index 9c69e027..8e0c5992 100644 --- a/src/rendering/icon.rs +++ b/src/rendering/icon.rs @@ -1,4 +1,6 @@ +use super::resource::Handle; + pub struct Icon { - pub image: T, + pub image: Handle, pub aspect_ratio: f32, } diff --git a/src/rendering/mod.rs b/src/rendering/mod.rs index 5724a6e9..65abb789 100644 --- a/src/rendering/mod.rs +++ b/src/rendering/mod.rs @@ -1,7 +1,12 @@ -//! The rendering module provides a renderer for layout states that is -//! abstracted over different backends. It focuses on rendering paths, i.e. -//! lines, quadratic and cubic bezier curves. A backend therefore needs to be -//! able to render paths. An optional software renderer is available behind the +//! The rendering module provides a [`SceneManager`] that, when provided with a +//! [`LayoutState`], places [`Entities`](Entity) into a [`Scene`] and updates it +//! accordingly as the [`LayoutState`] changes. It is up to a specific renderer +//! to then take the [`Scene`] and render out the [`Entities`](Entity). There is +//! a [`ResourceAllocator`] trait that defines the types of resources an +//! [`Entity`] consists of. A specific renderer can then provide an +//! implementation that provides the resources it can render out. Those +//! resources are images and paths, i.e. lines, quadratic and cubic bezier +//! curves. An optional software renderer is available behind the //! `software-rendering` feature that uses tiny-skia to efficiently render the //! paths on the CPU. It is surprisingly fast and can be considered the default //! renderer. @@ -66,24 +71,38 @@ // thin separators have half of this thickness. mod component; +mod consts; +mod entity; mod font; mod icon; +mod resource; +mod scene; #[cfg(feature = "software-rendering")] pub mod software; use self::{ - font::{Font, GlyphCache, TEXT_FONT, TIMER_FONT}, + consts::{ + BOTH_PADDINGS, DEFAULT_TEXT_SIZE, DEFAULT_VERTICAL_WIDTH, PADDING, TEXT_ALIGN_BOTTOM, + TEXT_ALIGN_TOP, TWO_ROW_HEIGHT, + }, + font::FontCache, icon::Icon, + resource::Handles, }; use crate::{ - layout::{ComponentState, LayoutDirection, LayoutState}, - settings::{Color, FontStretch, FontStyle, FontWeight, Gradient}, + layout::{LayoutDirection, LayoutState}, + settings::{Color, Gradient}, }; use alloc::borrow::Cow; use core::iter; use euclid::{Transform2D, UnknownUnit}; -use rustybuzz::UnicodeBuffer; + +pub use self::{ + entity::Entity, + resource::{Handle, PathBuilder, ResourceAllocator, SharedOwnership}, + scene::{Layer, Scene}, +}; pub use euclid; @@ -96,63 +115,8 @@ pub type Rgba = [f32; 4]; /// scene. pub type Transform = Transform2D; -const PADDING: f32 = 0.35; -const BOTH_PADDINGS: f32 = 2.0 * PADDING; -const BOTH_VERTICAL_PADDINGS: f32 = DEFAULT_COMPONENT_HEIGHT - DEFAULT_TEXT_SIZE; -const VERTICAL_PADDING: f32 = BOTH_VERTICAL_PADDINGS / 2.0; -const ICON_MIN_VERTICAL_PADDING: f32 = 0.1; -const DEFAULT_COMPONENT_HEIGHT: f32 = 1.0; -const TWO_ROW_HEIGHT: f32 = 2.0 * DEFAULT_TEXT_SIZE + BOTH_VERTICAL_PADDINGS; -const DEFAULT_TEXT_SIZE: f32 = 0.725; -const DEFAULT_TEXT_ASCENT: f32 = 0.55; -const DEFAULT_TEXT_DESCENT: f32 = DEFAULT_TEXT_SIZE - DEFAULT_TEXT_ASCENT; -const TEXT_ALIGN_TOP: f32 = VERTICAL_PADDING + DEFAULT_TEXT_ASCENT; -const TEXT_ALIGN_BOTTOM: f32 = -(VERTICAL_PADDING + DEFAULT_TEXT_DESCENT); -const TEXT_ALIGN_CENTER: f32 = DEFAULT_TEXT_ASCENT - DEFAULT_TEXT_SIZE / 2.0; -const SEPARATOR_THICKNESS: f32 = 0.1; -const THIN_SEPARATOR_THICKNESS: f32 = SEPARATOR_THICKNESS / 2.0; -const PSEUDO_PIXELS: f32 = 1.0 / 24.0; -const DEFAULT_VERTICAL_WIDTH: f32 = 11.5; - -fn vertical_padding(height: f32) -> f32 { - (ICON_MIN_VERTICAL_PADDING * height).min(PADDING) -} - -/// The backend provides a path builder that defines how to build paths that can -/// be used for the backend. -pub trait PathBuilder { - /// The type of the path to build. This needs to be identical to the type of - /// the path used by the backend. - type Path; - - /// Moves the cursor to a specific position and starts a new contour. - fn move_to(&mut self, x: f32, y: f32); - - /// Adds a line from the previous position to the position specified, while - /// also moving the cursor along. - fn line_to(&mut self, x: f32, y: f32); - - /// Adds a quadratic bézier curve from the previous position to the position - /// specified, while also moving the cursor along. (x1, y1) specifies the - /// control point. - fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32); - - /// Adds a cubic bézier curve from the previous position to the position - /// specified, while also moving the cursor along. (x1, y1) and (x2, y2) - /// specify the two control points. - fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32); - - /// Closes the current contour. The current position and the initial - /// position get connected by a line, forming a continuous loop. Nothing - /// if the path is empty or already closed. - fn close(&mut self); - - /// Finishes building the path. - fn finish(self, backend: &mut B) -> Self::Path; -} - /// Specifies the colors to use when filling a path. -#[derive(Copy, Clone)] +#[derive(Copy, Clone, PartialEq)] pub enum FillShader { /// Use a single color for the whole path. SolidColor(Rgba), @@ -162,103 +126,27 @@ pub enum FillShader { HorizontalGradient(Rgba, Rgba), } -/// The rendering backend for the Renderer is abstracted out into the Backend -/// trait such that the rendering isn't tied to a specific rendering framework. -pub trait Backend { - /// The type the backend uses for building paths. - type PathBuilder: PathBuilder; - /// The type the backend uses for paths. - type Path; - /// The type the backend uses for textures. - type Image; - - /// Creates a new path builder to build a new path. - fn path_builder(&mut self) -> Self::PathBuilder; - - /// Builds a new circle. A default implementation that approximates the - /// circle with 4 cubic bézier curves is provided. For more accuracy or - /// performance a backend can change the implementation. - fn build_circle(&mut self, x: f32, y: f32, r: f32) -> Self::Path { - // Based on https://spencermortensen.com/articles/bezier-circle/ - const C: f64 = 0.551915024494; - let c = (C * r as f64) as f32; - let mut builder = self.path_builder(); - builder.move_to(x, y - r); - builder.curve_to(x + c, y - r, x + r, y - c, x + r, y); - builder.curve_to(x + r, y + c, x + c, y + r, x, y + r); - builder.curve_to(x - c, y + r, x - r, y + c, x - r, y); - builder.curve_to(x - r, y - c, x - c, y - r, x, y - r); - builder.close(); - builder.finish(self) - } - - /// Instructs the backend to render a path with a fill shader. The rendered - /// elements are supposed to be drawn on top of each other in the order that - /// the backend is being instructed to render them. The colors are supposed - /// to be alpha blended and don't use sRGB. The transform represents a - /// transformation matrix to be applied to the path in order to place it in - /// the scene. - fn render_fill_path(&mut self, path: &Self::Path, shader: FillShader, transform: Transform); - - /// Instructs the backend to render a path with a stroke shader. The - /// rendered elements are supposed to be drawn on top of each other in the - /// order that the backend is being instructed to render them. The colors - /// are supposed to be alpha blended and don't use sRGB. The transform - /// represents a transformation matrix to be applied to the path in order to - /// place it in the scene. - fn render_stroke_path( - &mut self, - path: &Self::Path, - stroke_width: f32, - color: Rgba, - transform: Transform, - ); - - /// Instructs the backend to render out an image. An optional rectangle path - /// is provided that the backend can use in case it needs a path to render - /// out images. The rendered elements are supposed to be drawn on top of - /// each other in the order that the backend is being instructed to render - /// them. The colors are supposed to be alpha blended and don't use sRGB. - /// The transform represents a transformation matrix to be applied to the - /// image in order to place it in the scene. - fn render_image(&mut self, image: &Self::Image, rectangle: &Self::Path, transform: Transform); - - /// Instructs the backend to free a path as it is not needed anymore. - fn free_path(&mut self, path: Self::Path); - - /// Instructs the backend to create an image out of the image data provided. - /// The image's resolution is provided as well. The data is an array of - /// RGBA8 encoded pixels (red, green, blue, alpha with each channel being an - /// u8). - fn create_image(&mut self, width: u32, height: u32, data: &[u8]) -> Self::Image; - - /// Instructs the backend to free an image as it is not needed anymore. - fn free_image(&mut self, image: Self::Image); -} - enum CachedSize { Vertical(f32), Horizontal(f32), } -/// A renderer can be used to render out layout states with the backend chosen. -pub struct Renderer { - #[cfg(feature = "font-loading")] - timer_font_setting: Option, - timer_font: Font<'static>, - timer_glyph_cache: GlyphCache

, - #[cfg(feature = "font-loading")] - times_font_setting: Option, - times_font: Font<'static>, - times_glyph_cache: GlyphCache

, - #[cfg(feature = "font-loading")] - text_font_setting: Option, - text_font: Font<'static>, - text_glyph_cache: GlyphCache

, - rectangle: Option

, - cached_size: Option, +/// The scene manager is the main entry point when it comes to writing a +/// renderer for livesplit-core. When provided with a [`LayoutState`], it places +/// [`Entities`](Entity) into a [`Scene`] and updates it accordingly as the +/// [`LayoutState`] changes. It is up to a specific renderer to then take the +/// [`Scene`] and render out the [`Entities`](Entity). There is a +/// [`ResourceAllocator`] trait that defines the types of resources an +/// [`Entity`] consists of. A specific renderer can then provide an +/// implementation that provides the resources it can render out. Those +/// resources are images and paths, i.e. lines, quadratic and cubic bezier +/// curves. +pub struct SceneManager { + scene: Scene, + fonts: FontCache

, icons: IconCache, - text_buffer: Option, + next_id: usize, + cached_size: Option, } struct IconCache { @@ -267,142 +155,75 @@ struct IconCache { detailed_timer_icon: Option>, } -impl Default for Renderer { - fn default() -> Self { - Self::new() - } -} +impl SceneManager { + /// Creates a new scene manager. + pub fn new(mut allocator: impl ResourceAllocator) -> Self { + let mut builder = allocator.path_builder(); + builder.move_to(0.0, 0.0); + builder.line_to(0.0, 1.0); + builder.line_to(1.0, 1.0); + builder.line_to(1.0, 0.0); + builder.close(); + let rectangle = Handle::new(0, builder.finish(&mut allocator)); -impl Renderer { - /// Creates a new renderer. - pub fn new() -> Self { Self { - #[cfg(feature = "font-loading")] - timer_font_setting: None, - timer_font: Font::from_slice( - TIMER_FONT, - 0, - FontStyle::Normal, - FontWeight::Bold, - FontStretch::Normal, - ) - .unwrap(), - timer_glyph_cache: GlyphCache::new(), - #[cfg(feature = "font-loading")] - times_font_setting: None, - times_font: Font::from_slice( - TEXT_FONT, - 0, - FontStyle::Normal, - FontWeight::Bold, - FontStretch::Normal, - ) - .unwrap(), - times_glyph_cache: GlyphCache::new(), - #[cfg(feature = "font-loading")] - text_font_setting: None, - text_font: Font::from_slice( - TEXT_FONT, - 0, - FontStyle::Normal, - FontWeight::Normal, - FontStretch::Normal, - ) - .unwrap(), - text_glyph_cache: GlyphCache::new(), - rectangle: None, + fonts: FontCache::new().unwrap(), icons: IconCache { game_icon: None, split_icons: Vec::new(), detailed_timer_icon: None, }, + // We use 0 for the rectangle. + next_id: 1, + scene: Scene::new(rectangle), cached_size: None, - text_buffer: None, } } - /// Renders the layout state with the backend provided. A resolution needs - /// to be provided as well so that the contents are rendered according to - /// aspect ratio of the render target. - pub fn render>( + /// Accesses the [`Scene`] in order to render the [`Entities`](Entity). + pub fn scene(&self) -> &Scene { + &self.scene + } + + /// Updates the [`Scene`] by updating the [`Entities`](Entity) according to + /// the [`LayoutState`] provided. The [`ResourceAllocator`] is used to + /// allocate the resources necessary that the [`Entities`](Entity) use. A + /// resolution needs to be provided as well so that the [`Entities`](Entity) + /// are positioned and sized correctly for a renderer to then consume. If a + /// change in the layout size is detected, a new more suitable resolution + /// for subsequent updates is being returned. This is however merely a hint + /// and can be completely ignored. + pub fn update_scene>( &mut self, - backend: &mut B, + allocator: A, resolution: (f32, f32), state: &LayoutState, ) -> Option<(f32, f32)> { #[cfg(feature = "font-loading")] - { - if self.timer_font_setting != state.timer_font { - self.timer_font = state - .timer_font - .as_ref() - .and_then(Font::load) - .unwrap_or_else(|| { - Font::from_slice( - TIMER_FONT, - 0, - FontStyle::Normal, - FontWeight::Bold, - FontStretch::Normal, - ) - .unwrap() - }); - self.timer_glyph_cache.clear(backend); - self.timer_font_setting.clone_from(&state.timer_font); - } + self.fonts.maybe_reload(state); - if self.times_font_setting != state.times_font { - self.times_font = state - .times_font - .as_ref() - .and_then(Font::load) - .unwrap_or_else(|| { - Font::from_slice( - TEXT_FONT, - 0, - FontStyle::Normal, - FontWeight::Bold, - FontStretch::Normal, - ) - .unwrap() - }); - self.times_glyph_cache.clear(backend); - self.times_font_setting.clone_from(&state.times_font); - } + self.scene.clear(); - if self.text_font_setting != state.text_font { - self.text_font = state - .text_font - .as_ref() - .and_then(Font::load) - .unwrap_or_else(|| { - Font::from_slice( - TEXT_FONT, - 0, - FontStyle::Normal, - FontWeight::Normal, - FontStretch::Normal, - ) - .unwrap() - }); - self.text_glyph_cache.clear(backend); - self.text_font_setting.clone_from(&state.text_font); - } - } + self.scene + .set_background(decode_gradient(&state.background)); - match state.direction { - LayoutDirection::Vertical => self.render_vertical(backend, resolution, state), - LayoutDirection::Horizontal => self.render_horizontal(backend, resolution, state), - } + let new_dimensions = match state.direction { + LayoutDirection::Vertical => self.render_vertical(allocator, resolution, state), + LayoutDirection::Horizontal => self.render_horizontal(allocator, resolution, state), + }; + + self.scene.recalculate_if_bottom_layer_changed(); + + new_dimensions } - fn render_vertical>( + fn render_vertical( &mut self, - backend: &mut B, + allocator: impl ResourceAllocator, resolution: (f32, f32), state: &LayoutState, ) -> Option<(f32, f32)> { - let total_height = state.components.iter().map(component_height).sum::(); + let total_height = component::layout_height(state); let cached_total_size = self .cached_size @@ -431,24 +252,12 @@ impl Renderer { let aspect_ratio = resolution.0 as f32 / resolution.1 as f32; let mut context = RenderContext { - backend, + handles: Handles::new(self.next_id, allocator), transform: Transform::scale(resolution.0 as f32, resolution.1 as f32), - rectangle: &mut self.rectangle, - timer_font: &self.timer_font, - timer_glyph_cache: &mut self.timer_glyph_cache, - times_font: &mut self.times_font, - times_glyph_cache: &mut self.times_glyph_cache, - text_font: &self.text_font, - text_glyph_cache: &mut self.text_glyph_cache, - text_buffer: &mut self.text_buffer, + scene: &mut self.scene, + fonts: &mut self.fonts, }; - // Initially we are in Backend Coordinate Space. - // We can render the background here from (0, 0) to (1, 1) as we just - // want to fill all of the background. We don't need to know anything - // about the aspect ratio or specific sizes. - context.render_background(&state.background); - // Now we transform the coordinate space to Renderer Coordinate Space by // non-uniformly adjusting for the aspect ratio. context.scale_non_uniform_x(aspect_ratio.recip()); @@ -463,25 +272,27 @@ impl Renderer { let width = aspect_ratio * total_height; for component in &state.components { - let height = component_height(component); + let height = component::height(component); let dim = [width, height]; - render_component(&mut context, &mut self.icons, component, state, dim); + component::render(&mut context, &mut self.icons, component, state, dim); // We translate the coordinate space to the Component Coordinate // Space of the next component by shifting by the height of the // current component in the Component Coordinate Space. context.translate(0.0, height); } + self.next_id = context.handles.into_next_id(); + new_resolution } - fn render_horizontal>( + fn render_horizontal( &mut self, - backend: &mut B, + allocator: impl ResourceAllocator, resolution: (f32, f32), state: &LayoutState, ) -> Option<(f32, f32)> { - let total_width = state.components.iter().map(component_width).sum::(); + let total_width = component::layout_width(state); let cached_total_size = self .cached_size @@ -509,24 +320,12 @@ impl Renderer { let aspect_ratio = resolution.0 as f32 / resolution.1 as f32; let mut context = RenderContext { - backend, + handles: Handles::new(self.next_id, allocator), transform: Transform::scale(resolution.0 as f32, resolution.1 as f32), - rectangle: &mut self.rectangle, - timer_font: &mut self.timer_font, - timer_glyph_cache: &mut self.timer_glyph_cache, - times_font: &mut self.times_font, - times_glyph_cache: &mut self.times_glyph_cache, - text_font: &mut self.text_font, - text_glyph_cache: &mut self.text_glyph_cache, - text_buffer: &mut self.text_buffer, + scene: &mut self.scene, + fonts: &mut self.fonts, }; - // Initially we are in Backend Coordinate Space. - // We can render the background here from (0, 0) to (1, 1) as we just - // want to fill all of the background. We don't need to know anything - // about the aspect ratio or specific sizes. - context.render_background(&state.background); - // Now we transform the coordinate space to Renderer Coordinate Space by // non-uniformly adjusting for the aspect ratio. context.scale_non_uniform_x(aspect_ratio.recip()); @@ -544,124 +343,90 @@ impl Renderer { let width_scaling = TWO_ROW_HEIGHT * aspect_ratio / total_width; for component in &state.components { - let width = component_width(component) * width_scaling; + let width = component::width(component) * width_scaling; let height = TWO_ROW_HEIGHT; let dim = [width, height]; - render_component(&mut context, &mut self.icons, component, state, dim); + component::render(&mut context, &mut self.icons, component, state, dim); // We translate the coordinate space to the Component Coordinate // Space of the next component by shifting by the width of the // current component in the Component Coordinate Space. context.translate(width, 0.0); } - new_resolution - } -} + self.next_id = context.handles.into_next_id(); -fn render_component( - context: &mut RenderContext<'_, B>, - icons: &mut IconCache, - component: &ComponentState, - state: &LayoutState, - dim: [f32; 2], -) { - match component { - ComponentState::BlankSpace(state) => component::blank_space::render(context, dim, state), - ComponentState::DetailedTimer(component) => component::detailed_timer::render( - context, - dim, - component, - state, - &mut icons.detailed_timer_icon, - ), - ComponentState::Graph(component) => { - component::graph::render(context, dim, component, state) - } - ComponentState::KeyValue(component) => { - component::key_value::render(context, dim, component, state) - } - ComponentState::Separator(component) => { - component::separator::render(context, dim, component, state) - } - ComponentState::Splits(component) => { - component::splits::render(context, dim, component, state, &mut icons.split_icons) - } - ComponentState::Text(component) => component::text::render(context, dim, component, state), - ComponentState::Timer(component) => { - component::timer::render(context, dim, component); - } - ComponentState::Title(component) => { - component::title::render(context, dim, component, state, &mut icons.game_icon) - } + new_resolution } } -struct RenderContext<'b, B: Backend> { +struct RenderContext<'b, A: ResourceAllocator> { transform: Transform, - backend: &'b mut B, - rectangle: &'b mut Option, - timer_font: &'b Font<'static>, - timer_glyph_cache: &'b mut GlyphCache, - text_font: &'b Font<'static>, - text_glyph_cache: &'b mut GlyphCache, - times_font: &'b Font<'static>, - times_glyph_cache: &'b mut GlyphCache, - text_buffer: &'b mut Option, + handles: Handles, + scene: &'b mut Scene, + fonts: &'b mut FontCache, } -impl RenderContext<'_, B> { +impl RenderContext<'_, A> { + fn rectangle(&self) -> Handle { + self.scene.rectangle() + } + fn backend_render_rectangle(&mut self, [x1, y1]: Pos, [x2, y2]: Pos, shader: FillShader) { let transform = self .transform .pre_translate([x1, y1].into()) .pre_scale(x2 - x1, y2 - y1); - let rectangle = self.rectangle.get_or_insert_with({ - let backend = &mut *self.backend; - move || { - let mut builder = backend.path_builder(); - builder.move_to(0.0, 0.0); - builder.line_to(0.0, 1.0); - builder.line_to(1.0, 1.0); - builder.line_to(1.0, 0.0); - builder.close(); - builder.finish(backend) - } - }); + let rectangle = self.rectangle(); - self.backend.render_fill_path(rectangle, shader, transform); + self.scene + .bottom_layer_mut() + .push(Entity::FillPath(rectangle, shader, transform)); } - fn render_path(&mut self, path: &B::Path, color: Color) { - self.backend - .render_fill_path(path, solid(&color), self.transform) + fn backend_render_top_rectangle(&mut self, [x1, y1]: Pos, [x2, y2]: Pos, shader: FillShader) { + let transform = self + .transform + .pre_translate([x1, y1].into()) + .pre_scale(x2 - x1, y2 - y1); + + let rectangle = self.rectangle(); + + self.scene + .top_layer_mut() + .push(Entity::FillPath(rectangle, shader, transform)); } - fn render_stroke_path(&mut self, path: &B::Path, color: Color, stroke_width: f32) { - self.backend - .render_stroke_path(path, stroke_width, color.to_array(), self.transform) + fn top_layer_path(&mut self, path: Handle, color: Color) { + self.scene + .top_layer_mut() + .push(Entity::FillPath(path, solid(&color), self.transform)); } - fn create_icon(&mut self, image_data: &[u8]) -> Option> { + fn top_layer_stroke_path(&mut self, path: Handle, color: Color, stroke_width: f32) { + self.scene.top_layer_mut().push(Entity::StrokePath( + path, + stroke_width, + color.to_array(), + self.transform, + )); + } + + fn create_icon(&mut self, image_data: &[u8]) -> Option> { if image_data.is_empty() { return None; } let image = image::load_from_memory(image_data).ok()?.to_rgba8(); - let backend_image = self - .backend - .create_image(image.width(), image.height(), &image); Some(Icon { - image: backend_image, aspect_ratio: image.width() as f32 / image.height() as f32, + image: self + .handles + .create_image(image.width(), image.height(), &image), }) } - fn free_path(&mut self, path: B::Path) { - self.backend.free_path(path) - } - fn scale(&mut self, factor: f32) { self.transform = self.transform.pre_scale(factor, factor); } @@ -680,11 +445,17 @@ impl RenderContext<'_, B> { } } + fn render_top_rectangle(&mut self, top_left: Pos, bottom_right: Pos, gradient: &Gradient) { + if let Some(colors) = decode_gradient(gradient) { + self.backend_render_top_rectangle(top_left, bottom_right, colors); + } + } + fn render_icon( &mut self, [mut x, mut y]: Pos, [mut width, mut height]: Pos, - icon: &Icon, + icon: &Icon, ) { let box_aspect_ratio = width / height; let aspect_ratio_diff = box_aspect_ratio / icon.aspect_ratio; @@ -706,25 +477,9 @@ impl RenderContext<'_, B> { .pre_translate([x, y].into()) .pre_scale(width, height); - // FIXME: Deduplicate - let rectangle = self.rectangle.get_or_insert_with({ - let backend = &mut *self.backend; - move || { - let mut builder = backend.path_builder(); - builder.move_to(0.0, 0.0); - builder.line_to(0.0, 1.0); - builder.line_to(1.0, 1.0); - builder.line_to(1.0, 0.0); - builder.close(); - builder.finish(backend) - } - }); - - self.backend.render_image(&icon.image, rectangle, transform); - } - - fn render_background(&mut self, background: &Gradient) { - self.render_rectangle([0.0, 0.0], [1.0, 1.0], background); + self.scene + .bottom_layer_mut() + .push(Entity::Image(icon.image.share(), transform)); } fn render_key_value_component( @@ -732,6 +487,7 @@ impl RenderContext<'_, B> { key: &str, abbreviations: &[Cow<'_, str>], value: &str, + updates_frequently: bool, [width, height]: [f32; 2], key_color: Color, value_color: Color, @@ -739,6 +495,7 @@ impl RenderContext<'_, B> { ) { let left_of_value_x = self.render_numbers( value, + Layer::from_updates_frequently(updates_frequently), [width - PADDING, height + TEXT_ALIGN_BOTTOM], DEFAULT_TEXT_SIZE, solid(&value_color), @@ -772,23 +529,24 @@ impl RenderContext<'_, B> { ) -> f32 { let mut cursor = font::Cursor::new(pos); - let mut buffer = self.text_buffer.take().unwrap_or_default(); + let mut buffer = self.fonts.buffer.take().unwrap_or_default(); buffer.push_str(text.trim()); buffer.guess_segment_properties(); - let font = self.text_font.scale(scale); + let font = self.fonts.text.font.scale(scale); let glyphs = font.shape(buffer); font::render( glyphs.left_aligned(&mut cursor, max_x), shader, &font, - self.text_glyph_cache, + &mut self.fonts.text.glyph_cache, &self.transform, - self.backend, + &mut self.handles, + self.scene.bottom_layer_mut(), ); - *self.text_buffer = Some(glyphs.into_buffer()); + self.fonts.buffer = Some(glyphs.into_buffer()); cursor.x } @@ -804,51 +562,54 @@ impl RenderContext<'_, B> { ) { let mut cursor = font::Cursor::new(pos); - let mut buffer = self.text_buffer.take().unwrap_or_default(); + let mut buffer = self.fonts.buffer.take().unwrap_or_default(); buffer.push_str(text.trim()); buffer.guess_segment_properties(); - let font = self.text_font.scale(scale); + let font = self.fonts.text.font.scale(scale); let glyphs = font.shape(buffer); font::render( glyphs.centered(&mut cursor, min_x, max_x), shader, &font, - self.text_glyph_cache, + &mut self.fonts.text.glyph_cache, &self.transform, - self.backend, + &mut self.handles, + self.scene.bottom_layer_mut(), ); - *self.text_buffer = Some(glyphs.into_buffer()); + self.fonts.buffer = Some(glyphs.into_buffer()); } fn render_text_right_align( &mut self, text: &str, + layer: Layer, pos: Pos, scale: f32, shader: FillShader, ) -> f32 { let mut cursor = font::Cursor::new(pos); - let mut buffer = self.text_buffer.take().unwrap_or_default(); + let mut buffer = self.fonts.buffer.take().unwrap_or_default(); buffer.push_str(text.trim()); buffer.guess_segment_properties(); - let font = self.text_font.scale(scale); + let font = self.fonts.text.font.scale(scale); let glyphs = font.shape(buffer); font::render( glyphs.right_aligned(&mut cursor), shader, &font, - self.text_glyph_cache, + &mut self.fonts.text.glyph_cache, &self.transform, - self.backend, + &mut self.handles, + self.scene.layer_mut(layer), ); - *self.text_buffer = Some(glyphs.into_buffer()); + self.fonts.buffer = Some(glyphs.into_buffer()); cursor.x } @@ -870,50 +631,66 @@ impl RenderContext<'_, B> { } } - fn render_numbers(&mut self, text: &str, pos: Pos, scale: f32, shader: FillShader) -> f32 { + fn render_numbers( + &mut self, + text: &str, + layer: Layer, + pos: Pos, + scale: f32, + shader: FillShader, + ) -> f32 { let mut cursor = font::Cursor::new(pos); - let mut buffer = self.text_buffer.take().unwrap_or_default(); + let mut buffer = self.fonts.buffer.take().unwrap_or_default(); buffer.push_str(text.trim()); buffer.guess_segment_properties(); - let font = self.times_font.scale(scale); + let font = self.fonts.times.font.scale(scale); let glyphs = font.shape_tabular_numbers(buffer); font::render( glyphs.tabular_numbers(&mut cursor), shader, &font, - self.times_glyph_cache, + &mut self.fonts.times.glyph_cache, &self.transform, - self.backend, + &mut self.handles, + self.scene.layer_mut(layer), ); - *self.text_buffer = Some(glyphs.into_buffer()); + self.fonts.buffer = Some(glyphs.into_buffer()); cursor.x } - fn render_timer(&mut self, text: &str, pos: Pos, scale: f32, shader: FillShader) -> f32 { + fn render_timer( + &mut self, + text: &str, + layer: Layer, + pos: Pos, + scale: f32, + shader: FillShader, + ) -> f32 { let mut cursor = font::Cursor::new(pos); - let mut buffer = self.text_buffer.take().unwrap_or_default(); + let mut buffer = self.fonts.buffer.take().unwrap_or_default(); buffer.push_str(text.trim()); buffer.guess_segment_properties(); - let font = self.timer_font.scale(scale); + let font = self.fonts.timer.font.scale(scale); let glyphs = font.shape_tabular_numbers(buffer); font::render( glyphs.tabular_numbers(&mut cursor), shader, &font, - self.timer_glyph_cache, + &mut self.fonts.timer.glyph_cache, &self.transform, - self.backend, + &mut self.handles, + self.scene.layer_mut(layer), ); - *self.text_buffer = Some(glyphs.into_buffer()); + self.fonts.buffer = Some(glyphs.into_buffer()); cursor.x } @@ -954,14 +731,14 @@ impl RenderContext<'_, B> { } fn measure_text(&mut self, text: &str, scale: f32) -> f32 { - let mut buffer = self.text_buffer.take().unwrap_or_default(); + let mut buffer = self.fonts.buffer.take().unwrap_or_default(); buffer.push_str(text.trim()); buffer.guess_segment_properties(); - let glyphs = self.text_font.scale(scale).shape(buffer); + let glyphs = self.fonts.text.font.scale(scale).shape(buffer); let width = glyphs.width(); - *self.text_buffer = Some(glyphs.into_buffer()); + self.fonts.buffer = Some(glyphs.into_buffer()); width } @@ -969,11 +746,16 @@ impl RenderContext<'_, B> { fn measure_numbers(&mut self, text: &str, scale: f32) -> f32 { let mut cursor = font::Cursor::new([0.0; 2]); - let mut buffer = self.text_buffer.take().unwrap_or_default(); + let mut buffer = self.fonts.buffer.take().unwrap_or_default(); buffer.push_str(text.trim()); buffer.guess_segment_properties(); - let glyphs = self.times_font.scale(scale).shape_tabular_numbers(buffer); + let glyphs = self + .fonts + .times + .font + .scale(scale) + .shape_tabular_numbers(buffer); // Iterate over all glyphs, to move the cursor forward. glyphs.tabular_numbers(&mut cursor).for_each(drop); @@ -981,7 +763,7 @@ impl RenderContext<'_, B> { // Wherever we end up is our width. let width = -cursor.x; - *self.text_buffer = Some(glyphs.into_buffer()); + self.fonts.buffer = Some(glyphs.into_buffer()); width } @@ -1003,59 +785,3 @@ const fn decode_gradient(gradient: &Gradient) -> Option { const fn solid(color: &Color) -> FillShader { FillShader::SolidColor(color.to_array()) } - -fn component_width(component: &ComponentState) -> f32 { - match component { - ComponentState::BlankSpace(state) => state.size as f32 * PSEUDO_PIXELS, - ComponentState::DetailedTimer(_) => 7.0, - ComponentState::Graph(_) => 7.0, - ComponentState::KeyValue(_) => 6.0, - ComponentState::Separator(_) => SEPARATOR_THICKNESS, - ComponentState::Splits(state) => { - let column_count = 2.0; // FIXME: Not always 2. - let split_width = 2.0 + column_count * component::splits::COLUMN_WIDTH; - state.splits.len() as f32 * split_width - } - ComponentState::Text(_) => 6.0, - ComponentState::Timer(_) => 8.25, - ComponentState::Title(_) => 8.0, - } -} - -fn component_height(component: &ComponentState) -> f32 { - match component { - ComponentState::BlankSpace(state) => state.size as f32 * PSEUDO_PIXELS, - ComponentState::DetailedTimer(_) => 2.5, - ComponentState::Graph(state) => state.height as f32 * PSEUDO_PIXELS, - ComponentState::KeyValue(state) => { - if state.display_two_rows { - TWO_ROW_HEIGHT - } else { - DEFAULT_COMPONENT_HEIGHT - } - } - ComponentState::Separator(_) => SEPARATOR_THICKNESS, - ComponentState::Splits(state) => { - state.splits.len() as f32 - * if state.display_two_rows { - TWO_ROW_HEIGHT - } else { - DEFAULT_COMPONENT_HEIGHT - } - + if state.column_labels.is_some() { - DEFAULT_COMPONENT_HEIGHT - } else { - 0.0 - } - } - ComponentState::Text(state) => { - if state.display_two_rows { - TWO_ROW_HEIGHT - } else { - DEFAULT_COMPONENT_HEIGHT - } - } - ComponentState::Timer(state) => state.height as f32 * PSEUDO_PIXELS, - ComponentState::Title(_) => TWO_ROW_HEIGHT, - } -} diff --git a/src/rendering/resource/allocation.rs b/src/rendering/resource/allocation.rs new file mode 100644 index 00000000..173aa2f7 --- /dev/null +++ b/src/rendering/resource/allocation.rs @@ -0,0 +1,117 @@ +use super::SharedOwnership; + +/// A resource allocator provides all the paths and images necessary to place +/// [`Entities`](super::super::Entity) in a [`Scene`](super::super::Scene). This +/// is usually implemented by a specific renderer where the paths and images are +/// types that the renderer can directly render out. +pub trait ResourceAllocator { + /// The type the renderer uses for building paths. + type PathBuilder: PathBuilder; + /// The type the renderer uses for paths. + type Path: SharedOwnership; + /// The type the renderer uses for textures. + type Image: SharedOwnership; + + /// Creates a new [`PathBuilder`] to build a new path. + fn path_builder(&mut self) -> Self::PathBuilder; + + /// Builds a new circle. A default implementation that approximates the + /// circle with 4 cubic bézier curves is provided. For more accuracy or + /// performance you can change the implementation. + fn build_circle(&mut self, x: f32, y: f32, r: f32) -> Self::Path { + // Based on https://spencermortensen.com/articles/bezier-circle/ + const C: f64 = 0.551915024494; + let c = (C * r as f64) as f32; + let mut builder = self.path_builder(); + builder.move_to(x, y - r); + builder.curve_to(x + c, y - r, x + r, y - c, x + r, y); + builder.curve_to(x + r, y + c, x + c, y + r, x, y + r); + builder.curve_to(x - c, y + r, x - r, y + c, x - r, y); + builder.curve_to(x - r, y - c, x - c, y - r, x, y - r); + builder.close(); + builder.finish(self) + } + + /// Instructs the backend to create an image out of the image data provided. + /// The image's resolution is provided as well. The data is an array of + /// RGBA8 encoded pixels (red, green, blue, alpha with each channel being an + /// u8). + fn create_image(&mut self, width: u32, height: u32, data: &[u8]) -> Self::Image; +} + +/// The [`ResourceAllocator`] provides a path builder that defines how to build +/// paths that can be used with the renderer. +pub trait PathBuilder { + /// The type of the path to build. This needs to be identical to the type of + /// the path used by the [`ResourceAllocator`]. + type Path; + + /// Moves the cursor to a specific position and starts a new contour. + fn move_to(&mut self, x: f32, y: f32); + + /// Adds a line from the previous position to the position specified, while + /// also moving the cursor along. + fn line_to(&mut self, x: f32, y: f32); + + /// Adds a quadratic bézier curve from the previous position to the position + /// specified, while also moving the cursor along. (x1, y1) specifies the + /// control point. + fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32); + + /// Adds a cubic bézier curve from the previous position to the position + /// specified, while also moving the cursor along. (x1, y1) and (x2, y2) + /// specify the two control points. + fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32); + + /// Closes the current contour. The current position and the initial + /// position get connected by a line, forming a continuous loop. Nothing + /// if the path is empty or already closed. + fn close(&mut self); + + /// Finishes building the path. + fn finish(self, allocator: &mut A) -> Self::Path; +} + +pub struct MutPathBuilder(PB); + +impl PathBuilder<&mut A> for MutPathBuilder { + type Path = A::Path; + + fn move_to(&mut self, x: f32, y: f32) { + self.0.move_to(x, y) + } + + fn line_to(&mut self, x: f32, y: f32) { + self.0.line_to(x, y) + } + + fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { + self.0.quad_to(x1, y1, x, y) + } + + fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { + self.0.curve_to(x1, y1, x2, y2, x, y) + } + + fn close(&mut self) { + self.0.close() + } + + fn finish(self, allocator: &mut &mut A) -> Self::Path { + self.0.finish(*allocator) + } +} + +impl ResourceAllocator for &mut A { + type PathBuilder = MutPathBuilder; + type Path = A::Path; + type Image = A::Image; + + fn path_builder(&mut self) -> Self::PathBuilder { + MutPathBuilder((*self).path_builder()) + } + + fn create_image(&mut self, width: u32, height: u32, data: &[u8]) -> Self::Image { + (*self).create_image(width, height, data) + } +} diff --git a/src/rendering/resource/handles.rs b/src/rendering/resource/handles.rs new file mode 100644 index 00000000..4605261b --- /dev/null +++ b/src/rendering/resource/handles.rs @@ -0,0 +1,127 @@ +use std::{ + hash::{Hash, Hasher}, + ops::Deref, +}; + +use super::{PathBuilder, ResourceAllocator, SharedOwnership}; + +pub struct Handles { + next_id: usize, + allocator: A, +} + +impl Handles { + pub const fn new(next_id: usize, allocator: A) -> Self { + Self { next_id, allocator } + } + + pub fn next(&mut self, element: T) -> Handle { + let id = self.next_id; + self.next_id += 1; + Handle { id, inner: element } + } + + /// Get a reference to the handles's next ID. + #[allow(clippy::missing_const_for_fn)] // FIXME: Drop is unsupported. + pub fn into_next_id(self) -> usize { + self.next_id + } +} + +pub struct HandlePathBuilder(A::PathBuilder); + +impl PathBuilder> for HandlePathBuilder { + type Path = Handle; + + fn move_to(&mut self, x: f32, y: f32) { + self.0.move_to(x, y); + } + + fn line_to(&mut self, x: f32, y: f32) { + self.0.line_to(x, y) + } + + fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { + self.0.quad_to(x1, y1, x, y) + } + + fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { + self.0.curve_to(x1, y1, x2, y2, x, y) + } + + fn close(&mut self) { + self.0.close() + } + + fn finish(self, handles: &mut Handles) -> Self::Path { + let path = self.0.finish(&mut handles.allocator); + handles.next(path) + } +} + +impl ResourceAllocator for Handles { + type PathBuilder = HandlePathBuilder; + type Path = Handle; + type Image = Handle; + + fn path_builder(&mut self) -> Self::PathBuilder { + HandlePathBuilder(self.allocator.path_builder()) + } + + fn build_circle(&mut self, x: f32, y: f32, r: f32) -> Self::Path { + let circle = self.allocator.build_circle(x, y, r); + self.next(circle) + } + + fn create_image(&mut self, width: u32, height: u32, data: &[u8]) -> Self::Image { + let image = self.allocator.create_image(width, height, data); + self.next(image) + } +} + +/// A handle can be used to uniquely identify the resource it wraps. +pub struct Handle { + id: usize, + inner: T, +} + +impl SharedOwnership for Handle { + fn share(&self) -> Self { + Self { + id: self.id, + inner: self.inner.share(), + } + } +} + +impl Handle { + /// Creates a handle based on some resource that it wraps and a unique ID. + pub fn new(id: usize, resource: T) -> Self { + Self { + id, + inner: resource, + } + } +} + +impl Deref for Handle { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl Hash for Handle { + fn hash(&self, state: &mut H) { + self.id.hash(state) + } +} + +impl Eq for Handle {} + +impl PartialEq for Handle { + fn eq(&self, other: &Self) -> bool { + self.id.eq(&other.id) + } +} diff --git a/src/rendering/resource/mod.rs b/src/rendering/resource/mod.rs new file mode 100644 index 00000000..1e6198f7 --- /dev/null +++ b/src/rendering/resource/mod.rs @@ -0,0 +1,9 @@ +mod allocation; +mod handles; +mod shared_ownership; + +pub use self::{ + allocation::{PathBuilder, ResourceAllocator}, + handles::*, + shared_ownership::SharedOwnership, +}; diff --git a/src/rendering/resource/shared_ownership.rs b/src/rendering/resource/shared_ownership.rs new file mode 100644 index 00000000..59f23a42 --- /dev/null +++ b/src/rendering/resource/shared_ownership.rs @@ -0,0 +1,31 @@ +use std::{rc::Rc, sync::Arc}; + +/// Describes that ownership of a value can be cheaply shared. This is similar +/// to the [`Clone`] trait, but is expected to only be implemented if sharing is +/// cheap, such as for [`Rc`] and [`Arc`]. +pub trait SharedOwnership { + /// Share the value. + fn share(&self) -> Self; +} + +impl SharedOwnership for Rc { + fn share(&self) -> Self { + self.clone() + } +} + +impl SharedOwnership for Arc { + fn share(&self) -> Self { + self.clone() + } +} + +impl SharedOwnership for Option { + fn share(&self) -> Self { + self.as_ref().map(SharedOwnership::share) + } +} + +impl SharedOwnership for () { + fn share(&self) -> Self {} +} diff --git a/src/rendering/scene.rs b/src/rendering/scene.rs new file mode 100644 index 00000000..548f3dfc --- /dev/null +++ b/src/rendering/scene.rs @@ -0,0 +1,130 @@ +use super::{ + entity::{calculate_hash, Entity}, + resource::{Handle, SharedOwnership}, + FillShader, +}; + +/// Describes a layer of a [`Scene`] to place an [`Entity`] on. +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum Layer { + /// The bottom layer is the layer where all the less frequently changing + /// [`Entities`](Entity) are being placed on. + Bottom, + /// The top layer is the layer where all the [`Entities`](Entity) that are + /// expected to frequently change are being placed on. + Top, +} + +impl Layer { + /// Returns the appropriate layer to use depending on whether the [`Entity`] + /// to place updates frequently or not. + pub const fn from_updates_frequently(updates_frequently: bool) -> Self { + match updates_frequently { + false => Self::Bottom, + true => Self::Top, + } + } +} + +/// A scene describes all the [`Entities`](Entity) to visualize. It consists of +/// two [`Layers`](Layer) that are supposed to be composited on top of each +/// other. The bottom [`Layer`] changes infrequently and doesn't need to be +/// rerendered for most frames. The top [`Layer`] contains all the per frame +/// changes and needs to be rerendered for every frame. If however it is empty +/// and both the bottom layer didn't change, then no new frame needs to be +/// rendered. While the top [`Layer`] is inherently transparent, the bottom +/// [`Layer`] has a background that needs to be considered. +pub struct Scene { + rectangle: Handle

, + background: Option, + bottom_hash: u64, + bottom_layer_changed: bool, + bottom_layer: Vec>, + top_layer: Vec>, +} + +impl Scene { + /// Creates a new scene with the rectangle provided to use for placing + /// rectangle entities. + pub fn new(rectangle: Handle

) -> Self { + Self { + rectangle, + background: None, + bottom_hash: calculate_hash::(&None, &[]), + bottom_layer_changed: false, + bottom_layer: Vec::new(), + top_layer: Vec::new(), + } + } + + /// Get a reference to the bottom [`Layer's`](Layer) background. While the + /// top [`Layer`] is inherently transparent, the bottom [`Layer`] has a + /// background that needs to be considered. + pub fn background(&self) -> &Option { + &self.background + } + + /// Check if the scene's bottom [`Layer`] changed. Use this method to check + /// if the bottom [`Layer`] needs to be rerendered. If the background of the + /// bottom [`Layer`] changes this also returns `true`, so the background + /// doesn't need to manually be compared. + pub fn bottom_layer_changed(&self) -> bool { + self.bottom_layer_changed + } + + /// Get a reference to the scene's bottom [`Layer`]. This [`Layer`] is + /// intended to infrequently change, so it doesn't need to be rerendered + /// every frame. + pub fn bottom_layer(&self) -> &[Entity] { + &self.bottom_layer + } + + /// Get a reference to the scene's top [`Layer`]. + pub fn top_layer(&self) -> &[Entity] { + &self.top_layer + } + + /// Get access to the rectangle resource the scene stores. + pub fn rectangle(&self) -> Handle

{ + self.rectangle.share() + } + + /// Set the bottom [`Layer's`](Layer) background. + pub fn set_background(&mut self, background: Option) { + self.background = background; + } + + /// Get a mutable reference to the scene's bottom [`Layer`]. + pub fn bottom_layer_mut(&mut self) -> &mut Vec> { + &mut self.bottom_layer + } + + /// Get a mutable reference to the scene's top [`Layer`]. + pub fn top_layer_mut(&mut self) -> &mut Vec> { + &mut self.top_layer + } + + /// Clears all the [`Layers`](Layer) such that no [`Entities`](Entity) are + /// left. + pub fn clear(&mut self) { + self.bottom_layer.clear(); + self.top_layer.clear(); + } + + /// Recalculates the hash of the bottom [`Layer`] and checks if it changed. + /// The bottom [`Layer`] is intended to infrequently change, such that it + /// doesn't need to be rerendered all the time. + pub fn recalculate_if_bottom_layer_changed(&mut self) { + let new_hash = calculate_hash(&self.background, &self.bottom_layer); + self.bottom_layer_changed = new_hash != self.bottom_hash; + self.bottom_hash = new_hash; + } + + /// Accesses the [`Layer`] specified mutably. + pub fn layer_mut(&mut self, layer: Layer) -> &mut Vec> { + match layer { + Layer::Bottom => &mut self.bottom_layer, + Layer::Top => &mut self.top_layer, + } + } +} diff --git a/src/rendering/software.rs b/src/rendering/software.rs index 5a4e0fdf..7604defb 100644 --- a/src/rendering/software.rs +++ b/src/rendering/software.rs @@ -1,20 +1,29 @@ //! Provides a software renderer that can be used without a GPU. The renderer is //! surprisingly fast and can be considered the default rendering backend. -use super::{Backend, FillShader, Renderer, Rgba, Transform}; +use std::{mem, ops::Deref, rc::Rc}; + +use super::{ + entity::Entity, + resource::{self, ResourceAllocator}, + FillShader, Scene, SceneManager, SharedOwnership, Transform, +}; use crate::layout::LayoutState; use image::ImageBuffer; use tiny_skia::{ - Canvas, Color, FillRule, FilterQuality, GradientStop, LinearGradient, Paint, Path, PathBuilder, - Pattern, Pixmap, PixmapMut, Point, Shader, SpreadMode, Stroke, + BlendMode, Color, FillRule, FilterQuality, GradientStop, LinearGradient, Paint, Path, + PathBuilder, Pattern, Pixmap, PixmapMut, Point, Rect, Shader, SpreadMode, Stroke, }; pub use image::{self, RgbaImage}; struct SkiaBuilder(PathBuilder); -impl super::PathBuilder> for SkiaBuilder { - type Path = Option; +type SkiaPath = Option>; +type SkiaImage = Option>; + +impl resource::PathBuilder for SkiaBuilder { + type Path = SkiaPath; fn move_to(&mut self, x: f32, y: f32) { self.0.move_to(x, y) @@ -36,134 +45,31 @@ impl super::PathBuilder> for SkiaBuilder { self.0.close() } - fn finish(self, _: &mut SoftwareBackend<'_>) -> Self::Path { - self.0.finish() + fn finish(self, _: &mut SkiaAllocator) -> Self::Path { + self.0.finish().map(UnsafeRc::new) } } -fn convert_color([r, g, b, a]: [f32; 4]) -> Color { +fn convert_color(&[r, g, b, a]: &[f32; 4]) -> Color { Color::from_rgba(r, g, b, a).unwrap() } -fn convert_transform(transform: Transform) -> tiny_skia::Transform { +fn convert_transform(transform: &Transform) -> tiny_skia::Transform { let [sx, ky, kx, sy, tx, ty] = transform.to_array(); - tiny_skia::Transform::from_row(sx, ky, kx, sy, tx, ty).unwrap() + tiny_skia::Transform::from_row(sx, ky, kx, sy, tx, ty) } -struct SoftwareBackend<'a> { - canvas: Canvas<'a>, -} +struct SkiaAllocator; -impl Backend for SoftwareBackend<'_> { +impl ResourceAllocator for SkiaAllocator { type PathBuilder = SkiaBuilder; - type Path = Option; - type Image = Option; + type Path = SkiaPath; + type Image = SkiaImage; fn path_builder(&mut self) -> Self::PathBuilder { SkiaBuilder(PathBuilder::new()) } - fn render_fill_path(&mut self, path: &Self::Path, shader: FillShader, transform: Transform) { - if let Some(path) = path { - self.canvas.set_transform(convert_transform(transform)); - - let shader = match shader { - FillShader::SolidColor(col) => Shader::SolidColor(convert_color(col)), - FillShader::VerticalGradient(top, bottom) => { - let bounds = path.bounds(); - LinearGradient::new( - Point::from_xy(0.0, bounds.top()), - Point::from_xy(0.0, bounds.bottom()), - vec![ - GradientStop::new(0.0, convert_color(top)), - GradientStop::new(1.0, convert_color(bottom)), - ], - SpreadMode::Pad, - tiny_skia::Transform::identity(), - ) - .unwrap() - } - FillShader::HorizontalGradient(left, right) => { - let bounds = path.bounds(); - LinearGradient::new( - Point::from_xy(bounds.left(), 0.0), - Point::from_xy(bounds.right(), 0.0), - vec![ - GradientStop::new(0.0, convert_color(left)), - GradientStop::new(1.0, convert_color(right)), - ], - SpreadMode::Pad, - tiny_skia::Transform::identity(), - ) - .unwrap() - } - }; - - self.canvas.fill_path( - path, - &Paint { - shader, - anti_alias: true, - ..Default::default() - }, - FillRule::Winding, - ); - } - } - - fn render_stroke_path( - &mut self, - path: &Self::Path, - stroke_width: f32, - color: Rgba, - transform: Transform, - ) { - if let Some(path) = path { - self.canvas.set_transform(convert_transform(transform)); - - self.canvas.stroke_path( - path, - &Paint { - shader: Shader::SolidColor(convert_color(color)), - anti_alias: true, - ..Default::default() - }, - &Stroke { - width: stroke_width, - ..Default::default() - }, - ); - } - } - - fn render_image(&mut self, image: &Self::Image, rectangle: &Self::Path, transform: Transform) { - if let (Some(path), Some(image)) = (rectangle, image) { - self.canvas.set_transform(convert_transform(transform)); - - self.canvas.fill_path( - path, - &Paint { - shader: Pattern::new( - image.as_ref(), - SpreadMode::Pad, - FilterQuality::Bilinear, - 1.0, - tiny_skia::Transform::from_scale( - 1.0 / image.width() as f32, - 1.0 / image.height() as f32, - ) - .unwrap(), - ), - anti_alias: true, - ..Default::default() - }, - FillRule::Winding, - ); - } - } - - fn free_path(&mut self, _: Self::Path) {} - fn create_image(&mut self, width: u32, height: u32, data: &[u8]) -> Self::Image { let mut image = Pixmap::new(width, height)?; for (d, &[r, g, b, a]) in image @@ -173,10 +79,8 @@ impl Backend for SoftwareBackend<'_> { { *d = tiny_skia::ColorU8::from_rgba(r, g, b, a).premultiply(); } - Some(image) + Some(UnsafeRc::new(image)) } - - fn free_image(&mut self, _: Self::Image) {} } /// The software renderer allows rendering layouts entirely on the CPU. This is @@ -184,54 +88,135 @@ impl Backend for SoftwareBackend<'_> { /// versions of the software renderer. This version of the software renderer /// does not own the image to render into. This allows the caller to manage /// their own image buffer. -pub struct BorrowedSoftwareRenderer { - renderer: Renderer, Option>, +pub struct BorrowedRenderer { + scene_manager: SceneManager, + background: Pixmap, + min_y: f32, + max_y: f32, +} + +struct UnsafeRc(Rc); + +impl Deref for UnsafeRc { + type Target = T; + + fn deref(&self) -> &Self::Target { + &*self.0 + } } -impl Default for BorrowedSoftwareRenderer { +impl UnsafeRc { + fn new(val: T) -> Self { + Self(Rc::new(val)) + } +} + +impl SharedOwnership for UnsafeRc { + fn share(&self) -> Self { + Self(self.0.share()) + } +} + +// TODO: Make sure this is actually true: + +// Safety: This is safe because the BorrowedSoftwareRenderer and the +// SceneManager never share any of their resources with anyone. For the +// BorrowedSoftwareRenderer this is trivially true as it doesn't share any its +// fields with anyone, you provide the image to render into yourself. For the +// SceneManager it's harder to prove. However as long as the trait bounds for +// the ResourceAllocator's Image and Path types do not require Sync or Send, +// then the SceneManager simply can't share any of the allocated resources +// across any threads at all. +unsafe impl Send for UnsafeRc {} + +// Safety: The BorrowedSoftwareRenderer only has a render method which requires +// exclusive access. The SceneManager could still mess it up. But as long as the +// ResourceAllocator's Image and Path types do not require Sync or Send, it +// can't make use of the Sync bound in any dangerous way anyway. +unsafe impl Sync for UnsafeRc {} + +impl Default for BorrowedRenderer { fn default() -> Self { Self::new() } } -impl BorrowedSoftwareRenderer { +impl BorrowedRenderer { /// Creates a new software renderer. pub fn new() -> Self { Self { - renderer: Renderer::new(), + scene_manager: SceneManager::new(SkiaAllocator), + background: Pixmap::new(1, 1).unwrap(), + min_y: f32::INFINITY, + max_y: f32::NEG_INFINITY, } } /// Renders the layout state provided into the image buffer provided. The - /// image has to be an array of RGBA8 encoded pixels (red, green, blue, + /// image has to be an array of `RGBA8` encoded pixels (red, green, blue, /// alpha with each channel being an u8). Some frameworks may over allocate - /// an image's dimensions. So an image with dimensions 100x50 may be over - /// allocated as 128x64. In that case you provide the real dimensions of - /// [100, 50] as the width and height, but a stride of 128 pixels as that + /// an image's dimensions. So an image with dimensions `100x50` may be over + /// allocated as `128x64`. In that case you provide the real dimensions of + /// `100x50` as the width and height, but a stride of `128` pixels as that /// correlates with the real width of the underlying buffer. It may detect /// that the layout got resized. In that case it returns the new ideal size. - /// This is entirely a hint and can be ignored entirely. The image is always - /// rendered with the resolution provided. + /// This is just a hint and can be ignored entirely. The image is always + /// rendered with the resolution provided. By default the renderer will try + /// not to redraw parts of the image that haven't changed. You can force a + /// redraw in case the image provided or its contents have changed. pub fn render( &mut self, state: &LayoutState, image: &mut [u8], [width, height]: [u32; 2], stride: u32, + force_redraw: bool, ) -> Option<(f32, f32)> { - let mut pixmap = PixmapMut::from_bytes(image, stride, height).unwrap(); + let mut frame_buffer = PixmapMut::from_bytes(image, stride, height).unwrap(); - // FIXME: .fill() once it's stable. - for b in pixmap.data_mut() { - *b = 0; + if stride != self.background.width() || height != self.background.height() { + self.background = Pixmap::new(stride, height).unwrap(); } - let mut backend = SoftwareBackend { - canvas: Canvas::from(pixmap), - }; + let new_resolution = + self.scene_manager + .update_scene(SkiaAllocator, (width as _, height as _), &state); + + let scene = self.scene_manager.scene(); + let rectangle = scene.rectangle(); + let rectangle = rectangle.as_deref().unwrap(); + + let bottom_layer_changed = scene.bottom_layer_changed(); + + let mut background = self.background.as_mut(); + + if bottom_layer_changed { + fill_background(scene, &mut background, width, height); + render_layer(&mut background, scene.bottom_layer(), rectangle); + } - self.renderer - .render(&mut backend, (width as _, height as _), &state) + let top_layer = scene.top_layer(); + + let (min_y, max_y) = calculate_bounds(top_layer); + let min_y = mem::replace(&mut self.min_y, min_y).min(min_y); + let max_y = mem::replace(&mut self.max_y, max_y).max(max_y); + + if force_redraw || bottom_layer_changed { + frame_buffer + .data_mut() + .copy_from_slice(background.data_mut()); + } else if min_y <= max_y { + let stride = 4 * stride as usize; + let min_y = stride * (min_y - 1.0) as usize; + let max_y = stride * ((max_y + 2.0) as usize).min(height as usize); + + frame_buffer.data_mut()[min_y..max_y] + .copy_from_slice(&background.data_mut()[min_y..max_y]); + } + + render_layer(&mut frame_buffer, top_layer, rectangle); + + new_resolution } } @@ -239,66 +224,62 @@ impl BorrowedSoftwareRenderer { /// surprisingly fast and can be considered the default renderer. There are two /// versions of the software renderer. This version of the software renderer /// owns the image it renders into. -pub struct SoftwareRenderer { - renderer: Renderer, Option>, - pixmap: Pixmap, +pub struct Renderer { + renderer: BorrowedRenderer, + frame_buffer: Pixmap, } -impl Default for SoftwareRenderer { +impl Default for Renderer { fn default() -> Self { Self::new() } } -impl SoftwareRenderer { +impl Renderer { /// Creates a new software renderer. pub fn new() -> Self { Self { - renderer: Renderer::new(), - pixmap: Pixmap::new(1, 1).unwrap(), + renderer: BorrowedRenderer::new(), + frame_buffer: Pixmap::new(1, 1).unwrap(), } } /// Renders the layout state provided with the chosen resolution. It may /// detect that the layout got resized. In that case it returns the new - /// ideal size. This is entirely a hint and can be ignored entirely. The - /// image is always rendered with the resolution provided. + /// ideal size. This is just a hint and can be ignored entirely. The image + /// is always rendered with the resolution provided. pub fn render(&mut self, state: &LayoutState, [width, height]: [u32; 2]) -> Option<(f32, f32)> { - if width != self.pixmap.width() || height != self.pixmap.height() { - self.pixmap = Pixmap::new(width, height).unwrap(); - } else { - // FIXME: .fill() once it's stable. - for b in self.pixmap.data_mut() { - *b = 0; - } + if width != self.frame_buffer.width() || height != self.frame_buffer.height() { + self.frame_buffer = Pixmap::new(width, height).unwrap(); } - let mut backend = SoftwareBackend { - canvas: Canvas::from(self.pixmap.as_mut()), - }; - - self.renderer - .render(&mut backend, (width as _, height as _), &state) + self.renderer.render( + state, + self.frame_buffer.data_mut(), + [width, height], + width, + false, + ) } /// Accesses the image as a byte slice of RGBA8 encoded pixels (red, green, /// blue, alpha with each channel being an u8). pub fn image_data(&self) -> &[u8] { - self.pixmap.data() + self.frame_buffer.data() } /// Turns the whole renderer into the underlying image buffer of RGBA8 /// encoded pixels (red, green, blue, alpha with each channel being an u8). pub fn into_image_data(self) -> Vec { - self.pixmap.take() + self.frame_buffer.take() } /// Accesses the image. pub fn image(&self) -> ImageBuffer, &[u8]> { ImageBuffer::from_raw( - self.pixmap.width(), - self.pixmap.height(), - self.pixmap.data(), + self.frame_buffer.width(), + self.frame_buffer.height(), + self.frame_buffer.data(), ) .unwrap() } @@ -306,10 +287,205 @@ impl SoftwareRenderer { /// Turns the whole renderer into the underlying image. pub fn into_image(self) -> RgbaImage { RgbaImage::from_raw( - self.pixmap.width(), - self.pixmap.height(), - self.pixmap.take(), + self.frame_buffer.width(), + self.frame_buffer.height(), + self.frame_buffer.take(), ) .unwrap() } } + +fn render_layer( + canvas: &mut PixmapMut<'_>, + layer: &[Entity], + rectangle: &Path, +) { + for entity in layer { + match entity { + Entity::FillPath(path, shader, transform) => { + if let Some(path) = path.as_deref() { + let shader = match shader { + FillShader::SolidColor(col) => Shader::SolidColor(convert_color(col)), + FillShader::VerticalGradient(top, bottom) => { + let bounds = path.bounds(); + LinearGradient::new( + Point::from_xy(0.0, bounds.top()), + Point::from_xy(0.0, bounds.bottom()), + vec![ + GradientStop::new(0.0, convert_color(top)), + GradientStop::new(1.0, convert_color(bottom)), + ], + SpreadMode::Pad, + tiny_skia::Transform::identity(), + ) + .unwrap() + } + FillShader::HorizontalGradient(left, right) => { + let bounds = path.bounds(); + LinearGradient::new( + Point::from_xy(bounds.left(), 0.0), + Point::from_xy(bounds.right(), 0.0), + vec![ + GradientStop::new(0.0, convert_color(left)), + GradientStop::new(1.0, convert_color(right)), + ], + SpreadMode::Pad, + tiny_skia::Transform::identity(), + ) + .unwrap() + } + }; + + canvas.fill_path( + path, + &Paint { + shader, + anti_alias: true, + ..Default::default() + }, + FillRule::Winding, + convert_transform(transform), + None, + ); + } + } + Entity::StrokePath(path, stroke_width, color, transform) => { + if let Some(path) = path.as_deref() { + canvas.stroke_path( + path, + &Paint { + shader: Shader::SolidColor(convert_color(color)), + anti_alias: true, + ..Default::default() + }, + &Stroke { + width: *stroke_width, + ..Default::default() + }, + convert_transform(transform), + None, + ); + } + } + Entity::Image(image, transform) => { + if let Some(image) = image.as_deref() { + canvas.fill_path( + rectangle, + &Paint { + shader: Pattern::new( + image.as_ref(), + SpreadMode::Pad, + FilterQuality::Bilinear, + 1.0, + tiny_skia::Transform::from_scale( + 1.0 / image.width() as f32, + 1.0 / image.height() as f32, + ), + ), + anti_alias: true, + ..Default::default() + }, + FillRule::Winding, + convert_transform(transform), + None, + ); + } + } + } + } +} + +fn fill_background( + scene: &Scene, + background: &mut PixmapMut<'_>, + width: u32, + height: u32, +) { + match scene.background() { + Some(shader) => match shader { + FillShader::SolidColor(color) => { + background + .pixels_mut() + .fill(convert_color(color).premultiply().to_color_u8()); + } + FillShader::VerticalGradient(top, bottom) => { + background.fill_rect( + Rect::from_xywh(0.0, 0.0, width as _, height as _).unwrap(), + &Paint { + shader: LinearGradient::new( + Point::from_xy(0.0, 0.0), + Point::from_xy(0.0, height as _), + vec![ + GradientStop::new(0.0, convert_color(top)), + GradientStop::new(1.0, convert_color(bottom)), + ], + SpreadMode::Pad, + tiny_skia::Transform::identity(), + ) + .unwrap(), + blend_mode: BlendMode::Source, + ..Default::default() + }, + tiny_skia::Transform::identity(), + None, + ); + } + FillShader::HorizontalGradient(left, right) => { + background.fill_rect( + Rect::from_xywh(0.0, 0.0, width as _, height as _).unwrap(), + &Paint { + shader: LinearGradient::new( + Point::from_xy(0.0, 0.0), + Point::from_xy(width as _, 0.0), + vec![ + GradientStop::new(0.0, convert_color(left)), + GradientStop::new(1.0, convert_color(right)), + ], + SpreadMode::Pad, + tiny_skia::Transform::identity(), + ) + .unwrap(), + blend_mode: BlendMode::Source, + ..Default::default() + }, + tiny_skia::Transform::identity(), + None, + ); + } + }, + None => background.data_mut().fill(0), + } +} + +fn calculate_bounds(layer: &[Entity]) -> (f32, f32) { + let (mut min_y, mut max_y) = (f32::INFINITY, f32::NEG_INFINITY); + for entity in layer.iter() { + match entity { + Entity::FillPath(path, _, transform) | Entity::StrokePath(path, _, _, transform) => { + if let Some(path) = &**path { + let [_, ky, _, sy, _, ty] = transform.to_array(); + let bounds = path.bounds(); + let (l, r, t, b) = + (bounds.left(), bounds.right(), bounds.top(), bounds.bottom()); + for &(x, y) in &[(l, t), (r, t), (l, b), (r, b)] { + let transformed_y = ky * x + sy * y + ty; + min_y = min_y.min(transformed_y); + max_y = max_y.max(transformed_y); + } + } + } + Entity::Image(image, transform) => { + if image.is_some() { + let [_, ky, _, sy, _, ty] = transform.to_array(); + let (l, r, t, b) = (0.0, 1.0, 0.0, 1.0); + for &(x, y) in &[(l, t), (r, t), (l, b), (r, b)] { + let transformed_y = ky * x + sy * y + ty; + min_y = min_y.min(transformed_y); + max_y = max_y.max(transformed_y); + } + } + } + } + } + (min_y, max_y) +} diff --git a/src/run/editor/mod.rs b/src/run/editor/mod.rs index cb2af349..db917858 100644 --- a/src/run/editor/mod.rs +++ b/src/run/editor/mod.rs @@ -5,15 +5,14 @@ //! kind of User Interface. use super::{ComparisonError, ComparisonResult}; -use crate::platform::prelude::*; -use crate::timing::ParseError as ParseTimeSpanError; use crate::{ comparison, + platform::prelude::*, settings::{CachedImageId, Image}, + timing::ParseError as ParseTimeSpanError, Run, Segment, Time, TimeSpan, TimingMethod, }; -use core::mem::swap; -use core::num::ParseIntError; +use core::{mem::swap, num::ParseIntError}; use snafu::{OptionExt, ResultExt}; pub mod cleaning; @@ -23,10 +22,12 @@ mod state; #[cfg(test)] mod tests; -pub use self::cleaning::SumOfBestCleaner; -pub use self::fuzzy_list::FuzzyList; -pub use self::segment_row::SegmentRow; -pub use self::state::{Buttons as ButtonsState, Segment as SegmentState, State}; +pub use self::{ + cleaning::SumOfBestCleaner, + fuzzy_list::FuzzyList, + segment_row::SegmentRow, + state::{Buttons as ButtonsState, Segment as SegmentState, SelectionState, State}, +}; /// Describes an Error that occurred while parsing a time. #[derive(Debug, snafu::Snafu)] @@ -167,6 +168,28 @@ impl Editor { self.selected_segments.push(index); } + /// Select all segments from the currently active segment to the segment at + /// the index provided. The segment at the index provided becomes the new + /// active segment. + /// + /// # Panics + /// + /// This panics if the index of the segment provided is out of bounds. + pub fn select_range(&mut self, index: usize) { + let active = self.active_segment_index(); + let range = if index < active { + index + 1..active + } else { + active + 1..index + }; + for i in range { + if !self.selected_segments.contains(&i) { + self.selected_segments.push(i); + } + } + self.select_additionally(index); + } + /// Selects the segment with the given index. All other segments are /// unselected. The segment chosen also becomes the active segment. /// diff --git a/src/run/editor/state.rs b/src/run/editor/state.rs index 822a3ab8..2500a98d 100644 --- a/src/run/editor/state.rs +++ b/src/run/editor/state.rs @@ -93,6 +93,13 @@ pub enum SelectionState { Active, } +impl SelectionState { + /// Returns `true` if the segment is selected. + pub const fn is_selected_or_active(&self) -> bool { + matches!(self, Self::Selected | Self::Active) + } +} + #[cfg(feature = "std")] impl State { /// Encodes the state object's information as JSON. diff --git a/src/run/mod.rs b/src/run/mod.rs index 17c6e799..87516b19 100644 --- a/src/run/mod.rs +++ b/src/run/mod.rs @@ -36,9 +36,12 @@ pub use run_metadata::{CustomVariable, RunMetadata}; pub use segment::Segment; pub use segment_history::SegmentHistory; -use crate::comparison::{default_generators, personal_best, ComparisonGenerator}; -use crate::platform::prelude::*; -use crate::{settings::Image, AtomicDateTime, Time, TimeSpan, TimingMethod}; +use crate::{ + comparison::{default_generators, personal_best, ComparisonGenerator}, + platform::prelude::*, + settings::Image, + AtomicDateTime, Time, TimeSpan, TimingMethod, +}; use alloc::borrow::Cow; #[cfg(not(feature = "std"))] use alloc::string::String as PathBuf; @@ -95,7 +98,7 @@ impl PartialEq for ComparisonGenerators { /// Error type for an invalid comparison name #[derive(PartialEq, Debug, snafu::Snafu)] pub enum ComparisonError { - /// Comparison name starts with "[Race]". + /// Comparison name starts with "\[Race\]". NameStartsWithRace, /// Comparison name is a duplicate. DuplicateName, diff --git a/src/settings/alignment.rs b/src/settings/alignment.rs index b554761f..e3f7c016 100644 --- a/src/settings/alignment.rs +++ b/src/settings/alignment.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; /// Describes the Alignment of the Title in the Title Component. -#[derive(Copy, Clone, Serialize, Deserialize)] +#[derive(Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum Alignment { /// Automatically align the title based on whether a game icon is shown. Auto, diff --git a/src/settings/color.rs b/src/settings/color.rs index 90629300..3a626908 100644 --- a/src/settings/color.rs +++ b/src/settings/color.rs @@ -1,10 +1,10 @@ use crate::platform::math::f32::abs; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -/// [`Color`]s can be used to describe what [`Color`] to use for visualizing -/// backgrounds, texts, lines and various other elements that are being shown. -/// They are stored as RGBA [`Color`]s with 32-bit float point numbers ranging from -/// 0.0 to 1.0 per channel. +/// [`Colors`](Color) can be used to describe what [`Color`] to use for +/// visualizing backgrounds, texts, lines and various other elements that are +/// being shown. They are stored as RGBA [`Colors`](Color) with 32-bit floating +/// point numbers ranging from 0.0 to 1.0 per channel. #[derive(Debug, Copy, Clone, Default, PartialEq)] #[repr(C)] pub struct Color { diff --git a/src/settings/font.rs b/src/settings/font.rs index c0217764..a59b55c1 100644 --- a/src/settings/font.rs +++ b/src/settings/font.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; /// that matches the settings most closely is chosen. The settings may be /// ignored entirely if the platform can't support different fonts, such as in a /// terminal. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Hash)] pub struct Font { /// The family name of the font to use. This corresponds with the /// `Typographic Family Name` (Name ID 16) in the name table of the font. If @@ -14,7 +14,9 @@ pub struct Font { /// is the one to choose. The subfamily is not specified at all, and instead /// a suitable subfamily is chosen based on the style, weight and stretch /// values. - /// https://docs.microsoft.com/en-us/typography/opentype/spec/name + /// + /// [`name — Naming Table` on Microsoft + /// Docs](https://docs.microsoft.com/en-us/typography/opentype/spec/name) /// /// This is to ensure the highest portability across various platforms. /// Platforms often select fonts very differently, so if necessary it is @@ -40,6 +42,12 @@ pub enum Style { Italic, } +impl Default for Style { + fn default() -> Self { + Self::Normal + } +} + impl Style { /// The value to assign to the `ital` variation axis. pub const fn value_for_italic(self) -> f32 { @@ -53,7 +61,9 @@ impl Style { /// The weight specifies the weight / boldness of a font. If there is no font /// with the exact weight value, a font with a similar weight is to be chosen /// based on an algorithm similar to this: -/// https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#Fallback_weights +/// +/// [`Fallback weights` on +/// MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#Fallback_weights) #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] #[serde(rename_all = "kebab-case")] pub enum Weight { @@ -81,6 +91,12 @@ pub enum Weight { ExtraBlack, } +impl Default for Weight { + fn default() -> Self { + Self::Normal + } +} + impl Weight { /// The numeric value of the weight. pub const fn value(self) -> f32 { @@ -103,7 +119,9 @@ impl Weight { /// The stretch specifies how wide a font should be. For example, it may make /// sense to reduce the stretch of a font to ensure split names are not cut off. /// A font with a stretch value that is close is to be selected. -/// https://developer.mozilla.org/en-US/docs/Web/CSS/font-stretch#Font_face_selection +/// +/// [`Font face selection` on +/// MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/font-stretch#Font_face_selection) #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] #[serde(rename_all = "kebab-case")] pub enum Stretch { @@ -127,6 +145,12 @@ pub enum Stretch { UltraExpanded, } +impl Default for Stretch { + fn default() -> Self { + Self::Normal + } +} + impl Stretch { /// The percentage the font is stretched by (50% to 200%). pub const fn percentage(self) -> f32 { diff --git a/src/settings/gradient.rs b/src/settings/gradient.rs index bc16921e..f8f53728 100644 --- a/src/settings/gradient.rs +++ b/src/settings/gradient.rs @@ -23,7 +23,7 @@ impl Default for Gradient { /// Describes an extended form of a gradient, specifically made for use with /// lists. It allows specifying different coloration for the rows in a list. -#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)] pub enum ListGradient { /// Use the same gradient for every row in the list. Same(Gradient), diff --git a/src/settings/value.rs b/src/settings/value.rs index 1d124a5d..7123b001 100644 --- a/src/settings/value.rs +++ b/src/settings/value.rs @@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize}; /// Describes a setting's value. Such a value can be of a variety of different /// types. -#[derive(derive_more::From, Serialize, Deserialize)] +#[derive(Clone, PartialEq, derive_more::From, Serialize, Deserialize)] pub enum Value { /// A boolean value. Bool(bool), @@ -24,8 +24,6 @@ pub enum Value { String(String), /// An optional string. OptionalString(Option), - /// A floating point number. - Float(f64), /// An accuracy, describing how many digits to show for the fractional part /// of a time. Accuracy(Accuracy), @@ -113,14 +111,6 @@ impl Value { } } - /// Tries to convert the value into a floating point number. - pub fn into_float(self) -> Result { - match self { - Value::Float(v) => Ok(v), - _ => Err(Error::WrongType), - } - } - /// Tries to convert the value into an accuracy. pub fn into_accuracy(self) -> Result { match self { @@ -273,12 +263,6 @@ impl From for Option { } } -impl From for f64 { - fn from(value: Value) -> Self { - value.into_float().unwrap() - } -} - impl From for Accuracy { fn from(value: Value) -> Self { value.into_accuracy().unwrap() diff --git a/src/timing/timer_phase.rs b/src/timing/timer_phase.rs index 8c9393ab..3fc66ac2 100644 --- a/src/timing/timer_phase.rs +++ b/src/timing/timer_phase.rs @@ -12,3 +12,25 @@ pub enum TimerPhase { /// There's an active attempt that is currently paused. Paused = 3, } + +impl TimerPhase { + /// Returns `true` if the value is [`TimerPhase::NotRunning`]. + pub const fn is_not_running(&self) -> bool { + matches!(self, Self::NotRunning) + } + + /// Returns `true` if the value is [`TimerPhase::Running`]. + pub const fn is_running(&self) -> bool { + matches!(self, Self::Running) + } + + /// Returns `true` if the value is [`TimerPhase::Ended`]. + pub const fn is_ended(&self) -> bool { + matches!(self, Self::Ended) + } + + /// Returns `true` if the value is [`TimerPhase::Paused`]. + pub const fn is_paused(&self) -> bool { + matches!(self, Self::Paused) + } +} diff --git a/tests/rendering.rs b/tests/rendering.rs index cde01908..79e2bbb6 100644 --- a/tests/rendering.rs +++ b/tests/rendering.rs @@ -10,7 +10,7 @@ use img_hash::{HasherConfig, ImageHash}; use livesplit_core::{ component::{self, timer}, layout::{self, Component, ComponentState, Layout, LayoutDirection, LayoutState}, - rendering::software::SoftwareRenderer, + rendering::software::Renderer, run::parser::{livesplit, llanfair, wsplit}, Run, Segment, TimeSpan, Timer, TimingMethod, }; @@ -240,7 +240,7 @@ fn check(state: &LayoutState, expected_hash_data: &str, name: &str) { #[track_caller] fn check_dims(state: &LayoutState, dims: [u32; 2], expected_hash_data: &str, name: &str) { - let mut renderer = SoftwareRenderer::new(); + let mut renderer = Renderer::new(); renderer.render(state, dims); let image = renderer.into_image();