Skip to content

Commit ebce871

Browse files
committed
parley: Implement caching of parley layouts
The result of shaping is cached in a per-renderer ItemCache. The cache can't be used for the size hints needed for layouts (as the word break changes as is used for shaping). The line breaking is also not re-used, but nevertheless this should reduce the amount of time spent rendering text. cc #10087 cc #2306
1 parent 2be0a57 commit ebce871

File tree

12 files changed

+569
-54
lines changed

12 files changed

+569
-54
lines changed

api/rs/slint/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ serde = { workspace = true }
318318
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "net", "io-util"] }
319319
async-compat = { version = "0.2.4" }
320320
bytemuck = { workspace = true }
321+
i-slint-renderer-software = { path = "../../../internal/renderers/software", features = ["testing"] }
321322

322323
[target.'cfg(target_os = "linux")'.dependencies]
323324
# this line is there to add the "enable" feature by default, but only on linux
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
// Copyright © SixtyFPS GmbH <info@slint.dev>
2+
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3+
4+
use slint::PhysicalSize;
5+
use slint::platform::software_renderer::{
6+
MinimalSoftwareWindow, PremultipliedRgbaColor, RepaintBufferType, SoftwareRenderer, TargetPixel,
7+
};
8+
use slint::platform::{PlatformError, WindowAdapter};
9+
use std::rc::Rc;
10+
11+
thread_local! {
12+
static WINDOW: Rc<MinimalSoftwareWindow> =
13+
MinimalSoftwareWindow::new(RepaintBufferType::ReusedBuffer);
14+
}
15+
16+
struct TestPlatform;
17+
impl slint::platform::Platform for TestPlatform {
18+
fn create_window_adapter(&self) -> Result<Rc<dyn WindowAdapter>, PlatformError> {
19+
Ok(WINDOW.with(|x| x.clone()))
20+
}
21+
}
22+
23+
#[derive(Clone, Copy, Default)]
24+
struct TestPixel(bool);
25+
26+
impl TargetPixel for TestPixel {
27+
fn blend(&mut self, _color: PremultipliedRgbaColor) {
28+
*self = Self(true);
29+
}
30+
31+
fn from_rgb(_red: u8, _green: u8, _blue: u8) -> Self {
32+
Self(true)
33+
}
34+
}
35+
36+
const WIDTH: usize = 200;
37+
const HEIGHT: usize = 100;
38+
39+
fn setup() -> Rc<MinimalSoftwareWindow> {
40+
slint::platform::set_platform(Box::new(TestPlatform)).ok();
41+
let window = WINDOW.with(|x| x.clone());
42+
window.set_size(PhysicalSize::new(WIDTH as u32, HEIGHT as u32));
43+
window
44+
}
45+
46+
fn render_and_get_miss_count(renderer: &SoftwareRenderer) -> u64 {
47+
renderer.text_layout_cache().reset_cache_miss_count();
48+
let mut buf = vec![TestPixel(false); WIDTH * HEIGHT];
49+
renderer.render(buf.as_mut_slice(), WIDTH);
50+
renderer.text_layout_cache().cache_miss_count()
51+
}
52+
53+
#[test]
54+
fn cache_hit_avoids_reshaping() {
55+
let window = setup();
56+
57+
slint::slint! {
58+
export component TestComponent inherits Window {
59+
Text {
60+
text: "Hello World";
61+
}
62+
}
63+
}
64+
65+
let ui = TestComponent::new().unwrap();
66+
ui.show().unwrap();
67+
68+
let mut miss_count = 0u64;
69+
70+
// First render: should shape at least once
71+
assert!(window.draw_if_needed(|renderer| {
72+
miss_count = render_and_get_miss_count(renderer);
73+
}));
74+
assert!(miss_count > 0, "Expected at least one cache miss on first render");
75+
76+
// Second render without changes: should hit cache
77+
window.request_redraw();
78+
assert!(window.draw_if_needed(|renderer| {
79+
miss_count = render_and_get_miss_count(renderer);
80+
}));
81+
assert_eq!(miss_count, 0, "Expected zero cache misses on re-render without changes");
82+
}
83+
84+
#[test]
85+
fn text_change_invalidates_cache() {
86+
let window = setup();
87+
88+
slint::slint! {
89+
export component TestComponent inherits Window {
90+
in property <string> label: "Hello";
91+
Text {
92+
text: label;
93+
}
94+
}
95+
}
96+
97+
let ui = TestComponent::new().unwrap();
98+
ui.show().unwrap();
99+
100+
// First render
101+
window.draw_if_needed(|renderer| {
102+
render_and_get_miss_count(renderer);
103+
});
104+
105+
// Change text
106+
ui.set_label("Goodbye".into());
107+
108+
let mut miss_count = 0u64;
109+
assert!(window.draw_if_needed(|renderer| {
110+
miss_count = render_and_get_miss_count(renderer);
111+
}));
112+
assert!(miss_count > 0, "Expected cache miss after text change");
113+
}
114+
115+
#[test]
116+
fn font_size_change_invalidates_cache() {
117+
let window = setup();
118+
119+
slint::slint! {
120+
export component TestComponent inherits Window {
121+
in property <length> size: 16px;
122+
Text {
123+
text: "Hello";
124+
font-size: size;
125+
}
126+
}
127+
}
128+
129+
let ui = TestComponent::new().unwrap();
130+
ui.show().unwrap();
131+
132+
// First render
133+
window.draw_if_needed(|renderer| {
134+
render_and_get_miss_count(renderer);
135+
});
136+
137+
// Change font-size
138+
ui.set_size(24.0);
139+
140+
let mut miss_count = 0u64;
141+
assert!(window.draw_if_needed(|renderer| {
142+
miss_count = render_and_get_miss_count(renderer);
143+
}));
144+
assert!(miss_count > 0, "Expected cache miss after font-size change");
145+
}
146+
147+
#[test]
148+
fn font_weight_change_invalidates_cache() {
149+
let window = setup();
150+
151+
slint::slint! {
152+
export component TestComponent inherits Window {
153+
in property <int> weight: 400;
154+
Text {
155+
text: "Hello";
156+
font-weight: weight;
157+
}
158+
}
159+
}
160+
161+
let ui = TestComponent::new().unwrap();
162+
ui.show().unwrap();
163+
164+
// First render
165+
window.draw_if_needed(|renderer| {
166+
render_and_get_miss_count(renderer);
167+
});
168+
169+
// Change font-weight
170+
ui.set_weight(700);
171+
172+
let mut miss_count = 0u64;
173+
assert!(window.draw_if_needed(|renderer| {
174+
miss_count = render_and_get_miss_count(renderer);
175+
}));
176+
assert!(miss_count > 0, "Expected cache miss after font-weight change");
177+
}
178+
179+
#[test]
180+
fn wrap_change_invalidates_cache() {
181+
let window = setup();
182+
183+
slint::slint! {
184+
export component TestComponent inherits Window {
185+
in property <bool> use-no-wrap: false;
186+
Text {
187+
text: "Hello World this is a long text";
188+
wrap: use-no-wrap ? no-wrap : word-wrap;
189+
}
190+
}
191+
}
192+
193+
let ui = TestComponent::new().unwrap();
194+
ui.show().unwrap();
195+
196+
// First render (word-wrap)
197+
window.draw_if_needed(|renderer| {
198+
render_and_get_miss_count(renderer);
199+
});
200+
201+
// Change wrap to no-wrap
202+
ui.set_use_no_wrap(true);
203+
204+
let mut miss_count = 0u64;
205+
assert!(window.draw_if_needed(|renderer| {
206+
miss_count = render_and_get_miss_count(renderer);
207+
}));
208+
assert!(miss_count > 0, "Expected cache miss after wrap change");
209+
}
210+
211+
#[test]
212+
fn alignment_change_does_not_reshape() {
213+
let window = setup();
214+
215+
slint::slint! {
216+
export component TestComponent inherits Window {
217+
in property <bool> use-center-align: false;
218+
Text {
219+
text: "Hello World";
220+
horizontal-alignment: use-center-align ? TextHorizontalAlignment.center : TextHorizontalAlignment.left;
221+
}
222+
}
223+
}
224+
225+
let ui = TestComponent::new().unwrap();
226+
ui.show().unwrap();
227+
228+
// First render (left-aligned)
229+
window.draw_if_needed(|renderer| {
230+
render_and_get_miss_count(renderer);
231+
});
232+
233+
// Change alignment to center
234+
ui.set_use_center_align(true);
235+
236+
let mut miss_count = 0u64;
237+
assert!(window.draw_if_needed(|renderer| {
238+
miss_count = render_and_get_miss_count(renderer);
239+
}));
240+
assert_eq!(miss_count, 0, "Alignment change should not cause reshaping");
241+
}
242+
243+
#[test]
244+
fn overflow_change_does_not_reshape() {
245+
let window = setup();
246+
247+
slint::slint! {
248+
export component TestComponent inherits Window {
249+
in property <bool> use-elide: false;
250+
Text {
251+
text: "Hello World";
252+
overflow: use-elide ? TextOverflow.elide : TextOverflow.clip;
253+
}
254+
}
255+
}
256+
257+
let ui = TestComponent::new().unwrap();
258+
ui.show().unwrap();
259+
260+
// First render (clip)
261+
window.draw_if_needed(|renderer| {
262+
render_and_get_miss_count(renderer);
263+
});
264+
265+
// Change overflow to elide
266+
ui.set_use_elide(true);
267+
268+
let mut miss_count = 0u64;
269+
assert!(window.draw_if_needed(|renderer| {
270+
miss_count = render_and_get_miss_count(renderer);
271+
}));
272+
assert_eq!(miss_count, 0, "Overflow change should not cause reshaping");
273+
}
274+
275+
#[test]
276+
fn color_change_does_not_reshape() {
277+
let window = setup();
278+
279+
slint::slint! {
280+
export component TestComponent inherits Window {
281+
in property <color> text-color: black;
282+
Text {
283+
text: "Hello World";
284+
color: text-color;
285+
}
286+
}
287+
}
288+
289+
let ui = TestComponent::new().unwrap();
290+
ui.show().unwrap();
291+
292+
// First render
293+
window.draw_if_needed(|renderer| {
294+
render_and_get_miss_count(renderer);
295+
});
296+
297+
// Change color
298+
ui.set_text_color(slint::Color::from_rgb_u8(255, 0, 0));
299+
300+
let mut miss_count = 0u64;
301+
assert!(window.draw_if_needed(|renderer| {
302+
miss_count = render_and_get_miss_count(renderer);
303+
}));
304+
assert_eq!(miss_count, 0, "Color change should not cause reshaping");
305+
}

internal/backends/testing/testing_backend.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ impl RendererSealed for TestingWindow {
226226
let height = num_lines as f32 * pixel_size;
227227
LogicalSize::new(width, height)
228228
} else {
229-
sharedparley::text_size(self, text_item, item_rc, max_width, text_wrap)
229+
sharedparley::text_size(self, text_item, item_rc, max_width, text_wrap, None)
230230
.unwrap_or_default()
231231
}
232232
}

internal/core/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ svg = ["dep:resvg"]
5656

5757
box-shadow-cache = []
5858

59+
testing = []
60+
5961
shared-fontique = ["i-slint-common/shared-fontique"]
6062

6163
raw-window-handle-06 = ["dep:raw-window-handle-06"]

internal/core/items/text.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ impl Item for StyledTextItem {
304304
self_rc,
305305
LogicalSize::from_lengths(self.width(), self.height()),
306306
*position * scale_factor,
307+
None, // No cache available from item event handler
307308
) {
308309
Self::FIELD_OFFSETS.link_clicked.apply_pin(self).call(&(link.into(),));
309310
}

0 commit comments

Comments
 (0)