1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
//! Benchmark timing tests for glyph rasterization.
//!
//! These are regular `#[test]` functions that measure wall-clock time and print
//! `eprintln!` timing summaries (visible with `cargo nextest run -- --nocapture`
//! or `cargo test -- --nocapture`). They assert only that each operation
//! completes within a generous time budget, so they never flap on slow CI.
#[cfg(test)]
mod tests {
use crate::{FontdueRaster, LcdFilterKernel, RasterBackend};
use std::path::Path;
/// Load the project test font, falling back to system fonts and then the
/// statically bundled Noto Sans Regular when fixtures are absent.
fn load_test_font() -> Vec<u8> {
let fixture =
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/test-font.ttf");
if fixture.exists() {
return std::fs::read(&fixture).expect("read fixture font");
}
let candidates = [
"/Library/Fonts/Arial Unicode.ttf",
"/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf",
];
for p in &candidates {
if Path::new(p).exists() {
return std::fs::read(p).expect("read system font");
}
}
// Fall back to statically bundled Noto Sans Regular for deterministic,
// system-font-independent testing.
oxifont_bundled::NOTO_SANS_REGULAR.to_vec()
}
/// Benchmark rasterization of 200 unique glyphs at 16 px, 32 px, and 64 px.
///
/// Checks that the entire batch finishes in under 30 seconds at every size
/// (generous budget — on a modern laptop each run takes < 50 ms).
#[test]
#[ignore]
fn bench_rasterize_1000_glyphs_multisize() {
use std::time::Instant;
let font_data = load_test_font();
let raster = FontdueRaster::new();
let n: u16 = 200;
for &px_size in &[16.0_f32, 32.0, 64.0] {
let start = Instant::now();
let mut visible = 0usize;
for gid in 0..n {
let out = raster.rasterize(&font_data, gid, px_size);
if out.width > 0 && out.height > 0 {
visible += 1;
}
}
let elapsed = start.elapsed();
eprintln!(
"[bench] rasterize {} glyphs at {}px: {:?} ({:?}/glyph) visible={}",
n,
px_size as u32,
elapsed,
elapsed / u32::from(n),
visible,
);
#[cfg(not(debug_assertions))]
assert!(
elapsed.as_secs() < 30,
"rasterization took too long at {}px: {:?}",
px_size as u32,
elapsed
);
}
}
/// Compare fontdue vs ab_glyph throughput for printable-ASCII glyph IDs.
///
/// Both backends must complete within 10 seconds; relative performance is
/// printed but not asserted.
#[cfg(feature = "ab-glyph-backend")]
#[test]
#[ignore]
fn bench_fontdue_vs_abglyph_throughput() {
use crate::backend::AbGlyphRaster;
use std::time::Instant;
let font_data = load_test_font();
let glyph_ids: Vec<u16> = (32..96).collect();
let n = glyph_ids.len() as u32;
let px_size = 24.0_f32;
// Fontdue
let fontdue = FontdueRaster::new();
let start = Instant::now();
for &gid in &glyph_ids {
let _ = fontdue.rasterize(&font_data, gid, px_size);
}
let fontdue_time = start.elapsed();
// ab_glyph
let abglyph = AbGlyphRaster;
let start = Instant::now();
for &gid in &glyph_ids {
let _ = abglyph.rasterize(&font_data, gid, px_size);
}
let abglyph_time = start.elapsed();
eprintln!(
"[bench] fontdue {} glyphs @ {}px: {:?} ({:?}/glyph)",
n,
px_size as u32,
fontdue_time,
fontdue_time / n,
);
eprintln!(
"[bench] ab_glyph {} glyphs @ {}px: {:?} ({:?}/glyph)",
n,
px_size as u32,
abglyph_time,
abglyph_time / n,
);
#[cfg(not(debug_assertions))]
assert!(
fontdue_time.as_secs() < 10,
"fontdue took too long: {:?}",
fontdue_time
);
#[cfg(not(debug_assertions))]
assert!(
abglyph_time.as_secs() < 10,
"ab_glyph took too long: {:?}",
abglyph_time
);
}
/// Benchmark LCD subpixel rasterization vs standard greyscale.
///
/// Uses glyph IDs 36–59 (a small ASCII subset) at 16 px. Both pipelines
/// must complete within 10 seconds; relative timings are printed.
#[test]
#[ignore]
fn bench_lcd_vs_greyscale_rasterization() {
use crate::lcd::rasterize_lcd;
use std::time::Instant;
let font_data = load_test_font();
let glyph_ids: Vec<u16> = (36..60).collect();
let n = glyph_ids.len() as u32;
let px_size = 16.0_f32;
// Standard greyscale via fontdue
let raster = FontdueRaster::new();
let start = Instant::now();
for &gid in &glyph_ids {
let _ = raster.rasterize(&font_data, gid, px_size);
}
let grey_time = start.elapsed();
// LCD subpixel via ab_glyph (rasterize_lcd always uses ab_glyph internally)
let start = Instant::now();
for &gid in &glyph_ids {
let _ = rasterize_lcd(
&font_data,
gid,
px_size,
LcdFilterKernel::FreeType5Tap,
false,
);
}
let lcd_time = start.elapsed();
eprintln!(
"[bench] greyscale {} glyphs @ {}px: {:?} ({:?}/glyph)",
n,
px_size as u32,
grey_time,
grey_time / n,
);
eprintln!(
"[bench] LCD {} glyphs @ {}px: {:?} ({:?}/glyph)",
n,
px_size as u32,
lcd_time,
lcd_time / n,
);
#[cfg(not(debug_assertions))]
assert!(
grey_time.as_secs() < 10,
"greyscale took too long: {:?}",
grey_time
);
#[cfg(not(debug_assertions))]
assert!(lcd_time.as_secs() < 10, "LCD took too long: {:?}", lcd_time);
}
}