Repository: LiveSplit/livesplit-core
Pull request #430 (2021-06-08 20:27:41)

Introduce Layers in the Renderer
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:

https://github.com/LiveSplit/LiveSplitOne/pull/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.


Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 0d38edb..0cd48a5 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 13ed02a..23747b8 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 9a9854d..cc01924 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 711e47d..25afb20 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 ed1a9b0..f464455 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 157bd4a..8958526 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<LayoutEditor>;
@@ -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 94ba6ae..1201bab 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::<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]
diff --git a/capi/src/software_renderer.rs b/capi/src/software_renderer.rs
index 78959c8..483cf04 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 fdf2319..8e21a0e 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<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();
 
@@ -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),
     }
 }
diff --git a/src/analysis/pb_chance/mod.rs b/src/analysis/pb_chance/mod.rs
index 73ac600..35363a5 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/component/current_comparison.rs b/src/component/current_comparison.rs
index b58a481..5c62c05 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 7ed52a1..962211b 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 bb96eb4..d4148ed 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 ff834af..b5d9f9f 100644
--- a/src/component/key_value.rs
+++ b/src/component/key_value.rs
@@ -33,6 +33,9 @@ pub struct State {
     pub key_abbreviations: Vec<Cow<'static, str>>,
     /// 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 c76754c..e7020cc 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 09342df..a27fba1 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 852ed0c..c7a73d0 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 24e0ccc..86208fb 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 b3201b2..66f4003 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<usize>,
     method: TimingMethod,
     comparison: &str,
-) -> Option<(Option<TimeSpan>, SemanticColor, ColumnFormatter)> {
+) -> Option<((Option<TimeSpan>, 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 58f7161..41d955c 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 0593db0..a132911 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 6726cff..145a697 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 14a4e84..e83a094 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 fbb0507..381e534 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 af4d653..0c820e9 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 2a54637..35bead6 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<B: Backend>(
+pub(in crate::rendering) fn render<B: ResourceAllocator>(
     context: &mut RenderContext<'_, B>,
     [width, height]: [f32; 2],
     component: &State,
@@ -22,9 +26,6 @@ pub(in crate::rendering) fn render<B: Backend>(
     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<B: Backend>(
     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<B: Backend>(
     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 3d788b2..4a0f05f 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 b21ac1d..7bada16 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 b8574af..ca69fda 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<A: ResourceAllocator>(
+    context: &mut RenderContext<'_, A>,
+    icons: &mut IconCache<A::Image>,
+    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 782a175..aef82f3 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 90c4ed3..5a3988b 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<B: Backend>(
+pub(in crate::rendering) fn render<B: ResourceAllocator>(
     context: &mut RenderContext<'_, B>,
     [width, height]: [f32; 2],
     component: &State,
@@ -65,11 +70,7 @@ pub(in crate::rendering) fn render<B: Backend>(
         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<B: Backend>(
                 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<B: Backend>(
                 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 160184a..5dcfeac 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 e370523..549a296 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 474a3e4..5a55b76 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<B: Backend>(
+pub(in crate::rendering) fn render<B: ResourceAllocator>(
     context: &mut RenderContext<'_, B>,
     [width, height]: [f32; 2],
     component: &State,
@@ -19,9 +24,6 @@ pub(in crate::rendering) fn render<B: Backend>(
     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<B: Backend>(
     };
     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 0000000..851fef4
--- /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 0000000..086a9e6
--- /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<P, I> {
+    /// 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<P>, 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<P>, f32, Rgba, Transform),
+    /// An image.
+    Image(Handle<I>, Transform),
+}
+
+pub fn calculate_hash<P, I>(background: &Option<FillShader>, entities: &[Entity<P, I>]) -> 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<P, I> Hash for Entity<P, I> {
+    fn hash<H: Hasher>(&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 0000000..2320125
--- /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<P> {
+    #[cfg(feature = "font-loading")]
+    setting: Option<crate::settings::Font>,
+    pub font: Font<'static>,
+    pub glyph_cache: GlyphCache<P>,
+}
+
+impl<P: SharedOwnership> CachedFont<P> {
+    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<crate::settings::Font>,
+        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<P> {
+    pub buffer: Option<UnicodeBuffer>,
+    pub timer: CachedFont<P>,
+    pub times: CachedFont<P>,
+    pub text: CachedFont<P>,
+}
+
+impl<P: SharedOwnership> FontCache<P> {
+    pub fn new() -> Option<Self> {
+        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 ffa0545..c96a23e 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<B, PB>(PB, PhantomData<fn(&mut B)>);
+struct GlyphBuilder<B, PB>(PB, PhantomData<fn(&mut B)>);
 
-impl<B, PB: super::super::PathBuilder<B>> OutlineBuilder for PathBuilder<B, PB> {
+impl<B, PB: PathBuilder<B>> OutlineBuilder for GlyphBuilder<B, PB> {
     fn move_to(&mut self, x: f32, y: f32) {
         self.0.move_to(x, -y);
     }
@@ -27,7 +30,7 @@ impl<B, PB: super::super::PathBuilder<B>> OutlineBuilder for PathBuilder<B, PB>
 }
 
 pub struct GlyphCache<P> {
-    glyphs: HashMap<u32, Vec<(Option<Color>, P)>>,
+    glyphs: HashMap<u32, Vec<(Option<Color>, Handle<P>)>>,
 }
 
 impl<P> Default for GlyphCache<P> {
@@ -38,32 +41,28 @@ impl<P> Default for GlyphCache<P> {
     }
 }
 
-impl<P> GlyphCache<P> {
+impl<P: SharedOwnership> GlyphCache<P> {
     pub fn new() -> Self {
         Default::default()
     }
 
     #[cfg(feature = "font-loading")]
-    pub fn clear(&mut self, backend: &mut impl Backend<Path = P>) {
-        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<Path = P>,
-    ) -> &[(Option<Color>, P)] {
+        handles: &mut Handles<impl ResourceAllocator<Path = P>>,
+    ) -> &[(Option<Color>, Handle<P>)] {
         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 e1592ce..4d7ec50 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<B: Backend>(
+pub fn render<A: ResourceAllocator>(
     layout: impl IntoIterator<Item = PositionedGlyph>,
     shader: FillShader,
     font: &ScaledFont<'_>,
-    glyph_cache: &mut GlyphCache<B::Path>,
+    glyph_cache: &mut GlyphCache<A::Path>,
     transform: &Transform,
-    backend: &mut B,
+    handles: &mut Handles<A>,
+    entities: &mut Vec<Entity<A::Path, A::Image>>,
 ) {
     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 9c69e02..8e0c599 100644
--- a/src/rendering/icon.rs
+++ b/src/rendering/icon.rs
@@ -1,4 +1,6 @@
+use super::resource::Handle;
+
 pub struct Icon<T> {
-    pub image: T,
+    pub image: Handle<T>,
     pub aspect_ratio: f32,
 }
diff --git a/src/rendering/mod.rs b/src/rendering/mod.rs
index 5724a6e..65abb78 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<f32, UnknownUnit, UnknownUnit>;
 
-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<B: ?Sized> {
-    /// 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<Self, Path = Self::Path>;
-    /// 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<P, I> {
-    #[cfg(feature = "font-loading")]
-    timer_font_setting: Option<crate::settings::Font>,
-    timer_font: Font<'static>,
-    timer_glyph_cache: GlyphCache<P>,
-    #[cfg(feature = "font-loading")]
-    times_font_setting: Option<crate::settings::Font>,
-    times_font: Font<'static>,
-    times_glyph_cache: GlyphCache<P>,
-    #[cfg(feature = "font-loading")]
-    text_font_setting: Option<crate::settings::Font>,
-    text_font: Font<'static>,
-    text_glyph_cache: GlyphCache<P>,
-    rectangle: Option<P>,
-    cached_size: Option<CachedSize>,
+/// 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<P, I> {
+    scene: Scene<P, I>,
+    fonts: FontCache<P>,
     icons: IconCache<I>,
-    text_buffer: Option<UnicodeBuffer>,
+    next_id: usize,
+    cached_size: Option<CachedSize>,
 }
 
 struct IconCache<I> {
@@ -267,142 +155,75 @@ struct IconCache<I> {
     detailed_timer_icon: Option<Icon<I>>,
 }
 
-impl<P, I> Default for Renderer<P, I> {
-    fn default() -> Self {
-        Self::new()
-    }
-}
+impl<P: SharedOwnership, I: SharedOwnership> SceneManager<P, I> {
+    /// Creates a new scene manager.
+    pub fn new(mut allocator: impl ResourceAllocator<Path = P, Image = I>) -> 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<P, I> Renderer<P, I> {
-    /// 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<B: Backend<Path = P, Image = I>>(
+    /// Accesses the [`Scene`] in order to render the [`Entities`](Entity).
+    pub fn scene(&self) -> &Scene<P, I> {
+        &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<A: ResourceAllocator<Path = P, Image = I>>(
         &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<B: Backend<Path = P, Image = I>>(
+    fn render_vertical(
         &mut self,
-        backend: &mut B,
+        allocator: impl ResourceAllocator<Path = P, Image = I>,
         resolution: (f32, f32),
         state: &LayoutState,
     ) -> Option<(f32, f32)> {
-        let total_height = state.components.iter().map(component_height).sum::<f32>();
+        let total_height = component::layout_height(state);
 
         let cached_total_size = self
             .cached_size
@@ -431,24 +252,12 @@ impl<P, I> Renderer<P, I> {
         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<P, I> Renderer<P, I> {
         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<B: Backend<Path = P, Image = I>>(
+    fn render_horizontal(
         &mut self,
-        backend: &mut B,
+        allocator: impl ResourceAllocator<Path = P, Image = I>,
         resolution: (f32, f32),
         state: &LayoutState,
     ) -> Option<(f32, f32)> {
-        let total_width = state.components.iter().map(component_width).sum::<f32>();
+        let total_width = component::layout_width(state);
 
         let cached_total_size = self
             .cached_size
@@ -509,24 +320,12 @@ impl<P, I> Renderer<P, I> {
         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<P, I> Renderer<P, I> {
         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<B: Backend>(
-    context: &mut RenderContext<'_, B>,
-    icons: &mut IconCache<B::Image>,
-    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<B::Path>,
-    timer_font: &'b Font<'static>,
-    timer_glyph_cache: &'b mut GlyphCache<B::Path>,
-    text_font: &'b Font<'static>,
-    text_glyph_cache: &'b mut GlyphCache<B::Path>,
-    times_font: &'b Font<'static>,
-    times_glyph_cache: &'b mut GlyphCache<B::Path>,
-    text_buffer: &'b mut Option<UnicodeBuffer>,
+    handles: Handles<A>,
+    scene: &'b mut Scene<A::Path, A::Image>,
+    fonts: &'b mut FontCache<A::Path>,
 }
 
-impl<B: Backend> RenderContext<'_, B> {
+impl<A: ResourceAllocator> RenderContext<'_, A> {
+    fn rectangle(&self) -> Handle<A::Path> {
+        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<A::Path>, color: Color) {
+        self.scene
+            .top_layer_mut()
+            .push(Entity::FillPath(path, solid(&color), self.transform));
     }
 
-    fn create_icon(&mut self, image_data: &[u8]) -> Option<Icon<B::Image>> {
+    fn top_layer_stroke_path(&mut self, path: Handle<A::Path>, 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<Icon<A::Image>> {
         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<B: Backend> 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<B::Image>,
+        icon: &Icon<A::Image>,
     ) {
         let box_aspect_ratio = width / height;
         let aspect_ratio_diff = box_aspect_ratio / icon.aspect_ratio;
@@ -706,25 +477,9 @@ impl<B: Backend> 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<B: Backend> 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<B: Backend> 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<B: Backend> 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<B: Backend> 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<B: Backend> 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<B: Backend> 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<B: Backend> 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<B: Backend> 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<FillShader> {
 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 0000000..173aa2f
--- /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<Self, Path = Self::Path>;
+    /// 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<A: ?Sized> {
+    /// 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>(PB);
+
+impl<A: ResourceAllocator> PathBuilder<&mut A> for MutPathBuilder<A::PathBuilder> {
+    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<A: ResourceAllocator> ResourceAllocator for &mut A {
+    type PathBuilder = MutPathBuilder<A::PathBuilder>;
+    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 0000000..4605261
--- /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<A> {
+    next_id: usize,
+    allocator: A,
+}
+
+impl<A> Handles<A> {
+    pub const fn new(next_id: usize, allocator: A) -> Self {
+        Self { next_id, allocator }
+    }
+
+    pub fn next<T>(&mut self, element: T) -> Handle<T> {
+        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: ResourceAllocator>(A::PathBuilder);
+
+impl<A: ResourceAllocator> PathBuilder<Handles<A>> for HandlePathBuilder<A> {
+    type Path = Handle<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, handles: &mut Handles<A>) -> Self::Path {
+        let path = self.0.finish(&mut handles.allocator);
+        handles.next(path)
+    }
+}
+
+impl<A: ResourceAllocator> ResourceAllocator for Handles<A> {
+    type PathBuilder = HandlePathBuilder<A>;
+    type Path = Handle<A::Path>;
+    type Image = Handle<A::Image>;
+
+    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<T> {
+    id: usize,
+    inner: T,
+}
+
+impl<T: SharedOwnership> SharedOwnership for Handle<T> {
+    fn share(&self) -> Self {
+        Self {
+            id: self.id,
+            inner: self.inner.share(),
+        }
+    }
+}
+
+impl<T: SharedOwnership> Handle<T> {
+    /// 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<T> Deref for Handle<T> {
+    type Target = T;
+
+    fn deref(&self) -> &Self::Target {
+        &self.inner
+    }
+}
+
+impl<T> Hash for Handle<T> {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        self.id.hash(state)
+    }
+}
+
+impl<T> Eq for Handle<T> {}
+
+impl<T> PartialEq for Handle<T> {
+    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 0000000..1e6198f
--- /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 0000000..59f23a4
--- /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<T: ?Sized> SharedOwnership for Rc<T> {
+    fn share(&self) -> Self {
+        self.clone()
+    }
+}
+
+impl<T: ?Sized> SharedOwnership for Arc<T> {
+    fn share(&self) -> Self {
+        self.clone()
+    }
+}
+
+impl<T: SharedOwnership> SharedOwnership for Option<T> {
+    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 0000000..548f3df
--- /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<P, I> {
+    rectangle: Handle<P>,
+    background: Option<FillShader>,
+    bottom_hash: u64,
+    bottom_layer_changed: bool,
+    bottom_layer: Vec<Entity<P, I>>,
+    top_layer: Vec<Entity<P, I>>,
+}
+
+impl<P: SharedOwnership, I: SharedOwnership> Scene<P, I> {
+    /// Creates a new scene with the rectangle provided to use for placing
+    /// rectangle entities.
+    pub fn new(rectangle: Handle<P>) -> Self {
+        Self {
+            rectangle,
+            background: None,
+            bottom_hash: calculate_hash::<P, I>(&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<FillShader> {
+        &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<P, I>] {
+        &self.bottom_layer
+    }
+
+    /// Get a reference to the scene's top [`Layer`].
+    pub fn top_layer(&self) -> &[Entity<P, I>] {
+        &self.top_layer
+    }
+
+    /// Get access to the rectangle resource the scene stores.
+    pub fn rectangle(&self) -> Handle<P> {
+        self.rectangle.share()
+    }
+
+    /// Set the bottom [`Layer's`](Layer) background.
+    pub fn set_background(&mut self, background: Option<FillShader>) {
+        self.background = background;
+    }
+
+    /// Get a mutable reference to the scene's bottom [`Layer`].
+    pub fn bottom_layer_mut(&mut self) -> &mut Vec<Entity<P, I>> {
+        &mut self.bottom_layer
+    }
+
+    /// Get a mutable reference to the scene's top [`Layer`].
+    pub fn top_layer_mut(&mut self) -> &mut Vec<Entity<P, I>> {
+        &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<Entity<P, I>> {
+        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 5a4e0fd..7604def 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<SoftwareBackend<'_>> for SkiaBuilder {
-    type Path = Option<Path>;
+type SkiaPath = Option<UnsafeRc<Path>>;
+type SkiaImage = Option<UnsafeRc<Pixmap>>;
+
+impl resource::PathBuilder<SkiaAllocator> 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<SoftwareBackend<'_>> 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<Path>;
-    type Image = Option<Pixmap>;
+    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<Path>, Option<Pixmap>>,
+pub struct BorrowedRenderer {
+    scene_manager: SceneManager<SkiaPath, SkiaImage>,
+    background: Pixmap,
+    min_y: f32,
+    max_y: f32,
+}
+
+struct UnsafeRc<T>(Rc<T>);
+
+impl<T: Send + Sync> Deref for UnsafeRc<T> {
+    type Target = T;
+
+    fn deref(&self) -> &Self::Target {
+        &*self.0
+    }
 }
 
-impl Default for BorrowedSoftwareRenderer {
+impl<T> UnsafeRc<T> {
+    fn new(val: T) -> Self {
+        Self(Rc::new(val))
+    }
+}
+
+impl<T: Send + Sync> SharedOwnership for UnsafeRc<T> {
+    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<T: Send + Sync> Send for UnsafeRc<T> {}
+
+// 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<T: Send + Sync> Sync for UnsafeRc<T> {}
+
+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<Path>, Option<Pixmap>>,
-    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<u8> {
-        self.pixmap.take()
+        self.frame_buffer.take()
     }
 
     /// Accesses the image.
     pub fn image(&self) -> ImageBuffer<image::Rgba<u8>, &[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<SkiaPath, SkiaImage>],
+    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<SkiaPath, SkiaImage>,
+    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<SkiaPath, SkiaImage>]) -> (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 cb2af34..db91785 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 822a3ab..2500a98 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 17c6e79..87516b1 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 b554761..e3f7c01 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 9062930..3a62690 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 c021776..a59b55c 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 bc16921..f8f5372 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 1d124a5..7123b00 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<String>),
-    /// 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<f64> {
-        match self {
-            Value::Float(v) => Ok(v),
-            _ => Err(Error::WrongType),
-        }
-    }
-
     /// Tries to convert the value into an accuracy.
     pub fn into_accuracy(self) -> Result<Accuracy> {
         match self {
@@ -273,12 +263,6 @@ impl From<Value> for Option<String> {
     }
 }
 
-impl From<Value> for f64 {
-    fn from(value: Value) -> Self {
-        value.into_float().unwrap()
-    }
-}
-
 impl From<Value> 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 8c9393a..3fc66ac 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)
+    }
+}


Test patch:
diff --git a/src/analysis/pb_chance/tests.rs b/src/analysis/pb_chance/tests.rs
index 9121f1b..8668a8d 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/tests/rendering.rs b/tests/rendering.rs
index cde0190..79e2bbb 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();