Skip to main content

telar_renderer_core/
preprocess.rs

1use std::sync::Arc;
2
3use rustc_hash::FxHashMap;
4
5use crate::style::Scale;
6use crate::{Color, DrawCommand, Paint, PathData, PathStyle, RectStyle, TextStyle};
7
8fn fill_layer_alpha(style: &RectStyle) -> Option<f32> {
9    // Skip when a shadow is present: shadow.color.a controls shadow opacity independently and would be incorrectly scaled inside a fill-alpha layer.
10    if style.radius.is_zero() || style.shadow.is_some() {
11        return None;
12    }
13    match style.fill {
14        Some(Paint::Solid(c)) if c.a > 0.0 && c.a < 1.0 => Some(c.a),
15        _ => None,
16    }
17}
18
19fn would_expand_fill_layers(commands: &[DrawCommand]) -> bool {
20    commands.iter().any(|cmd| match cmd {
21        DrawCommand::Rect { style, .. } => fill_layer_alpha(style).is_some(),
22        _ => false,
23    })
24}
25
26pub fn expand_fill_layers(commands: &[DrawCommand]) -> Option<Vec<DrawCommand>> {
27    if !would_expand_fill_layers(commands) {
28        return None;
29    }
30    let mut result = Vec::with_capacity(commands.len() + 4);
31    for cmd in commands {
32        if let DrawCommand::Rect { rect, style } = cmd {
33            if let Some(alpha) = fill_layer_alpha(style) {
34                let mut opaque = **style;
35                if let Some(Paint::Solid(c)) = opaque.fill {
36                    opaque.fill = Some(Paint::Solid(Color { a: 1.0, ..c }));
37                }
38                result.push(DrawCommand::PushLayer {
39                    opacity: alpha,
40                    backdrop_blur: 0.0,
41                });
42                result.push(DrawCommand::Rect {
43                    rect: *rect,
44                    style: Arc::new(opaque),
45                });
46                result.push(DrawCommand::PopLayer);
47                continue;
48            }
49        }
50        result.push(cmd.clone());
51    }
52    Some(result)
53}
54
55pub fn blur_sigma(blur_radius: f32) -> f32 {
56    blur_radius / 2.0
57}
58
59pub fn blur_padding(sigma: f32) -> i32 {
60    (sigma * 3.0).ceil() as i32 + 1
61}
62
63fn scale_path_data(data: &crate::PathData, sf: f32) -> crate::PathData {
64    let mut out = crate::PathData::new();
65    for verb in data.verbs() {
66        out = match verb {
67            crate::PathVerb::MoveTo(p) => out.move_to(p.scale(sf)),
68            crate::PathVerb::LineTo(p) => out.line_to(p.scale(sf)),
69            crate::PathVerb::QuadTo { ctrl, to } => out.quad_to(ctrl.scale(sf), to.scale(sf)),
70            crate::PathVerb::CubicTo { ctrl1, ctrl2, to } => {
71                out.cubic_to(ctrl1.scale(sf), ctrl2.scale(sf), to.scale(sf))
72            }
73            crate::PathVerb::Close => out.close(),
74        };
75    }
76    out
77}
78
79fn scale_command(cmd: &DrawCommand, sf: f32) -> DrawCommand {
80    match cmd {
81        DrawCommand::Rect { rect, style } => DrawCommand::Rect {
82            rect: rect.scale(sf),
83            style: Arc::new((**style).scale(sf)),
84        },
85        DrawCommand::Text { text, rect, style } => DrawCommand::Text {
86            text: text.clone(),
87            rect: rect.scale(sf),
88            style: Arc::new((**style).scale(sf)),
89        },
90        DrawCommand::RichText { runs, rect, base } => DrawCommand::RichText {
91            runs: runs.clone(),
92            rect: rect.scale(sf),
93            base: Arc::new((**base).scale(sf)),
94        },
95        DrawCommand::Image { data, rect, filter } => DrawCommand::Image {
96            data: data.clone(),
97            rect: rect.scale(sf),
98            filter: *filter,
99        },
100        DrawCommand::Line { p1, p2, style } => DrawCommand::Line {
101            p1: p1.scale(sf),
102            p2: p2.scale(sf),
103            style: (*style).scale(sf),
104        },
105        DrawCommand::Path { data, style } => DrawCommand::Path {
106            data: Arc::new(scale_path_data(data, sf)),
107            style: Arc::new((**style).scale(sf)),
108        },
109        DrawCommand::PushClip { rect, radius } => DrawCommand::PushClip {
110            rect: rect.scale(sf),
111            radius: (*radius).scale(sf),
112        },
113        DrawCommand::PopClip => DrawCommand::PopClip,
114        DrawCommand::PushMatrix { matrix } => DrawCommand::PushMatrix {
115            // Only the translation components (e, f at 4-5) are scaled; the linear part stays since sf*(a*x + c*y + e) = a*(sf*x) + c*(sf*y) + sf*e.
116            matrix: [
117                matrix[0],
118                matrix[1],
119                matrix[2],
120                matrix[3],
121                matrix[4] * sf,
122                matrix[5] * sf,
123            ],
124        },
125        DrawCommand::PopMatrix => DrawCommand::PopMatrix,
126        DrawCommand::PushLayer {
127            opacity,
128            backdrop_blur,
129        } => DrawCommand::PushLayer {
130            opacity: *opacity,
131            backdrop_blur: backdrop_blur * sf,
132        },
133        DrawCommand::PopLayer => DrawCommand::PopLayer,
134    }
135}
136
137/// Reusable scratch for scaling draw commands to physical pixels on the software path. Holds the
138/// output buffer plus per-frame caches keyed by the source `Arc` pointer, so a style shared by many
139/// commands (the common case for a UI tree) is scaled and heap-allocated once per frame rather than
140/// once per command.
141#[derive(Default)]
142pub struct ScaleScratch {
143    storage: Vec<DrawCommand>,
144    rect_styles: FxHashMap<usize, Arc<RectStyle>>,
145    text_styles: FxHashMap<usize, Arc<TextStyle>>,
146    path_styles: FxHashMap<usize, Arc<PathStyle>>,
147    path_data: FxHashMap<usize, Arc<PathData>>,
148}
149
150impl ScaleScratch {
151    pub fn new() -> Self {
152        Self::default()
153    }
154
155    /// Scales every command in `commands` by `sf` into the reusable internal buffer and returns it.
156    /// The pointer-keyed caches are cleared on entry, so a recycled allocator address can never yield
157    /// a stale hit (ABA-safe); cache reuse only spans commands within this single call.
158    pub fn scale_into(&mut self, commands: &[DrawCommand], sf: f32) -> &[DrawCommand] {
159        // Destructure so the output buffer and the caches are borrowed as disjoint fields in the loop.
160        let Self {
161            storage,
162            rect_styles,
163            text_styles,
164            path_styles,
165            path_data,
166        } = self;
167        storage.clear();
168        rect_styles.clear();
169        text_styles.clear();
170        path_styles.clear();
171        path_data.clear();
172        storage.reserve(commands.len());
173        for cmd in commands {
174            storage.push(scale_command_cached(
175                cmd,
176                sf,
177                rect_styles,
178                text_styles,
179                path_styles,
180                path_data,
181            ));
182        }
183        storage
184    }
185}
186
187#[inline]
188fn scaled_style_arc<T: Scale + Copy>(
189    cache: &mut FxHashMap<usize, Arc<T>>,
190    style: &Arc<T>,
191    sf: f32,
192) -> Arc<T> {
193    let key = Arc::as_ptr(style) as usize;
194    cache
195        .entry(key)
196        .or_insert_with(|| Arc::new((**style).scale(sf)))
197        .clone()
198}
199
200#[inline]
201fn scaled_path_arc(
202    cache: &mut FxHashMap<usize, Arc<PathData>>,
203    data: &Arc<PathData>,
204    sf: f32,
205) -> Arc<PathData> {
206    let key = Arc::as_ptr(data) as usize;
207    cache
208        .entry(key)
209        .or_insert_with(|| Arc::new(scale_path_data(data, sf)))
210        .clone()
211}
212
213fn scale_command_cached(
214    cmd: &DrawCommand,
215    sf: f32,
216    rect_styles: &mut FxHashMap<usize, Arc<RectStyle>>,
217    text_styles: &mut FxHashMap<usize, Arc<TextStyle>>,
218    path_styles: &mut FxHashMap<usize, Arc<PathStyle>>,
219    path_data: &mut FxHashMap<usize, Arc<PathData>>,
220) -> DrawCommand {
221    match cmd {
222        DrawCommand::Rect { rect, style } => DrawCommand::Rect {
223            rect: rect.scale(sf),
224            style: scaled_style_arc(rect_styles, style, sf),
225        },
226        DrawCommand::Text { text, rect, style } => DrawCommand::Text {
227            text: text.clone(),
228            rect: rect.scale(sf),
229            style: scaled_style_arc(text_styles, style, sf),
230        },
231        DrawCommand::Path { data, style } => DrawCommand::Path {
232            data: scaled_path_arc(path_data, data, sf),
233            style: scaled_style_arc(path_styles, style, sf),
234        },
235        // The remaining variants either allocate nothing per command (Line/clip/matrix/layer) or only bump an Arc refcount (Image), so the uncached path is already cheap.
236        other => scale_command(other, sf),
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243    use crate::{BorderRadius, RectStyle};
244    use geometry_core::Rect;
245
246    fn rect_cmd(style: &Arc<RectStyle>, x: f32) -> DrawCommand {
247        DrawCommand::Rect {
248            rect: Rect::new(x, 0.0, 10.0, 10.0),
249            style: style.clone(),
250        }
251    }
252
253    #[test]
254    fn scale_into_correctly_scales_and_shares_arcs() {
255        let style = Arc::new(RectStyle::default().with_radius(BorderRadius::all(4.0)));
256        let cmds = vec![
257            rect_cmd(&style, 0.0),
258            rect_cmd(&style, 20.0),
259            rect_cmd(&style, 40.0),
260        ];
261        let sf = 3.0;
262
263        let mut scratch = ScaleScratch::new();
264        let got = scratch.scale_into(&cmds, sf);
265
266        assert_eq!(got.len(), 3);
267
268        for (i, cmd) in got.iter().enumerate() {
269            match cmd {
270                DrawCommand::Rect { rect, style: _ } => {
271                    assert_eq!(rect.x, (i as f32) * 20.0 * sf);
272                    assert_eq!(rect.y, 0.0);
273                    assert_eq!(rect.width, 30.0);
274                    assert_eq!(rect.height, 30.0);
275                }
276                other => panic!("expected Rect, got {other:?}"),
277            }
278        }
279
280        // The three commands shared one input style Arc, so the scaled output Arcs are shared too (one Arc::new instead of three).
281        let style_of = |c: &DrawCommand| match c {
282            DrawCommand::Rect { style, .. } => style.clone(),
283            _ => unreachable!(),
284        };
285        let a0 = style_of(&got[0]);
286        let a1 = style_of(&got[1]);
287        let a2 = style_of(&got[2]);
288        assert!(Arc::ptr_eq(&a0, &a1));
289        assert!(Arc::ptr_eq(&a1, &a2));
290        assert_eq!(a0.radius.top_left, 12.0);
291    }
292
293    #[test]
294    fn scale_into_reuses_buffer_across_frames() {
295        let style = Arc::new(RectStyle::default());
296        let cmds = vec![rect_cmd(&style, 0.0), rect_cmd(&style, 10.0)];
297        let mut scratch = ScaleScratch::new();
298        let _ = scratch.scale_into(&cmds, 2.0);
299        let cap = scratch.storage.capacity();
300        assert!(cap >= cmds.len());
301        let _ = scratch.scale_into(&cmds, 2.0);
302        // Buffer capacity persists between frames: no per-frame Vec reallocation for the same command count.
303        assert_eq!(scratch.storage.capacity(), cap);
304    }
305}