From 6d3e1d835e1688fbc58622a03a784ed25ed3f0e1 Mon Sep 17 00:00:00 2001
From: Héctor Ramón Jiménez <hector@hecrj.dev>
Date: Fri, 5 Apr 2024 23:59:21 +0200
Subject: Decouple caching from layering and simplify everything

---
 wgpu/src/geometry.rs  | 188 ++++++++-----------
 wgpu/src/image/mod.rs |   7 +-
 wgpu/src/layer.rs     | 258 +++++++++++++-------------
 wgpu/src/lib.rs       | 444 +++++++++++++-------------------------------
 wgpu/src/quad.rs      | 188 ++++---------------
 wgpu/src/text.rs      | 427 ++++++++++++++++++++++--------------------
 wgpu/src/triangle.rs  | 503 +++++++++++++++++++++++++-------------------------
 7 files changed, 855 insertions(+), 1160 deletions(-)

(limited to 'wgpu/src')

diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs
index 611e81f1..c8c350c5 100644
--- a/wgpu/src/geometry.rs
+++ b/wgpu/src/geometry.rs
@@ -6,40 +6,44 @@ use crate::core::{
 use crate::graphics::color;
 use crate::graphics::geometry::fill::{self, Fill};
 use crate::graphics::geometry::{
-    self, LineCap, LineDash, LineJoin, Path, Stroke, Style, Text,
+    self, LineCap, LineDash, LineJoin, Path, Stroke, Style,
 };
 use crate::graphics::gradient::{self, Gradient};
 use crate::graphics::mesh::{self, Mesh};
-use crate::graphics::{self, Cached};
-use crate::layer;
+use crate::graphics::{self, Cached, Text};
 use crate::text;
+use crate::triangle;
 
 use lyon::geom::euclid;
 use lyon::tessellation;
 
 use std::borrow::Cow;
-use std::cell::RefCell;
-use std::rc::Rc;
 
 /// A frame for drawing some geometry.
 #[allow(missing_debug_implementations)]
 pub struct Frame {
-    size: Size,
+    clip_bounds: Rectangle,
     buffers: BufferStack,
-    layers: Vec<layer::Live>,
-    text: text::Batch,
+    meshes: Vec<Mesh>,
+    text: Vec<Text>,
     transforms: Transforms,
     fill_tessellator: tessellation::FillTessellator,
     stroke_tessellator: tessellation::StrokeTessellator,
 }
 
 pub enum Geometry {
-    Live(Vec<layer::Live>),
-    Cached(Rc<[Rc<RefCell<layer::Cached>>]>),
+    Live { meshes: Vec<Mesh>, text: Vec<Text> },
+    Cached(Cache),
+}
+
+#[derive(Clone)]
+pub struct Cache {
+    pub meshes: triangle::Cache,
+    pub text: text::Cache,
 }
 
 impl Cached for Geometry {
-    type Cache = Rc<[Rc<RefCell<layer::Cached>>]>;
+    type Cache = Cache;
 
     fn load(cache: &Self::Cache) -> Self {
         Geometry::Cached(cache.clone())
@@ -47,31 +51,18 @@ impl Cached for Geometry {
 
     fn cache(self, previous: Option<Self::Cache>) -> Self::Cache {
         match self {
-            Self::Live(live) => {
-                let mut layers = live.into_iter();
-
-                let mut new: Vec<_> = previous
-                    .map(|previous| {
-                        previous
-                            .iter()
-                            .cloned()
-                            .zip(layers.by_ref())
-                            .map(|(cached, live)| {
-                                cached.borrow_mut().update(live);
-                                cached
-                            })
-                            .collect()
-                    })
-                    .unwrap_or_default();
-
-                new.extend(
-                    layers
-                        .map(layer::Live::into_cached)
-                        .map(RefCell::new)
-                        .map(Rc::new),
-                );
-
-                Rc::from(new)
+            Self::Live { meshes, text } => {
+                if let Some(mut previous) = previous {
+                    previous.meshes.update(meshes);
+                    previous.text.update(text);
+
+                    previous
+                } else {
+                    Cache {
+                        meshes: triangle::Cache::new(meshes),
+                        text: text::Cache::new(text),
+                    }
+                }
             }
             Self::Cached(cache) => cache,
         }
@@ -81,69 +72,26 @@ impl Cached for Geometry {
 impl Frame {
     /// Creates a new [`Frame`] with the given [`Size`].
     pub fn new(size: Size) -> Frame {
+        Self::with_clip(Rectangle::with_size(size))
+    }
+
+    /// Creates a new [`Frame`] with the given clip bounds.
+    pub fn with_clip(bounds: Rectangle) -> Frame {
         Frame {
-            size,
+            clip_bounds: bounds,
             buffers: BufferStack::new(),
-            layers: Vec::new(),
-            text: text::Batch::new(),
+            meshes: Vec::new(),
+            text: Vec::new(),
             transforms: Transforms {
                 previous: Vec::new(),
-                current: Transform(lyon::math::Transform::identity()),
+                current: Transform(lyon::math::Transform::translation(
+                    bounds.x, bounds.y,
+                )),
             },
             fill_tessellator: tessellation::FillTessellator::new(),
             stroke_tessellator: tessellation::StrokeTessellator::new(),
         }
     }
-
-    fn into_layers(mut self) -> Vec<layer::Live> {
-        if !self.text.is_empty() || !self.buffers.stack.is_empty() {
-            let clip_bounds = Rectangle::with_size(self.size);
-            let transformation = Transformation::IDENTITY;
-
-            // TODO: Generate different meshes for different transformations (?)
-            // Instead of transforming each path
-            let meshes = self
-                .buffers
-                .stack
-                .into_iter()
-                .filter_map(|buffer| match buffer {
-                    Buffer::Solid(buffer) if !buffer.indices.is_empty() => {
-                        Some(Mesh::Solid {
-                            buffers: mesh::Indexed {
-                                vertices: buffer.vertices,
-                                indices: buffer.indices,
-                            },
-                            transformation: Transformation::IDENTITY,
-                            size: self.size,
-                        })
-                    }
-                    Buffer::Gradient(buffer) if !buffer.indices.is_empty() => {
-                        Some(Mesh::Gradient {
-                            buffers: mesh::Indexed {
-                                vertices: buffer.vertices,
-                                indices: buffer.indices,
-                            },
-                            transformation: Transformation::IDENTITY,
-                            size: self.size,
-                        })
-                    }
-                    _ => None,
-                })
-                .collect();
-
-            let layer = layer::Live {
-                bounds: Some(clip_bounds),
-                transformation,
-                meshes,
-                text: self.text,
-                ..layer::Live::default()
-            };
-
-            self.layers.push(layer);
-        }
-
-        self.layers
-    }
 }
 
 impl geometry::frame::Backend for Frame {
@@ -151,22 +99,22 @@ impl geometry::frame::Backend for Frame {
 
     #[inline]
     fn width(&self) -> f32 {
-        self.size.width
+        self.clip_bounds.width
     }
 
     #[inline]
     fn height(&self) -> f32 {
-        self.size.height
+        self.clip_bounds.height
     }
 
     #[inline]
     fn size(&self) -> Size {
-        self.size
+        self.clip_bounds.size()
     }
 
     #[inline]
     fn center(&self) -> Point {
-        Point::new(self.size.width / 2.0, self.size.height / 2.0)
+        Point::new(self.clip_bounds.width / 2.0, self.clip_bounds.height / 2.0)
     }
 
     fn fill(&mut self, path: &Path, fill: impl Into<Fill>) {
@@ -269,7 +217,7 @@ impl geometry::frame::Backend for Frame {
         .expect("Stroke path");
     }
 
-    fn fill_text(&mut self, text: impl Into<Text>) {
+    fn fill_text(&mut self, text: impl Into<geometry::Text>) {
         let text = text.into();
 
         let (scale_x, scale_y) = self.transforms.current.scale();
@@ -312,12 +260,12 @@ impl geometry::frame::Backend for Frame {
                 bounds,
                 color: text.color,
                 size,
-                line_height,
+                line_height: line_height.to_absolute(size),
                 font: text.font,
                 horizontal_alignment: text.horizontal_alignment,
                 vertical_alignment: text.vertical_alignment,
                 shaping: text.shaping,
-                clip_bounds: Rectangle::with_size(Size::INFINITY),
+                clip_bounds: self.clip_bounds,
             });
         } else {
             text.draw_with(|path, color| self.fill(&path, color));
@@ -368,22 +316,25 @@ impl geometry::frame::Backend for Frame {
         self.transforms.current = self.transforms.previous.pop().unwrap();
     }
 
-    fn draft(&mut self, size: Size) -> Frame {
-        Frame::new(size)
+    fn draft(&mut self, clip_bounds: Rectangle) -> Frame {
+        Frame::with_clip(clip_bounds)
     }
 
-    fn paste(&mut self, frame: Frame, at: Point) {
-        let translation = Transformation::translate(at.x, at.y);
+    fn paste(&mut self, frame: Frame, _at: Point) {
+        self.meshes
+            .extend(frame.buffers.into_meshes(frame.clip_bounds));
 
-        self.layers
-            .extend(frame.into_layers().into_iter().map(|mut layer| {
-                layer.transformation = layer.transformation * translation;
-                layer
-            }));
+        self.text.extend(frame.text);
     }
 
-    fn into_geometry(self) -> Self::Geometry {
-        Geometry::Live(self.into_layers())
+    fn into_geometry(mut self) -> Self::Geometry {
+        self.meshes
+            .extend(self.buffers.into_meshes(self.clip_bounds));
+
+        Geometry::Live {
+            meshes: self.meshes,
+            text: self.text,
+        }
     }
 }
 
@@ -469,6 +420,27 @@ impl BufferStack {
             _ => unreachable!(),
         }
     }
+
+    fn into_meshes(self, clip_bounds: Rectangle) -> impl Iterator<Item = Mesh> {
+        self.stack.into_iter().map(move |buffer| match buffer {
+            Buffer::Solid(buffer) => Mesh::Solid {
+                buffers: mesh::Indexed {
+                    vertices: buffer.vertices,
+                    indices: buffer.indices,
+                },
+                clip_bounds,
+                transformation: Transformation::IDENTITY,
+            },
+            Buffer::Gradient(buffer) => Mesh::Gradient {
+                buffers: mesh::Indexed {
+                    vertices: buffer.vertices,
+                    indices: buffer.indices,
+                },
+                clip_bounds,
+                transformation: Transformation::IDENTITY,
+            },
+        })
+    }
 }
 
 #[derive(Debug)]
diff --git a/wgpu/src/image/mod.rs b/wgpu/src/image/mod.rs
index 88e6bdb9..86731cbf 100644
--- a/wgpu/src/image/mod.rs
+++ b/wgpu/src/image/mod.rs
@@ -9,7 +9,6 @@ mod raster;
 #[cfg(feature = "svg")]
 mod vector;
 
-use crate::core::image;
 use crate::core::{Rectangle, Size, Transformation};
 use crate::Buffer;
 
@@ -234,10 +233,12 @@ impl Pipeline {
                             [bounds.width, bounds.height],
                             atlas_entry,
                             match filter_method {
-                                image::FilterMethod::Nearest => {
+                                crate::core::image::FilterMethod::Nearest => {
                                     nearest_instances
                                 }
-                                image::FilterMethod::Linear => linear_instances,
+                                crate::core::image::FilterMethod::Linear => {
+                                    linear_instances
+                                }
                             },
                         );
                     }
diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs
index d415da72..4c864cb0 100644
--- a/wgpu/src/layer.rs
+++ b/wgpu/src/layer.rs
@@ -8,39 +8,46 @@ use crate::quad::{self, Quad};
 use crate::text::{self, Text};
 use crate::triangle;
 
-use std::cell::{self, RefCell};
-use std::rc::Rc;
-
-pub enum Layer<'a> {
-    Live(&'a Live),
-    Cached(Transformation, cell::Ref<'a, Cached>),
+pub struct Layer {
+    pub bounds: Rectangle,
+    pub quads: quad::Batch,
+    pub triangles: triangle::Batch,
+    pub text: text::Batch,
+    pub images: image::Batch,
 }
 
-pub enum LayerMut<'a> {
-    Live(&'a mut Live),
-    Cached(Transformation, cell::RefMut<'a, Cached>),
+impl Default for Layer {
+    fn default() -> Self {
+        Self {
+            bounds: Rectangle::INFINITE,
+            quads: quad::Batch::default(),
+            triangles: triangle::Batch::default(),
+            text: text::Batch::default(),
+            images: image::Batch::default(),
+        }
+    }
 }
 
 pub struct Stack {
-    live: Vec<Live>,
-    cached: Vec<(Transformation, Rc<RefCell<Cached>>)>,
-    order: Vec<Kind>,
+    layers: Vec<Layer>,
     transformations: Vec<Transformation>,
     previous: Vec<usize>,
+    pending_meshes: Vec<Vec<Mesh>>,
+    pending_text: Vec<Vec<Text>>,
     current: usize,
-    live_count: usize,
+    active_count: usize,
 }
 
 impl Stack {
     pub fn new() -> Self {
         Self {
-            live: vec![Live::default()],
-            cached: Vec::new(),
-            order: vec![Kind::Live],
+            layers: vec![Layer::default()],
             transformations: vec![Transformation::IDENTITY],
-            previous: Vec::new(),
+            previous: vec![],
+            pending_meshes: vec![Vec::new()],
+            pending_text: vec![Vec::new()],
             current: 0,
-            live_count: 1,
+            active_count: 1,
         }
     }
 
@@ -59,7 +66,7 @@ impl Stack {
             shadow_blur_radius: quad.shadow.blur_radius,
         };
 
-        self.live[self.current].quads.add(quad, &background);
+        self.layers[self.current].quads.add(quad, &background);
     }
 
     pub fn draw_paragraph(
@@ -77,7 +84,7 @@ impl Stack {
             transformation: self.transformations.last().copied().unwrap(),
         };
 
-        self.live[self.current].text.push(paragraph);
+        self.pending_text[self.current].push(paragraph);
     }
 
     pub fn draw_editor(
@@ -87,7 +94,7 @@ impl Stack {
         color: Color,
         clip_bounds: Rectangle,
     ) {
-        let paragraph = Text::Editor {
+        let editor = Text::Editor {
             editor: editor.downgrade(),
             position,
             color,
@@ -95,7 +102,7 @@ impl Stack {
             transformation: self.transformation(),
         };
 
-        self.live[self.current].text.push(paragraph);
+        self.pending_text[self.current].push(editor);
     }
 
     pub fn draw_text(
@@ -107,12 +114,13 @@ impl Stack {
     ) {
         let transformation = self.transformation();
 
-        let paragraph = Text::Cached {
+        let text = Text::Cached {
             content: text.content,
             bounds: Rectangle::new(position, text.bounds) * transformation,
             color,
             size: text.size * transformation.scale_factor(),
-            line_height: text.line_height,
+            line_height: text.line_height.to_absolute(text.size)
+                * transformation.scale_factor(),
             font: text.font,
             horizontal_alignment: text.horizontal_alignment,
             vertical_alignment: text.vertical_alignment,
@@ -120,7 +128,7 @@ impl Stack {
             clip_bounds: clip_bounds * transformation,
         };
 
-        self.live[self.current].text.push(paragraph);
+        self.pending_text[self.current].push(text);
     }
 
     pub fn draw_image(
@@ -135,7 +143,7 @@ impl Stack {
             bounds: bounds * self.transformation(),
         };
 
-        self.live[self.current].images.push(image);
+        self.layers[self.current].images.push(image);
     }
 
     pub fn draw_svg(
@@ -150,7 +158,7 @@ impl Stack {
             bounds: bounds * self.transformation(),
         };
 
-        self.live[self.current].images.push(svg);
+        self.layers[self.current].images.push(svg);
     }
 
     pub fn draw_mesh(&mut self, mut mesh: Mesh) {
@@ -161,51 +169,86 @@ impl Stack {
             }
         }
 
-        self.live[self.current].meshes.push(mesh);
+        self.pending_meshes[self.current].push(mesh);
     }
 
-    pub fn draw_layer(&mut self, mut layer: Live) {
-        layer.transformation = layer.transformation * self.transformation();
+    pub fn draw_mesh_group(&mut self, meshes: Vec<Mesh>) {
+        self.flush_pending();
 
-        if self.live_count == self.live.len() {
-            self.live.push(layer);
-        } else {
-            self.live[self.live_count] = layer;
-        }
+        let transformation = self.transformation();
 
-        self.live_count += 1;
-        self.order.push(Kind::Live);
+        self.layers[self.current]
+            .triangles
+            .push(triangle::Item::Group {
+                transformation,
+                meshes,
+            });
     }
 
-    pub fn draw_cached_layer(&mut self, layer: &Rc<RefCell<Cached>>) {
-        self.cached.push((self.transformation(), layer.clone()));
-        self.order.push(Kind::Cache);
+    pub fn draw_mesh_cache(&mut self, cache: triangle::Cache) {
+        self.flush_pending();
+
+        let transformation = self.transformation();
+
+        self.layers[self.current]
+            .triangles
+            .push(triangle::Item::Cached {
+                transformation,
+                cache,
+            });
+    }
+
+    pub fn draw_text_group(&mut self, text: Vec<Text>) {
+        self.flush_pending();
+
+        let transformation = self.transformation();
+
+        self.layers[self.current].text.push(text::Item::Group {
+            transformation,
+            text,
+        });
+    }
+
+    pub fn draw_text_cache(&mut self, cache: text::Cache) {
+        self.flush_pending();
+
+        let transformation = self.transformation();
+
+        self.layers[self.current].text.push(text::Item::Cached {
+            transformation,
+            cache,
+        });
     }
 
-    pub fn push_clip(&mut self, bounds: Option<Rectangle>) {
+    pub fn push_clip(&mut self, bounds: Rectangle) {
         self.previous.push(self.current);
-        self.order.push(Kind::Live);
 
-        self.current = self.live_count;
-        self.live_count += 1;
+        self.current = self.active_count;
+        self.active_count += 1;
 
-        let bounds = bounds.map(|bounds| bounds * self.transformation());
+        let bounds = bounds * self.transformation();
 
-        if self.current == self.live.len() {
-            self.live.push(Live {
+        if self.current == self.layers.len() {
+            self.layers.push(Layer {
                 bounds,
-                ..Live::default()
+                ..Layer::default()
             });
+            self.pending_meshes.push(Vec::new());
+            self.pending_text.push(Vec::new());
         } else {
-            self.live[self.current].bounds = bounds;
+            self.layers[self.current].bounds = bounds;
         }
     }
 
     pub fn pop_clip(&mut self) {
+        self.flush_pending();
+
         self.current = self.previous.pop().unwrap();
     }
 
     pub fn push_transformation(&mut self, transformation: Transformation) {
+        self.flush_pending();
+
         self.transformations
             .push(self.transformation() * transformation);
     }
@@ -218,109 +261,62 @@ impl Stack {
         self.transformations.last().copied().unwrap()
     }
 
-    pub fn iter_mut(&mut self) -> impl Iterator<Item = LayerMut<'_>> {
-        let mut live = self.live.iter_mut();
-        let mut cached = self.cached.iter_mut();
-
-        self.order.iter().map(move |kind| match kind {
-            Kind::Live => LayerMut::Live(live.next().unwrap()),
-            Kind::Cache => {
-                let (transformation, layer) = cached.next().unwrap();
-                let layer = layer.borrow_mut();
+    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Layer> {
+        self.flush_pending();
 
-                LayerMut::Cached(*transformation * layer.transformation, layer)
-            }
-        })
+        self.layers[..self.active_count].iter_mut()
     }
 
-    pub fn iter(&self) -> impl Iterator<Item = Layer<'_>> {
-        let mut live = self.live.iter();
-        let mut cached = self.cached.iter();
-
-        self.order.iter().map(move |kind| match kind {
-            Kind::Live => Layer::Live(live.next().unwrap()),
-            Kind::Cache => {
-                let (transformation, layer) = cached.next().unwrap();
-                let layer = layer.borrow();
-
-                Layer::Cached(*transformation * layer.transformation, layer)
-            }
-        })
+    pub fn iter(&self) -> impl Iterator<Item = &Layer> {
+        self.layers[..self.active_count].iter()
     }
 
     pub fn clear(&mut self) {
-        for live in &mut self.live[..self.live_count] {
-            live.bounds = None;
-            live.transformation = Transformation::IDENTITY;
+        for (live, pending_meshes) in self.layers[..self.active_count]
+            .iter_mut()
+            .zip(self.pending_meshes.iter_mut())
+        {
+            live.bounds = Rectangle::INFINITE;
 
             live.quads.clear();
-            live.meshes.clear();
+            live.triangles.clear();
             live.text.clear();
             live.images.clear();
+            pending_meshes.clear();
         }
 
         self.current = 0;
-        self.live_count = 1;
-
-        self.order.clear();
-        self.order.push(Kind::Live);
-
-        self.cached.clear();
+        self.active_count = 1;
         self.previous.clear();
     }
-}
 
-impl Default for Stack {
-    fn default() -> Self {
-        Self::new()
-    }
-}
+    // We want to keep the allocated memory
+    #[allow(clippy::drain_collect)]
+    fn flush_pending(&mut self) {
+        let transformation = self.transformation();
 
-#[derive(Default)]
-pub struct Live {
-    pub bounds: Option<Rectangle>,
-    pub transformation: Transformation,
-    pub quads: quad::Batch,
-    pub meshes: triangle::Batch,
-    pub text: text::Batch,
-    pub images: image::Batch,
-}
+        let pending_meshes = &mut self.pending_meshes[self.current];
+        if !pending_meshes.is_empty() {
+            self.layers[self.current]
+                .triangles
+                .push(triangle::Item::Group {
+                    transformation,
+                    meshes: pending_meshes.drain(..).collect(),
+                });
+        }
 
-impl Live {
-    pub fn into_cached(self) -> Cached {
-        Cached {
-            bounds: self.bounds,
-            transformation: self.transformation,
-            quads: quad::Cache::Staged(self.quads),
-            meshes: triangle::Cache::Staged(self.meshes),
-            text: text::Cache::Staged(self.text),
-            images: self.images,
+        let pending_text = &mut self.pending_text[self.current];
+        if !pending_text.is_empty() {
+            self.layers[self.current].text.push(text::Item::Group {
+                transformation,
+                text: pending_text.drain(..).collect(),
+            });
         }
     }
 }
 
-#[derive(Default)]
-pub struct Cached {
-    pub bounds: Option<Rectangle>,
-    pub transformation: Transformation,
-    pub quads: quad::Cache,
-    pub meshes: triangle::Cache,
-    pub text: text::Cache,
-    pub images: image::Batch,
-}
-
-impl Cached {
-    pub fn update(&mut self, live: Live) {
-        self.bounds = live.bounds;
-
-        self.quads.update(live.quads);
-        self.meshes.update(live.meshes);
-        self.text.update(live.text);
-        self.images = live.images;
+impl Default for Stack {
+    fn default() -> Self {
+        Self::new()
     }
 }
-
-enum Kind {
-    Live,
-    Cache,
-}
diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs
index 4705cfa0..d632919f 100644
--- a/wgpu/src/lib.rs
+++ b/wgpu/src/lib.rs
@@ -60,7 +60,7 @@ pub use iced_graphics::core;
 pub use wgpu;
 
 pub use engine::Engine;
-pub use layer::{Layer, LayerMut};
+pub use layer::Layer;
 pub use primitive::Primitive;
 pub use settings::Settings;
 
@@ -85,6 +85,9 @@ pub struct Renderer {
     default_text_size: Pixels,
     layers: layer::Stack,
 
+    triangle_storage: triangle::Storage,
+    text_storage: text::Storage,
+
     // TODO: Centralize all the image feature handling
     #[cfg(any(feature = "svg", feature = "image"))]
     image_cache: image::cache::Shared,
@@ -97,6 +100,9 @@ impl Renderer {
             default_text_size: settings.default_text_size,
             layers: layer::Stack::new(),
 
+            triangle_storage: triangle::Storage::new(),
+            text_storage: text::Storage::new(),
+
             #[cfg(any(feature = "svg", feature = "image"))]
             image_cache: _engine.image_cache().clone(),
         }
@@ -117,9 +123,11 @@ impl Renderer {
         overlay: &[T],
     ) {
         self.draw_overlay(overlay, viewport);
-
         self.prepare(engine, device, queue, format, encoder, viewport);
         self.render(engine, device, encoder, frame, clear_color, viewport);
+
+        self.triangle_storage.trim();
+        self.text_storage.trim();
     }
 
     fn prepare(
@@ -134,116 +142,51 @@ impl Renderer {
         let scale_factor = viewport.scale_factor() as f32;
 
         for layer in self.layers.iter_mut() {
-            match layer {
-                LayerMut::Live(live) => {
-                    if !live.quads.is_empty() {
-                        engine.quad_pipeline.prepare_batch(
-                            device,
-                            encoder,
-                            &mut engine.staging_belt,
-                            &live.quads,
-                            viewport.projection(),
-                            scale_factor,
-                        );
-                    }
-
-                    if !live.meshes.is_empty() {
-                        engine.triangle_pipeline.prepare_batch(
-                            device,
-                            encoder,
-                            &mut engine.staging_belt,
-                            &live.meshes,
-                            viewport.projection()
-                                * Transformation::scale(scale_factor),
-                        );
-                    }
-
-                    if !live.text.is_empty() {
-                        engine.text_pipeline.prepare_batch(
-                            device,
-                            queue,
-                            encoder,
-                            &live.text,
-                            live.bounds.unwrap_or(Rectangle::with_size(
-                                viewport.logical_size(),
-                            )),
-                            live.transformation
-                                * Transformation::scale(scale_factor),
-                            viewport.physical_size(),
-                        );
-                    }
-
-                    #[cfg(any(feature = "svg", feature = "image"))]
-                    if !live.images.is_empty() {
-                        engine.image_pipeline.prepare(
-                            device,
-                            encoder,
-                            &mut engine.staging_belt,
-                            &live.images,
-                            viewport.projection(),
-                            scale_factor,
-                        );
-                    }
-                }
-                LayerMut::Cached(layer_transformation, mut cached) => {
-                    if !cached.quads.is_empty() {
-                        engine.quad_pipeline.prepare_cache(
-                            device,
-                            encoder,
-                            &mut engine.staging_belt,
-                            &mut cached.quads,
-                            viewport.projection(),
-                            scale_factor,
-                        );
-                    }
-
-                    if !cached.meshes.is_empty() {
-                        let transformation =
-                            Transformation::scale(scale_factor)
-                                * layer_transformation;
-
-                        engine.triangle_pipeline.prepare_cache(
-                            device,
-                            encoder,
-                            &mut engine.staging_belt,
-                            &mut cached.meshes,
-                            viewport.projection(),
-                            transformation,
-                        );
-                    }
-
-                    if !cached.text.is_empty() {
-                        let bounds = cached.bounds.unwrap_or(
-                            Rectangle::with_size(viewport.logical_size()),
-                        );
-
-                        let transformation =
-                            Transformation::scale(scale_factor)
-                                * layer_transformation;
-
-                        engine.text_pipeline.prepare_cache(
-                            device,
-                            queue,
-                            encoder,
-                            &mut cached.text,
-                            bounds,
-                            transformation,
-                            viewport.physical_size(),
-                        );
-                    }
-
-                    #[cfg(any(feature = "svg", feature = "image"))]
-                    if !cached.images.is_empty() {
-                        engine.image_pipeline.prepare(
-                            device,
-                            encoder,
-                            &mut engine.staging_belt,
-                            &cached.images,
-                            viewport.projection(),
-                            scale_factor,
-                        );
-                    }
-                }
+            if !layer.quads.is_empty() {
+                engine.quad_pipeline.prepare(
+                    device,
+                    encoder,
+                    &mut engine.staging_belt,
+                    &layer.quads,
+                    viewport.projection(),
+                    scale_factor,
+                );
+            }
+
+            if !layer.triangles.is_empty() {
+                engine.triangle_pipeline.prepare(
+                    device,
+                    encoder,
+                    &mut engine.staging_belt,
+                    &mut self.triangle_storage,
+                    &layer.triangles,
+                    viewport.projection() * Transformation::scale(scale_factor),
+                );
+            }
+
+            if !layer.text.is_empty() {
+                engine.text_pipeline.prepare(
+                    device,
+                    queue,
+                    encoder,
+                    &mut self.text_storage,
+                    &layer.text,
+                    layer.bounds,
+                    Transformation::scale(scale_factor),
+                    viewport.physical_size(),
+                );
+            }
+
+            #[cfg(any(feature = "svg", feature = "image"))]
+            if !layer.images.is_empty() {
+                engine.image_pipeline.prepare(
+                    device,
+                    encoder,
+                    &mut engine.staging_belt,
+                    &layer.images,
+                    viewport.projection(),
+                    scale_factor,
+                );
             }
         }
     }
@@ -297,208 +240,87 @@ impl Renderer {
         #[cfg(any(feature = "svg", feature = "image"))]
         let mut image_layer = 0;
 
-        // TODO: Can we avoid collecting here?
         let scale_factor = viewport.scale_factor() as f32;
-        let screen_bounds = Rectangle::with_size(viewport.logical_size());
         let physical_bounds = Rectangle::<f32>::from(Rectangle::with_size(
             viewport.physical_size(),
         ));
 
-        let layers: Vec<_> = self.layers.iter().collect();
-        let mut i = 0;
+        let scale = Transformation::scale(scale_factor);
 
-        // println!("RENDER");
+        for layer in self.layers.iter() {
+            let Some(physical_bounds) =
+                physical_bounds.intersection(&(layer.bounds * scale))
+            else {
+                continue;
+            };
 
-        while i < layers.len() {
-            match layers[i] {
-                Layer::Live(live) => {
-                    let layer_transformation =
-                        Transformation::scale(scale_factor)
-                            * live.transformation;
+            let scissor_rect = physical_bounds.snap();
 
-                    let layer_bounds = live.bounds.unwrap_or(screen_bounds);
+            if !layer.quads.is_empty() {
+                engine.quad_pipeline.render(
+                    quad_layer,
+                    scissor_rect,
+                    &layer.quads,
+                    &mut render_pass,
+                );
 
-                    let Some(physical_bounds) = physical_bounds
-                        .intersection(&(layer_bounds * layer_transformation))
-                        .map(Rectangle::snap)
-                    else {
-                        continue;
-                    };
+                quad_layer += 1;
+            }
 
-                    if !live.quads.is_empty() {
-                        engine.quad_pipeline.render_batch(
-                            quad_layer,
-                            physical_bounds,
-                            &live.quads,
-                            &mut render_pass,
-                        );
-
-                        quad_layer += 1;
-                    }
-
-                    if !live.meshes.is_empty() {
-                        // println!("LIVE PASS");
-                        let _ = ManuallyDrop::into_inner(render_pass);
-
-                        engine.triangle_pipeline.render_batch(
-                            device,
-                            encoder,
-                            frame,
-                            mesh_layer,
-                            viewport.physical_size(),
-                            &live.meshes,
-                            physical_bounds,
-                            &layer_transformation,
-                        );
-
-                        mesh_layer += 1;
-
-                        render_pass =
-                            ManuallyDrop::new(encoder.begin_render_pass(
-                                &wgpu::RenderPassDescriptor {
-                                    label: Some("iced_wgpu render pass"),
-                                    color_attachments: &[Some(
-                                        wgpu::RenderPassColorAttachment {
-                                            view: frame,
-                                            resolve_target: None,
-                                            ops: wgpu::Operations {
-                                                load: wgpu::LoadOp::Load,
-                                                store: wgpu::StoreOp::Store,
-                                            },
-                                        },
-                                    )],
-                                    depth_stencil_attachment: None,
-                                    timestamp_writes: None,
-                                    occlusion_query_set: None,
-                                },
-                            ));
-                    }
-
-                    if !live.text.is_empty() {
-                        engine.text_pipeline.render_batch(
-                            text_layer,
-                            physical_bounds,
-                            &mut render_pass,
-                        );
-
-                        text_layer += 1;
-                    }
-
-                    #[cfg(any(feature = "svg", feature = "image"))]
-                    if !live.images.is_empty() {
-                        engine.image_pipeline.render(
-                            image_layer,
-                            physical_bounds,
-                            &mut render_pass,
-                        );
-
-                        image_layer += 1;
-                    }
-
-                    i += 1;
-                }
-                Layer::Cached(_, _) => {
-                    let group_len = layers[i..]
-                        .iter()
-                        .position(|layer| matches!(layer, Layer::Live(_)))
-                        .unwrap_or(layers.len() - i);
-
-                    let group =
-                        layers[i..i + group_len].iter().filter_map(|layer| {
-                            let Layer::Cached(transformation, cached) = layer
-                            else {
-                                unreachable!()
-                            };
-
-                            let physical_bounds = cached
-                                .bounds
-                                .and_then(|bounds| {
-                                    physical_bounds.intersection(
-                                        &(bounds
-                                            * *transformation
-                                            * Transformation::scale(
-                                                scale_factor,
-                                            )),
-                                    )
-                                })
-                                .unwrap_or(physical_bounds)
-                                .snap();
-
-                            Some((cached, physical_bounds))
-                        });
-
-                    for (cached, bounds) in group.clone() {
-                        if !cached.quads.is_empty() {
-                            engine.quad_pipeline.render_cache(
-                                &cached.quads,
-                                bounds,
-                                &mut render_pass,
-                            );
-                        }
-                    }
-
-                    let group_has_meshes = group
-                        .clone()
-                        .any(|(cached, _)| !cached.meshes.is_empty());
-
-                    if group_has_meshes {
-                        // println!("CACHE PASS");
-                        let _ = ManuallyDrop::into_inner(render_pass);
-
-                        engine.triangle_pipeline.render_cache_group(
-                            device,
-                            encoder,
-                            frame,
-                            viewport.physical_size(),
-                            group.clone().map(|(cached, bounds)| {
-                                (&cached.meshes, bounds)
-                            }),
-                        );
-
-                        render_pass =
-                            ManuallyDrop::new(encoder.begin_render_pass(
-                                &wgpu::RenderPassDescriptor {
-                                    label: Some("iced_wgpu render pass"),
-                                    color_attachments: &[Some(
-                                        wgpu::RenderPassColorAttachment {
-                                            view: frame,
-                                            resolve_target: None,
-                                            ops: wgpu::Operations {
-                                                load: wgpu::LoadOp::Load,
-                                                store: wgpu::StoreOp::Store,
-                                            },
-                                        },
-                                    )],
-                                    depth_stencil_attachment: None,
-                                    timestamp_writes: None,
-                                    occlusion_query_set: None,
+            if !layer.triangles.is_empty() {
+                let _ = ManuallyDrop::into_inner(render_pass);
+
+                mesh_layer += engine.triangle_pipeline.render(
+                    device,
+                    encoder,
+                    frame,
+                    &self.triangle_storage,
+                    mesh_layer,
+                    &layer.triangles,
+                    viewport.physical_size(),
+                    physical_bounds,
+                    scale,
+                );
+
+                render_pass = ManuallyDrop::new(encoder.begin_render_pass(
+                    &wgpu::RenderPassDescriptor {
+                        label: Some("iced_wgpu render pass"),
+                        color_attachments: &[Some(
+                            wgpu::RenderPassColorAttachment {
+                                view: frame,
+                                resolve_target: None,
+                                ops: wgpu::Operations {
+                                    load: wgpu::LoadOp::Load,
+                                    store: wgpu::StoreOp::Store,
                                 },
-                            ));
-                    }
-
-                    for (cached, bounds) in group {
-                        if !cached.text.is_empty() {
-                            engine.text_pipeline.render_cache(
-                                &cached.text,
-                                bounds,
-                                &mut render_pass,
-                            );
-                        }
-
-                        #[cfg(any(feature = "svg", feature = "image"))]
-                        if !cached.images.is_empty() {
-                            engine.image_pipeline.render(
-                                image_layer,
-                                bounds,
-                                &mut render_pass,
-                            );
-
-                            image_layer += 1;
-                        }
-                    }
-
-                    i += group_len;
-                }
+                            },
+                        )],
+                        depth_stencil_attachment: None,
+                        timestamp_writes: None,
+                        occlusion_query_set: None,
+                    },
+                ));
+            }
+
+            if !layer.text.is_empty() {
+                text_layer += engine.text_pipeline.render(
+                    &self.text_storage,
+                    text_layer,
+                    &layer.text,
+                    scissor_rect,
+                    &mut render_pass,
+                );
+            }
+
+            #[cfg(any(feature = "svg", feature = "image"))]
+            if !layer.images.is_empty() {
+                engine.image_pipeline.render(
+                    image_layer,
+                    scissor_rect,
+                    &mut render_pass,
+                );
+
+                image_layer += 1;
             }
         }
 
@@ -552,7 +374,7 @@ impl Renderer {
 
 impl core::Renderer for Renderer {
     fn start_layer(&mut self, bounds: Rectangle) {
-        self.layers.push_clip(Some(bounds));
+        self.layers.push_clip(bounds);
     }
 
     fn end_layer(&mut self, _bounds: Rectangle) {
@@ -690,15 +512,13 @@ impl graphics::geometry::Renderer for Renderer {
 
     fn draw_geometry(&mut self, geometry: Self::Geometry) {
         match geometry {
-            Geometry::Live(layers) => {
-                for layer in layers {
-                    self.layers.draw_layer(layer);
-                }
+            Geometry::Live { meshes, text } => {
+                self.layers.draw_mesh_group(meshes);
+                self.layers.draw_text_group(text);
             }
-            Geometry::Cached(layers) => {
-                for layer in layers.as_ref() {
-                    self.layers.draw_cached_layer(layer);
-                }
+            Geometry::Cached(cache) => {
+                self.layers.draw_mesh_cache(cache.meshes);
+                self.layers.draw_text_cache(cache.text);
             }
         }
     }
diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs
index 16d50b04..de432d2f 100644
--- a/wgpu/src/quad.rs
+++ b/wgpu/src/quad.rs
@@ -80,7 +80,7 @@ impl Pipeline {
         }
     }
 
-    pub fn prepare_batch(
+    pub fn prepare(
         &mut self,
         device: &wgpu::Device,
         encoder: &mut wgpu::CommandEncoder,
@@ -99,64 +99,7 @@ impl Pipeline {
         self.prepare_layer += 1;
     }
 
-    pub fn prepare_cache(
-        &self,
-        device: &wgpu::Device,
-        encoder: &mut wgpu::CommandEncoder,
-        belt: &mut wgpu::util::StagingBelt,
-        cache: &mut Cache,
-        transformation: Transformation,
-        scale: f32,
-    ) {
-        match cache {
-            Cache::Staged(_) => {
-                let Cache::Staged(batch) =
-                    std::mem::replace(cache, Cache::Staged(Batch::default()))
-                else {
-                    unreachable!()
-                };
-
-                let mut layer = Layer::new(device, &self.constant_layout);
-                layer.prepare(
-                    device,
-                    encoder,
-                    belt,
-                    &batch,
-                    transformation,
-                    scale,
-                );
-
-                *cache = Cache::Uploaded {
-                    layer,
-                    batch,
-                    needs_reupload: false,
-                }
-            }
-
-            Cache::Uploaded {
-                batch,
-                layer,
-                needs_reupload,
-            } => {
-                if *needs_reupload {
-                    layer.prepare(
-                        device,
-                        encoder,
-                        belt,
-                        batch,
-                        transformation,
-                        scale,
-                    );
-
-                    *needs_reupload = false;
-                } else {
-                    layer.update(device, encoder, belt, transformation, scale);
-                }
-            }
-        }
-    }
-
-    pub fn render_batch<'a>(
+    pub fn render<'a>(
         &'a self,
         layer: usize,
         bounds: Rectangle<u32>,
@@ -164,59 +107,38 @@ impl Pipeline {
         render_pass: &mut wgpu::RenderPass<'a>,
     ) {
         if let Some(layer) = self.layers.get(layer) {
-            self.render(bounds, layer, &quads.order, render_pass);
-        }
-    }
-
-    pub fn render_cache<'a>(
-        &'a self,
-        cache: &'a Cache,
-        bounds: Rectangle<u32>,
-        render_pass: &mut wgpu::RenderPass<'a>,
-    ) {
-        if let Cache::Uploaded { layer, batch, .. } = cache {
-            self.render(bounds, layer, &batch.order, render_pass);
-        }
-    }
-
-    fn render<'a>(
-        &'a self,
-        bounds: Rectangle<u32>,
-        layer: &'a Layer,
-        order: &Order,
-        render_pass: &mut wgpu::RenderPass<'a>,
-    ) {
-        render_pass.set_scissor_rect(
-            bounds.x,
-            bounds.y,
-            bounds.width,
-            bounds.height,
-        );
-
-        let mut solid_offset = 0;
-        let mut gradient_offset = 0;
-
-        for (kind, count) in order {
-            match kind {
-                Kind::Solid => {
-                    self.solid.render(
-                        render_pass,
-                        &layer.constants,
-                        &layer.solid,
-                        solid_offset..(solid_offset + count),
-                    );
-
-                    solid_offset += count;
-                }
-                Kind::Gradient => {
-                    self.gradient.render(
-                        render_pass,
-                        &layer.constants,
-                        &layer.gradient,
-                        gradient_offset..(gradient_offset + count),
-                    );
-
-                    gradient_offset += count;
+            render_pass.set_scissor_rect(
+                bounds.x,
+                bounds.y,
+                bounds.width,
+                bounds.height,
+            );
+
+            let mut solid_offset = 0;
+            let mut gradient_offset = 0;
+
+            for (kind, count) in &quads.order {
+                match kind {
+                    Kind::Solid => {
+                        self.solid.render(
+                            render_pass,
+                            &layer.constants,
+                            &layer.solid,
+                            solid_offset..(solid_offset + count),
+                        );
+
+                        solid_offset += count;
+                    }
+                    Kind::Gradient => {
+                        self.gradient.render(
+                            render_pass,
+                            &layer.constants,
+                            &layer.gradient,
+                            gradient_offset..(gradient_offset + count),
+                        );
+
+                        gradient_offset += count;
+                    }
                 }
             }
         }
@@ -227,48 +149,6 @@ impl Pipeline {
     }
 }
 
-#[derive(Debug)]
-pub enum Cache {
-    Staged(Batch),
-    Uploaded {
-        batch: Batch,
-        layer: Layer,
-        needs_reupload: bool,
-    },
-}
-
-impl Cache {
-    pub fn is_empty(&self) -> bool {
-        match self {
-            Cache::Staged(batch) | Cache::Uploaded { batch, .. } => {
-                batch.is_empty()
-            }
-        }
-    }
-
-    pub fn update(&mut self, new_batch: Batch) {
-        match self {
-            Self::Staged(batch) => {
-                *batch = new_batch;
-            }
-            Self::Uploaded {
-                batch,
-                needs_reupload,
-                ..
-            } => {
-                *batch = new_batch;
-                *needs_reupload = true;
-            }
-        }
-    }
-}
-
-impl Default for Cache {
-    fn default() -> Self {
-        Self::Staged(Batch::default())
-    }
-}
-
 #[derive(Debug)]
 pub struct Layer {
     constants: wgpu::BindGroup,
diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs
index a7695b74..e84e675d 100644
--- a/wgpu/src/text.rs
+++ b/wgpu/src/text.rs
@@ -4,245 +4,273 @@ use crate::graphics::color;
 use crate::graphics::text::cache::{self, Cache as BufferCache};
 use crate::graphics::text::{font_system, to_color, Editor, Paragraph};
 
+use rustc_hash::{FxHashMap, FxHashSet};
+use std::collections::hash_map;
+use std::rc::Rc;
+use std::sync::atomic::{self, AtomicU64};
 use std::sync::Arc;
 
 pub use crate::graphics::Text;
 
-pub type Batch = Vec<Text>;
+const COLOR_MODE: glyphon::ColorMode = if color::GAMMA_CORRECTION {
+    glyphon::ColorMode::Accurate
+} else {
+    glyphon::ColorMode::Web
+};
 
-#[allow(missing_debug_implementations)]
-pub struct Pipeline {
-    format: wgpu::TextureFormat,
-    atlas: glyphon::TextAtlas,
-    renderers: Vec<glyphon::TextRenderer>,
-    prepare_layer: usize,
-    cache: BufferCache,
-}
+pub type Batch = Vec<Item>;
 
-pub enum Cache {
-    Staged(Batch),
-    Uploaded {
-        batch: Batch,
-        renderer: glyphon::TextRenderer,
-        atlas: Option<glyphon::TextAtlas>,
-        buffer_cache: Option<BufferCache>,
+#[derive(Debug)]
+pub enum Item {
+    Group {
         transformation: Transformation,
-        target_size: Size<u32>,
-        needs_reupload: bool,
+        text: Vec<Text>,
     },
+    Cached {
+        transformation: Transformation,
+        cache: Cache,
+    },
+}
+
+#[derive(Debug, Clone)]
+pub struct Cache {
+    id: Id,
+    text: Rc<[Text]>,
+    version: usize,
 }
 
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Id(u64);
+
 impl Cache {
-    pub fn is_empty(&self) -> bool {
-        match self {
-            Cache::Staged(batch) | Cache::Uploaded { batch, .. } => {
-                batch.is_empty()
-            }
+    pub fn new(text: Vec<Text>) -> Self {
+        static NEXT_ID: AtomicU64 = AtomicU64::new(0);
+
+        Self {
+            id: Id(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)),
+            text: Rc::from(text),
+            version: 0,
         }
     }
 
-    pub fn update(&mut self, new_batch: Batch) {
-        match self {
-            Self::Staged(batch) => {
-                *batch = new_batch;
-            }
-            Self::Uploaded {
-                batch,
-                needs_reupload,
-                ..
-            } => {
-                *batch = new_batch;
-                *needs_reupload = true;
-            }
-        }
+    pub fn update(&mut self, text: Vec<Text>) {
+        self.text = Rc::from(text);
+        self.version += 1;
     }
 }
 
-impl Default for Cache {
-    fn default() -> Self {
-        Self::Staged(Batch::default())
-    }
+struct Upload {
+    renderer: glyphon::TextRenderer,
+    atlas: glyphon::TextAtlas,
+    buffer_cache: BufferCache,
+    transformation: Transformation,
+    version: usize,
 }
 
-impl Pipeline {
-    pub fn new(
-        device: &wgpu::Device,
-        queue: &wgpu::Queue,
-        format: wgpu::TextureFormat,
-    ) -> Self {
-        Pipeline {
-            format,
-            renderers: Vec::new(),
-            atlas: glyphon::TextAtlas::with_color_mode(
-                device,
-                queue,
-                format,
-                if color::GAMMA_CORRECTION {
-                    glyphon::ColorMode::Accurate
-                } else {
-                    glyphon::ColorMode::Web
-                },
-            ),
-            prepare_layer: 0,
-            cache: BufferCache::new(),
-        }
+#[derive(Default)]
+pub struct Storage {
+    uploads: FxHashMap<Id, Upload>,
+    recently_used: FxHashSet<Id>,
+}
+
+impl Storage {
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    fn get(&self, id: Id) -> Option<&Upload> {
+        self.uploads.get(&id)
     }
 
-    pub fn prepare_batch(
+    fn prepare(
         &mut self,
         device: &wgpu::Device,
         queue: &wgpu::Queue,
         encoder: &mut wgpu::CommandEncoder,
-        sections: &Batch,
-        layer_bounds: Rectangle,
-        layer_transformation: Transformation,
+        format: wgpu::TextureFormat,
+        cache: &Cache,
+        new_transformation: Transformation,
+        bounds: Rectangle,
         target_size: Size<u32>,
     ) {
-        if self.renderers.len() <= self.prepare_layer {
-            self.renderers.push(glyphon::TextRenderer::new(
-                &mut self.atlas,
-                device,
-                wgpu::MultisampleState::default(),
-                None,
-            ));
-        }
+        match self.uploads.entry(cache.id) {
+            hash_map::Entry::Occupied(entry) => {
+                let upload = entry.into_mut();
 
-        let renderer = &mut self.renderers[self.prepare_layer];
-        let result = prepare(
-            device,
-            queue,
-            encoder,
-            renderer,
-            &mut self.atlas,
-            &mut self.cache,
-            sections,
-            layer_bounds,
-            layer_transformation,
-            target_size,
-        );
+                if upload.version != cache.version
+                    || upload.transformation != new_transformation
+                {
+                    let _ = prepare(
+                        device,
+                        queue,
+                        encoder,
+                        &mut upload.renderer,
+                        &mut upload.atlas,
+                        &mut upload.buffer_cache,
+                        &cache.text,
+                        bounds,
+                        new_transformation,
+                        target_size,
+                    );
 
-        match result {
-            Ok(()) => {
-                self.prepare_layer += 1;
-            }
-            Err(glyphon::PrepareError::AtlasFull) => {
-                // If the atlas cannot grow, then all bets are off.
-                // Instead of panicking, we will just pray that the result
-                // will be somewhat readable...
-            }
-        }
-    }
+                    upload.version = cache.version;
+                    upload.transformation = new_transformation;
 
-    pub fn prepare_cache(
-        &mut self,
-        device: &wgpu::Device,
-        queue: &wgpu::Queue,
-        encoder: &mut wgpu::CommandEncoder,
-        cache: &mut Cache,
-        layer_bounds: Rectangle,
-        new_transformation: Transformation,
-        new_target_size: Size<u32>,
-    ) {
-        match cache {
-            Cache::Staged(_) => {
-                let Cache::Staged(batch) =
-                    std::mem::replace(cache, Cache::Staged(Batch::default()))
-                else {
-                    unreachable!()
-                };
-
-                // TODO: Find a better heuristic (?)
-                let (mut atlas, mut buffer_cache) = if batch.len() > 10 {
-                    (
-                        Some(glyphon::TextAtlas::with_color_mode(
-                            device,
-                            queue,
-                            self.format,
-                            if color::GAMMA_CORRECTION {
-                                glyphon::ColorMode::Accurate
-                            } else {
-                                glyphon::ColorMode::Web
-                            },
-                        )),
-                        Some(BufferCache::new()),
-                    )
-                } else {
-                    (None, None)
-                };
+                    upload.buffer_cache.trim();
+                    upload.atlas.trim();
+                }
+            }
+            hash_map::Entry::Vacant(entry) => {
+                let mut atlas = glyphon::TextAtlas::with_color_mode(
+                    device, queue, format, COLOR_MODE,
+                );
 
                 let mut renderer = glyphon::TextRenderer::new(
-                    atlas.as_mut().unwrap_or(&mut self.atlas),
+                    &mut atlas,
                     device,
                     wgpu::MultisampleState::default(),
                     None,
                 );
 
+                let mut buffer_cache = BufferCache::new();
+
                 let _ = prepare(
                     device,
                     queue,
                     encoder,
                     &mut renderer,
-                    atlas.as_mut().unwrap_or(&mut self.atlas),
-                    buffer_cache.as_mut().unwrap_or(&mut self.cache),
-                    &batch,
-                    layer_bounds,
+                    &mut atlas,
+                    &mut buffer_cache,
+                    &cache.text,
+                    bounds,
                     new_transformation,
-                    new_target_size,
+                    target_size,
                 );
 
-                *cache = Cache::Uploaded {
-                    batch,
-                    needs_reupload: false,
+                let _ = entry.insert(Upload {
                     renderer,
                     atlas,
                     buffer_cache,
                     transformation: new_transformation,
-                    target_size: new_target_size,
-                }
+                    version: 0,
+                });
             }
-            Cache::Uploaded {
-                batch,
-                needs_reupload,
-                renderer,
-                atlas,
-                buffer_cache,
-                transformation,
-                target_size,
-            } => {
-                if *needs_reupload
-                    || atlas.is_none()
-                    || buffer_cache.is_none()
-                    || new_transformation != *transformation
-                    || new_target_size != *target_size
-                {
-                    let _ = prepare(
+        }
+
+        let _ = self.recently_used.insert(cache.id);
+    }
+
+    pub fn trim(&mut self) {
+        self.uploads.retain(|id, _| self.recently_used.contains(id));
+        self.recently_used.clear();
+    }
+}
+
+#[allow(missing_debug_implementations)]
+pub struct Pipeline {
+    format: wgpu::TextureFormat,
+    atlas: glyphon::TextAtlas,
+    renderers: Vec<glyphon::TextRenderer>,
+    prepare_layer: usize,
+    cache: BufferCache,
+}
+
+impl Pipeline {
+    pub fn new(
+        device: &wgpu::Device,
+        queue: &wgpu::Queue,
+        format: wgpu::TextureFormat,
+    ) -> Self {
+        Pipeline {
+            format,
+            renderers: Vec::new(),
+            atlas: glyphon::TextAtlas::with_color_mode(
+                device, queue, format, COLOR_MODE,
+            ),
+            prepare_layer: 0,
+            cache: BufferCache::new(),
+        }
+    }
+
+    pub fn prepare(
+        &mut self,
+        device: &wgpu::Device,
+        queue: &wgpu::Queue,
+        encoder: &mut wgpu::CommandEncoder,
+        storage: &mut Storage,
+        batch: &Batch,
+        layer_bounds: Rectangle,
+        layer_transformation: Transformation,
+        target_size: Size<u32>,
+    ) {
+        for item in batch {
+            match item {
+                Item::Group {
+                    transformation,
+                    text,
+                } => {
+                    if self.renderers.len() <= self.prepare_layer {
+                        self.renderers.push(glyphon::TextRenderer::new(
+                            &mut self.atlas,
+                            device,
+                            wgpu::MultisampleState::default(),
+                            None,
+                        ));
+                    }
+
+                    let renderer = &mut self.renderers[self.prepare_layer];
+                    let result = prepare(
                         device,
                         queue,
                         encoder,
                         renderer,
-                        atlas.as_mut().unwrap_or(&mut self.atlas),
-                        buffer_cache.as_mut().unwrap_or(&mut self.cache),
-                        batch,
+                        &mut self.atlas,
+                        &mut self.cache,
+                        text,
                         layer_bounds,
-                        new_transformation,
-                        new_target_size,
+                        layer_transformation * *transformation,
+                        target_size,
                     );
 
-                    *transformation = new_transformation;
-                    *target_size = new_target_size;
-                    *needs_reupload = false;
+                    match result {
+                        Ok(()) => {
+                            self.prepare_layer += 1;
+                        }
+                        Err(glyphon::PrepareError::AtlasFull) => {
+                            // If the atlas cannot grow, then all bets are off.
+                            // Instead of panicking, we will just pray that the result
+                            // will be somewhat readable...
+                        }
+                    }
+                }
+                Item::Cached {
+                    transformation,
+                    cache,
+                } => {
+                    storage.prepare(
+                        device,
+                        queue,
+                        encoder,
+                        self.format,
+                        cache,
+                        layer_transformation * *transformation,
+                        layer_bounds,
+                        target_size,
+                    );
                 }
             }
         }
     }
 
-    pub fn render_batch<'a>(
+    pub fn render<'a>(
         &'a self,
-        layer: usize,
+        storage: &'a Storage,
+        start: usize,
+        batch: &'a Batch,
         bounds: Rectangle<u32>,
         render_pass: &mut wgpu::RenderPass<'a>,
-    ) {
-        let renderer = &self.renderers[layer];
+    ) -> usize {
+        let mut layer_count = 0;
 
         render_pass.set_scissor_rect(
             bounds.x,
@@ -251,34 +279,29 @@ impl Pipeline {
             bounds.height,
         );
 
-        renderer
-            .render(&self.atlas, render_pass)
-            .expect("Render text");
-    }
+        for item in batch {
+            match item {
+                Item::Group { .. } => {
+                    let renderer = &self.renderers[start + layer_count];
 
-    pub fn render_cache<'a>(
-        &'a self,
-        cache: &'a Cache,
-        bounds: Rectangle<u32>,
-        render_pass: &mut wgpu::RenderPass<'a>,
-    ) {
-        let Cache::Uploaded {
-            renderer, atlas, ..
-        } = cache
-        else {
-            return;
-        };
+                    renderer
+                        .render(&self.atlas, render_pass)
+                        .expect("Render text");
 
-        render_pass.set_scissor_rect(
-            bounds.x,
-            bounds.y,
-            bounds.width,
-            bounds.height,
-        );
+                    layer_count += 1;
+                }
+                Item::Cached { cache, .. } => {
+                    if let Some(upload) = storage.get(cache.id) {
+                        upload
+                            .renderer
+                            .render(&upload.atlas, render_pass)
+                            .expect("Render cached text");
+                    }
+                }
+            }
+        }
 
-        renderer
-            .render(atlas.as_ref().unwrap_or(&self.atlas), render_pass)
-            .expect("Render text");
+        layer_count
     }
 
     pub fn end_frame(&mut self) {
@@ -296,7 +319,7 @@ fn prepare(
     renderer: &mut glyphon::TextRenderer,
     atlas: &mut glyphon::TextAtlas,
     buffer_cache: &mut BufferCache,
-    sections: &Batch,
+    sections: &[Text],
     layer_bounds: Rectangle,
     layer_transformation: Transformation,
     target_size: Size<u32>,
@@ -333,8 +356,8 @@ fn prepare(
                     font_system,
                     cache::Key {
                         content,
-                        size: (*size).into(),
-                        line_height: f32::from(line_height.to_absolute(*size)),
+                        size: f32::from(*size),
+                        line_height: f32::from(*line_height),
                         font: *font,
                         bounds: Size {
                             width: bounds.width,
diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs
index 3a184da2..a08b6987 100644
--- a/wgpu/src/triangle.rs
+++ b/wgpu/src/triangle.rs
@@ -6,10 +6,136 @@ use crate::graphics::mesh::{self, Mesh};
 use crate::graphics::Antialiasing;
 use crate::Buffer;
 
+use rustc_hash::{FxHashMap, FxHashSet};
+use std::collections::hash_map;
+use std::rc::Rc;
+use std::sync::atomic::{self, AtomicU64};
+
 const INITIAL_INDEX_COUNT: usize = 1_000;
 const INITIAL_VERTEX_COUNT: usize = 1_000;
 
-pub type Batch = Vec<Mesh>;
+pub type Batch = Vec<Item>;
+
+pub enum Item {
+    Group {
+        transformation: Transformation,
+        meshes: Vec<Mesh>,
+    },
+    Cached {
+        transformation: Transformation,
+        cache: Cache,
+    },
+}
+
+#[derive(Debug, Clone)]
+pub struct Cache {
+    id: Id,
+    batch: Rc<[Mesh]>,
+    version: usize,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Id(u64);
+
+impl Cache {
+    pub fn new(meshes: Vec<Mesh>) -> Self {
+        static NEXT_ID: AtomicU64 = AtomicU64::new(0);
+
+        Self {
+            id: Id(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)),
+            batch: Rc::from(meshes),
+            version: 0,
+        }
+    }
+
+    pub fn update(&mut self, meshes: Vec<Mesh>) {
+        self.batch = Rc::from(meshes);
+        self.version += 1;
+    }
+}
+
+#[derive(Debug)]
+struct Upload {
+    layer: Layer,
+    transformation: Transformation,
+    version: usize,
+}
+
+#[derive(Debug, Default)]
+pub struct Storage {
+    uploads: FxHashMap<Id, Upload>,
+    recently_used: FxHashSet<Id>,
+}
+
+impl Storage {
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    fn get(&self, id: Id) -> Option<&Upload> {
+        self.uploads.get(&id)
+    }
+
+    fn prepare(
+        &mut self,
+        device: &wgpu::Device,
+        encoder: &mut wgpu::CommandEncoder,
+        belt: &mut wgpu::util::StagingBelt,
+        solid: &solid::Pipeline,
+        gradient: &gradient::Pipeline,
+        cache: &Cache,
+        new_transformation: Transformation,
+    ) {
+        match self.uploads.entry(cache.id) {
+            hash_map::Entry::Occupied(entry) => {
+                let upload = entry.into_mut();
+
+                if upload.version != cache.version
+                    || upload.transformation != new_transformation
+                {
+                    upload.layer.prepare(
+                        device,
+                        encoder,
+                        belt,
+                        solid,
+                        gradient,
+                        &cache.batch,
+                        new_transformation,
+                    );
+
+                    upload.version = cache.version;
+                    upload.transformation = new_transformation;
+                }
+            }
+            hash_map::Entry::Vacant(entry) => {
+                let mut layer = Layer::new(device, solid, gradient);
+
+                layer.prepare(
+                    device,
+                    encoder,
+                    belt,
+                    solid,
+                    gradient,
+                    &cache.batch,
+                    new_transformation,
+                );
+
+                let _ = entry.insert(Upload {
+                    layer,
+                    transformation: new_transformation,
+                    version: 0,
+                });
+            }
+        }
+
+        let _ = self.recently_used.insert(cache.id);
+    }
+
+    pub fn trim(&mut self) {
+        self.uploads.retain(|id, _| self.recently_used.contains(id));
+        self.recently_used.clear();
+    }
+}
 
 #[derive(Debug)]
 pub struct Pipeline {
@@ -35,180 +161,103 @@ impl Pipeline {
         }
     }
 
-    pub fn prepare_batch(
-        &mut self,
-        device: &wgpu::Device,
-        encoder: &mut wgpu::CommandEncoder,
-        belt: &mut wgpu::util::StagingBelt,
-        meshes: &Batch,
-        transformation: Transformation,
-    ) {
-        if self.layers.len() <= self.prepare_layer {
-            self.layers
-                .push(Layer::new(device, &self.solid, &self.gradient));
-        }
-
-        let layer = &mut self.layers[self.prepare_layer];
-        layer.prepare(
-            device,
-            encoder,
-            belt,
-            &self.solid,
-            &self.gradient,
-            meshes,
-            transformation,
-        );
-
-        self.prepare_layer += 1;
-    }
-
-    pub fn prepare_cache(
+    pub fn prepare(
         &mut self,
         device: &wgpu::Device,
         encoder: &mut wgpu::CommandEncoder,
         belt: &mut wgpu::util::StagingBelt,
-        cache: &mut Cache,
-        new_projection: Transformation,
-        new_transformation: Transformation,
+        storage: &mut Storage,
+        items: &[Item],
+        projection: Transformation,
     ) {
-        let new_projection = new_projection * new_transformation;
-
-        match cache {
-            Cache::Staged(_) => {
-                let Cache::Staged(batch) =
-                    std::mem::replace(cache, Cache::Staged(Batch::default()))
-                else {
-                    unreachable!()
-                };
-
-                let mut layer = Layer::new(device, &self.solid, &self.gradient);
-                layer.prepare(
-                    device,
-                    encoder,
-                    belt,
-                    &self.solid,
-                    &self.gradient,
-                    &batch,
-                    new_projection,
-                );
+        for item in items {
+            match item {
+                Item::Group {
+                    transformation,
+                    meshes,
+                } => {
+                    if self.layers.len() <= self.prepare_layer {
+                        self.layers.push(Layer::new(
+                            device,
+                            &self.solid,
+                            &self.gradient,
+                        ));
+                    }
 
-                *cache = Cache::Uploaded {
-                    layer,
-                    batch,
-                    transformation: new_transformation,
-                    projection: new_projection,
-                    needs_reupload: false,
-                }
-            }
-            Cache::Uploaded {
-                batch,
-                layer,
-                transformation,
-                projection,
-                needs_reupload,
-            } => {
-                if *needs_reupload || new_projection != *projection {
+                    let layer = &mut self.layers[self.prepare_layer];
                     layer.prepare(
                         device,
                         encoder,
                         belt,
                         &self.solid,
                         &self.gradient,
-                        batch,
-                        new_projection,
+                        meshes,
+                        projection * *transformation,
                     );
 
-                    *transformation = new_transformation;
-                    *projection = new_projection;
-                    *needs_reupload = false;
+                    self.prepare_layer += 1;
+                }
+                Item::Cached {
+                    transformation,
+                    cache,
+                } => {
+                    storage.prepare(
+                        device,
+                        encoder,
+                        belt,
+                        &self.solid,
+                        &self.gradient,
+                        cache,
+                        projection * *transformation,
+                    );
                 }
             }
         }
     }
 
-    pub fn render_batch(
-        &mut self,
-        device: &wgpu::Device,
-        encoder: &mut wgpu::CommandEncoder,
-        target: &wgpu::TextureView,
-        layer: usize,
-        target_size: Size<u32>,
-        meshes: &Batch,
-        bounds: Rectangle<u32>,
-        transformation: &Transformation,
-    ) {
-        Self::render(
-            device,
-            encoder,
-            target,
-            self.blit.as_mut(),
-            &self.solid,
-            &self.gradient,
-            target_size,
-            std::iter::once((
-                &self.layers[layer],
-                meshes,
-                transformation,
-                bounds,
-            )),
-        );
-    }
-
-    #[allow(dead_code)]
-    pub fn render_cache(
+    pub fn render(
         &mut self,
         device: &wgpu::Device,
         encoder: &mut wgpu::CommandEncoder,
         target: &wgpu::TextureView,
+        storage: &Storage,
+        start: usize,
+        batch: &Batch,
         target_size: Size<u32>,
-        cache: &Cache,
-        bounds: Rectangle<u32>,
-    ) {
-        let Cache::Uploaded {
-            batch,
-            layer,
-            transformation,
-            ..
-        } = cache
-        else {
-            return;
-        };
+        bounds: Rectangle,
+        screen_transformation: Transformation,
+    ) -> usize {
+        let mut layer_count = 0;
 
-        Self::render(
-            device,
-            encoder,
-            target,
-            self.blit.as_mut(),
-            &self.solid,
-            &self.gradient,
-            target_size,
-            std::iter::once((layer, batch, transformation, bounds)),
-        );
-    }
+        let items = batch.iter().filter_map(|item| match item {
+            Item::Group {
+                transformation,
+                meshes,
+            } => {
+                let layer = &self.layers[start + layer_count];
+                layer_count += 1;
 
-    pub fn render_cache_group<'a>(
-        &mut self,
-        device: &wgpu::Device,
-        encoder: &mut wgpu::CommandEncoder,
-        target: &wgpu::TextureView,
-        target_size: Size<u32>,
-        group: impl Iterator<Item = (&'a Cache, Rectangle<u32>)>,
-    ) {
-        let group = group.filter_map(|(cache, bounds)| {
-            if let Cache::Uploaded {
-                batch,
-                layer,
+                Some((
+                    layer,
+                    meshes.as_slice(),
+                    screen_transformation * *transformation,
+                ))
+            }
+            Item::Cached {
                 transformation,
-                ..
-            } = cache
-            {
-                Some((layer, batch, transformation, bounds))
-            } else {
-                None
+                cache,
+            } => {
+                let upload = storage.get(cache.id)?;
+
+                Some((
+                    &upload.layer,
+                    &cache.batch,
+                    screen_transformation * *transformation,
+                ))
             }
         });
 
-        Self::render(
+        render(
             device,
             encoder,
             target,
@@ -216,71 +265,11 @@ impl Pipeline {
             &self.solid,
             &self.gradient,
             target_size,
-            group,
+            bounds,
+            items,
         );
-    }
-
-    fn render<'a>(
-        device: &wgpu::Device,
-        encoder: &mut wgpu::CommandEncoder,
-        target: &wgpu::TextureView,
-        mut blit: Option<&mut msaa::Blit>,
-        solid: &solid::Pipeline,
-        gradient: &gradient::Pipeline,
-        target_size: Size<u32>,
-        group: impl Iterator<
-            Item = (&'a Layer, &'a Batch, &'a Transformation, Rectangle<u32>),
-        >,
-    ) {
-        {
-            let (attachment, resolve_target, load) = if let Some(blit) =
-                &mut blit
-            {
-                let (attachment, resolve_target) =
-                    blit.targets(device, target_size.width, target_size.height);
-
-                (
-                    attachment,
-                    Some(resolve_target),
-                    wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
-                )
-            } else {
-                (target, None, wgpu::LoadOp::Load)
-            };
-
-            let mut render_pass =
-                encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
-                    label: Some("iced_wgpu.triangle.render_pass"),
-                    color_attachments: &[Some(
-                        wgpu::RenderPassColorAttachment {
-                            view: attachment,
-                            resolve_target,
-                            ops: wgpu::Operations {
-                                load,
-                                store: wgpu::StoreOp::Store,
-                            },
-                        },
-                    )],
-                    depth_stencil_attachment: None,
-                    timestamp_writes: None,
-                    occlusion_query_set: None,
-                });
-
-            for (layer, meshes, transformation, bounds) in group {
-                layer.render(
-                    solid,
-                    gradient,
-                    meshes,
-                    bounds,
-                    *transformation,
-                    &mut render_pass,
-                );
-            }
-        }
 
-        if let Some(blit) = blit {
-            blit.draw(encoder, target);
-        }
+        layer_count
     }
 
     pub fn end_frame(&mut self) {
@@ -288,47 +277,61 @@ impl Pipeline {
     }
 }
 
-#[derive(Debug)]
-pub enum Cache {
-    Staged(Batch),
-    Uploaded {
-        batch: Batch,
-        layer: Layer,
-        transformation: Transformation,
-        projection: Transformation,
-        needs_reupload: bool,
-    },
-}
-
-impl Cache {
-    pub fn is_empty(&self) -> bool {
-        match self {
-            Cache::Staged(batch) | Cache::Uploaded { batch, .. } => {
-                batch.is_empty()
-            }
-        }
-    }
+fn render<'a>(
+    device: &wgpu::Device,
+    encoder: &mut wgpu::CommandEncoder,
+    target: &wgpu::TextureView,
+    mut blit: Option<&mut msaa::Blit>,
+    solid: &solid::Pipeline,
+    gradient: &gradient::Pipeline,
+    target_size: Size<u32>,
+    bounds: Rectangle,
+    group: impl Iterator<Item = (&'a Layer, &'a [Mesh], Transformation)>,
+) {
+    {
+        let (attachment, resolve_target, load) = if let Some(blit) = &mut blit {
+            let (attachment, resolve_target) =
+                blit.targets(device, target_size.width, target_size.height);
+
+            (
+                attachment,
+                Some(resolve_target),
+                wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
+            )
+        } else {
+            (target, None, wgpu::LoadOp::Load)
+        };
 
-    pub fn update(&mut self, new_batch: Batch) {
-        match self {
-            Self::Staged(batch) => {
-                *batch = new_batch;
-            }
-            Self::Uploaded {
-                batch,
-                needs_reupload,
-                ..
-            } => {
-                *batch = new_batch;
-                *needs_reupload = true;
-            }
+        let mut render_pass =
+            encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+                label: Some("iced_wgpu.triangle.render_pass"),
+                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+                    view: attachment,
+                    resolve_target,
+                    ops: wgpu::Operations {
+                        load,
+                        store: wgpu::StoreOp::Store,
+                    },
+                })],
+                depth_stencil_attachment: None,
+                timestamp_writes: None,
+                occlusion_query_set: None,
+            });
+
+        for (layer, meshes, transformation) in group {
+            layer.render(
+                solid,
+                gradient,
+                meshes,
+                bounds,
+                transformation,
+                &mut render_pass,
+            );
         }
     }
-}
 
-impl Default for Cache {
-    fn default() -> Self {
-        Self::Staged(Batch::default())
+    if let Some(blit) = blit {
+        blit.draw(encoder, target);
     }
 }
 
@@ -366,7 +369,7 @@ impl Layer {
         belt: &mut wgpu::util::StagingBelt,
         solid: &solid::Pipeline,
         gradient: &gradient::Pipeline,
-        meshes: &Batch,
+        meshes: &[Mesh],
         transformation: Transformation,
     ) {
         // Count the total amount of vertices & indices we need to handle
@@ -471,8 +474,8 @@ impl Layer {
         &'a self,
         solid: &'a solid::Pipeline,
         gradient: &'a gradient::Pipeline,
-        meshes: &Batch,
-        layer_bounds: Rectangle<u32>,
+        meshes: &[Mesh],
+        bounds: Rectangle,
         transformation: Transformation,
         render_pass: &mut wgpu::RenderPass<'a>,
     ) {
@@ -481,8 +484,8 @@ impl Layer {
         let mut last_is_solid = None;
 
         for (index, mesh) in meshes.iter().enumerate() {
-            let Some(clip_bounds) = Rectangle::<f32>::from(layer_bounds)
-                .intersection(&(mesh.clip_bounds() * transformation))
+            let Some(clip_bounds) =
+                bounds.intersection(&(mesh.clip_bounds() * transformation))
             else {
                 continue;
             };
-- 
cgit