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