Skip to content

Commit

Permalink
Introduce layers in the Renderer
Browse files Browse the repository at this point in the history
There are a lot of elements that are basically almost entirely static on
the layout. Other elements such as the timer are frequently changing. In
the browser we already noticed that by pushing the timer onto its own
layer, you get a fairly big performance boost out of it.

Here's that Pull Request:

LiveSplit/LiveSplitOne#378

We want to apply a similar idea to the native renderer. In order to do
this we first introduce flags in the layout state that mark whether
a component or a part of a component is at the specific time considered
frequently changing. By doing this not only does the native renderer
benefit from it, but other renderers such as the web version can also
make decisions based on that, as opposed to only applying the
optimization to the timer component.

With all the components providing these flags, the renderer can now
split up the frame into a bottom layer and a top layer where all the
frequently changing elements are rendered onto the top layer and all the
other elements rendered onto the bottom layer. In fact the top layer is
actually the frame buffer and it simply gets cleared by copying over
regions from the bottom layer. That way there is no additional
compositing necessary.

Additionally the renderer is now more of a scene manager that manages
entities in a scene rather than directly emitting draw calls to a
backend. The scene is a data structure that allows traversing the two
layers in any way a renderer (i.e. what used to be a backend) would want
to render them out. So the whole design is a lot more decoupled now.
  • Loading branch information
CryZe committed Jun 8, 2021
1 parent 17e36f7 commit 6d339b7
Show file tree
Hide file tree
Showing 53 changed files with 1,754 additions and 908 deletions.
5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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 }
Expand All @@ -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"]
Expand Down
41 changes: 23 additions & 18 deletions benches/dummy_rendering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, SharedOwnership},
run::parser::livesplit,
Run, Segment, TimeSpan, Timer, TimingMethod,
};
Expand All @@ -14,31 +14,36 @@ cfg_if::cfg_if! {

struct Dummy;

impl SharedOwnership for Dummy {
fn share(&self) -> Self {
Self
}
}

impl PathBuilder<Dummy> for Dummy {
type Path = ();
type Path = Dummy;

fn move_to(&mut self, _: f32, _: f32) {}
fn line_to(&mut self, _: f32, _: f32) {}
fn quad_to(&mut self, _: f32, _: f32, _: f32, _: f32) {}
fn curve_to(&mut self, _: f32, _: f32, _: f32, _: f32, _: f32, _: f32) {}
fn close(&mut self) {}
fn finish(self, _: &mut Dummy) -> Self::Path {}
fn finish(self, _: &mut Dummy) -> Self::Path {
Dummy
}
}

impl Backend for Dummy {
impl ResourceAllocator for Dummy {
type PathBuilder = Dummy;
type Path = ();
type Image = ();
type Path = Dummy;
type Image = Dummy;

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 create_image(&mut self, _: u32, _: u32, _: &[u8]) -> Self::Image {
Dummy
}
}

fn default(c: &mut Criterion) {
Expand All @@ -54,10 +59,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))
});
}

Expand All @@ -73,10 +78,10 @@ cfg_if::cfg_if! {
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))
});
}

Expand Down
6 changes: 3 additions & 3 deletions benches/software_rendering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand All @@ -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]))
Expand All @@ -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]))
Expand Down
3 changes: 1 addition & 2 deletions capi/bind_gen/src/typescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];

Expand Down Expand Up @@ -598,7 +598,6 @@ export type SettingsDescriptionValueJson =
{ Int: number } |
{ String: string } |
{ OptionalString: string | null } |
{ Float: number } |
{ Accuracy: AccuracyJson } |
{ DigitsFormat: DigitsFormatJson } |
{ OptionalTimingMethod: TimingMethodJson | null } |
Expand Down
34 changes: 29 additions & 5 deletions capi/src/layout_editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<LayoutEditor>;
Expand Down Expand Up @@ -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.
Expand Down
6 changes: 0 additions & 6 deletions capi/src/setting_value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,6 @@ pub extern "C" fn SettingValue_from_optional_empty_string() -> OwnedSettingValue
Box::new(None::<String>.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, <NULL> is returned.
#[no_mangle]
Expand Down
22 changes: 13 additions & 9 deletions capi/src/software_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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,
);
}
18 changes: 13 additions & 5 deletions src/analysis/current_pace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TimeSpan> {
pub fn calculate(timer: &Snapshot<'_>, comparison: &str) -> (Option<TimeSpan>, bool) {
let timing_method = timer.current_timing_method();
let last_segment = timer.run().segments().last().unwrap();

Expand All @@ -21,20 +21,28 @@ pub fn calculate(timer: &Snapshot<'_>, comparison: &str) -> Option<TimeSpan> {
)
.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),
}
}
9 changes: 6 additions & 3 deletions src/component/current_comparison.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
22 changes: 13 additions & 9 deletions src/component/current_pace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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)
};
Expand Down Expand Up @@ -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.
Expand Down
Loading

0 comments on commit 6d339b7

Please sign in to comment.