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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
//! Glyph-run lowering: a `frust_scene::GlyphRun` becomes engine strips.
//!
//! Text is the one display-list primitive the compiler does not rasterize
//! itself. A glyph is a font outline that has to be fetched, scaled, hinted,
//! emboldened and cached before it is a path at all, and `glifo` — the same
//! crate the sparse-strip reference renderers drive their text through — is
//! where all of that already lives. This module is the adapter between the
//! two: it turns one display-list run into one
//! [`GlyphRunBuilder`](glifo::GlyphRunBuilder), points that builder at the
//! compiler's own strip generator through [`backend`], and hands back what the
//! run cost.
//!
//! # No conversion at the seam
//!
//! The display list's [`FontHandle`](frust_scene::FontHandle) wraps a
//! `peniko::FontData`, which is exactly what
//! [`GlyphRunBuilder::new`](glifo::GlyphRunBuilder::new) takes, and `glifo`
//! keys its caches on that blob's own process-unique id. So a run reaches
//! `glifo` by handing over the font it already carries — there is no font
//! registry in between, no re-parse, and no identity of the engine's own
//! invention that two paths could disagree about. A run's glyphs convert the
//! same way: `{id, x, y}` on both sides, mapped one for one.
//!
//! # Parity with the classic tier
//!
//! `frust-text` drops parley's synthesis when it builds a run — no synthetic
//! oblique, no synthetic bold, no variation coordinates — so the run this
//! module builds states that explicitly rather than inheriting whatever
//! `glifo`'s defaults happen to be: no normalized coordinates, and the default
//! (no-op) embolden. Hinting is the one knob set *against* the default:
//! `glifo` hints by default, the classic tier does not hint at all, and a
//! hinted outline lands on different pixels — so a run built here states its
//! own hinting choice explicitly too, rather than inheriting `glifo`'s.
//!
//! # The hinting policy, split in two
//!
//! Whether a run ends up hinted is two independent questions, answered on two
//! sides of the `glifo` boundary:
//!
//! - **Device class**, this crate's half: [`lower_glyph_run`]'s `hint`
//! parameter, which [`SceneCompiler`](crate::compile::SceneCompiler) derives
//! from the adapter's `TierCaps` in
//! [`SceneCompiler::for_caps`](crate::compile::SceneCompiler::for_caps) —
//! on for a desktop-class adapter, off for a mobile one, off by default
//! until an adapter is known (the same caution
//! [`AtlasBudget::MOBILE`](crate::cache::AtlasBudget::MOBILE) is chosen
//! for). Nothing here decides *when* a hinted outline would actually help;
//! it only decides whether the device is one hinting is worth paying for at
//! all.
//! - **Transform shape**, `glifo`'s own half and not reimplemented here:
//! [`GlyphRunBuilder::hint`] documents that hinting is applied only when the
//! run's combined transform is a positive uniform scale with no vertical
//! skew or rotation, and falls back to an unhinted direct draw otherwise —
//! vertical-only hinting cannot answer for a transform it cannot express as
//! a single vertical scale. A rotated or skewed run is therefore unhinted
//! regardless of what `hint` this module passes, on `glifo`'s own terms.
//!
//! # Cost
//!
//! The outline and hinting caches are retained across frames by the compiler
//! (they are the reason a steady-state frame of text costs no font-table work
//! at all), and maintained once per compiled frame. What a *glyph* costs on top
//! of that is [`atlas_policy`]'s decision, made once per run before the run is
//! lowered and carried into `glifo` as the
//! [`AtlasCacher`](glifo::AtlasCacher) [`lower_glyph_run`] is handed:
//!
//! - **Routed to the atlas.** `glifo` resolves each glyph against the policy's
//! [`GlyphAtlas`](glifo::GlyphAtlas), and a hit is one image draw sampling
//! the slot — no outline fetched, no path flattened, no coverage generated.
//! A miss allocates a slot out of the *shared* atlas allocator, records the
//! fills that rasterize it into that page's command recorder, and draws the
//! slot anyway; the recorded pages are replayed into the atlas by
//! [`crate::gpu::atlas::AtlasRenderer`] before the frame's scene pass, which
//! is the ordering that makes sampling a just-filled slot sound. So a page of
//! static text pays its rasterization on one frame and nothing on the next
//! thousand.
//! - **Routed to outlines.** Every glyph is fetched, scaled and rasterized as
//! strips, exactly as before — one draw per glyph, no texture residency at
//! all. This is what an animating size, an oversized run, an unusable size, a
//! [colour face](font_has_color_glyphs), a transform `glifo` will not absorb
//! a scale out of, a full glyph residency and `FRUST_ENGINE_NO_ATLAS` all
//! get, and it is also where a glyph the atlas had no room for lands on its
//! own. Correct pixels, slower — never wrong ones.
//!
//! The two paths are not pixel-identical by construction and are not claimed to
//! be: an atlas glyph is rasterized once at its quantized size and sampled,
//! an outline glyph is rasterized per frame at its exact transform.
//!
//! The *size* that quantization applies to is the device one — `font_size` with
//! the run's own transform scale absorbed into it, which is the quantity
//! `glifo` builds its key from. [`atlas_policy::device_font_size`] is where that
//! is restated, and its module doc has the whole reason a guard watching the
//! display list's `font_size` alone watches the wrong number.
//!
//! # Which glyphs never reach the atlas at all
//!
//! A colour (COLR) glyph is not merely uncacheable on this tier — cached, it is
//! *destructive*: `glifo` records it as a clip bracket around a colour-layer
//! stream, into the command recorder shared by every glyph on its atlas page,
//! and a replay that cannot lower a clip loses the whole page while its entries
//! stay resident pointing at texels nothing wrote. `glifo` 0.3.0 has no way to
//! withdraw them afterwards, so the run is refused the route beforehand:
//! [`font_has_color_glyphs`] reads the face's table directory once per run, and
//! a face carrying `COLR` draws every glyph through [`color`]'s layer
//! recombination instead.
//!
//! # The font gate
//!
//! `glifo` takes the font blob as read: it parses the face and reads its
//! `head` table with an `unwrap` on each, so a display list carrying a handle
//! whose blob is not a font — an unloaded resource, a truncated read, a
//! placeholder — would panic the frame path rather than return an
//! [`EngineError`](crate::EngineError) (E17). [`font_is_readable`] is the gate
//! that keeps it out: a run whose face fails it is refused whole, before its
//! brush is even encoded, and its glyphs are counted as skipped. The residual
//! `unwrap`s inside `glifo` — a corrupt outline that fails to draw, a colour
//! layer whose gradient carries no stops — are reachable only from a face that
//! parses and are shared with the reference renderers on the same pinned
//! version; the gate closes the class a plain display list can actually
//! deliver.
pub
pub
pub
use ;
use Affine;
use FontData;
use Paint;
use StripGenerator;
use GlyphRun;
use crateImageResidency;
use crateClipStack;
use crate;
use crateconfig;
pub use AtlasPolicy;
pub use ;
pub use ;
use ;
/// The pieces of a compile in progress a glyph run is drawn against.
///
/// Grouped into one value rather than passed as five arguments because they
/// are borrowed disjointly out of the compiler and the frame it is filling,
/// and naming that split in one place is what keeps the lowering free of
/// borrow gymnastics at the call site.
pub
/// Draw `run` into `targets`, painted with the already-encoded `paint`.
///
/// `transform` is the run's own transform composed with the frame root — the
/// full device-space mapping — and is what `glifo` derives every glyph's draw
/// transform from. `paint` is the run's brush as the compiler encoded it once
/// against that same transform (see [`backend`]'s module doc for why once is
/// enough), and `context_brush` is the brush it was encoded from, which
/// `glifo` reads back for a colour glyph's context colour. `hint` is the
/// device-class half of the hinting policy this module's doc splits out —
/// [`SceneCompiler`](crate::compile::SceneCompiler)'s own choice, passed
/// through unchanged; `glifo` still applies its transform-shape half on top
/// (see the module doc).
///
/// `cacher` is [`atlas_policy`]'s answer for this run, already decided:
/// [`AtlasCacher::Enabled`] over the policy's own entry map and the shared
/// atlas allocator for a run it routed to the atlas, [`AtlasCacher::Disabled`]
/// for one it routed to outlines. Nothing here re-decides it — the
/// classification happens once, before the brush is even encoded, so a run
/// cannot be routed one way by the policy and drawn the other.
///
/// Returns what the run cost: how many glyphs became draws, and how many were
/// refused.
pub
/// The glyph-atlas policy for `images`' allocator.
///
/// The policy packs into the residency's own
/// [`ImageCache`](vello_common::image_cache::ImageCache) — the process's single
/// atlas allocator — rather than one of its own, so a glyph slot and an image
/// slot can never be handed the same `ImageId` or overlapping texels of the
/// same layer. [`crate::cache::images`] states why that has to be one cache;
/// here it is simply where the policy's pages come from.
///
/// The tier decision therefore arrives already made. That geometry is
/// [`AtlasBudget::for_caps`](crate::cache::AtlasBudget::for_caps)' — mobile
/// `(1024, 1024)` x4 layers, desktop `(2048, 2048)` x8,
/// `FRUST_ENGINE_ATLAS_SIZE` redistributing that tier's own allowance between
/// extent and depth, then clamped to what the adapter can actually create —
/// applied when the residency was built, and read back off the allocator rather
/// than derived a second time from the same `TierCaps`. Deriving it twice is
/// exactly the shape that let the two halves disagree.
///
/// `FRUST_ENGINE_NO_ATLAS` is read here, once per policy, rather than per run —
/// it is a process-global kill switch ([`config::atlas_disabled`]), and a run
/// that consulted it separately could not be told from one the size tracker
/// refused. A residency that is disabled for any other reason takes the glyphs
/// with it: the two classes share one array, and caching glyphs into an atlas
/// the images were denied would be one class living in a texture the other was
/// told does not exist.
pub
/// The OpenType table directory's own fixed header length.
const TABLE_DIRECTORY_LEN: usize = 12;
/// One table record: tag, checksum, offset, length.
const TABLE_RECORD_LEN: usize = 16;
/// The `head` table's fixed length.
const HEAD_TABLE_LEN: usize = 54;
/// Byte offset of `unitsPerEm` inside the `head` table.
const HEAD_UNITS_PER_EM: usize = 18;
/// `head`, as a table record's tag reads.
const HEAD_TAG: = *b"head";
/// `COLR`, on the same terms — the colour-glyph table
/// [`font_has_color_glyphs`] looks for.
const COLR_TAG: = *b"COLR";
/// A font collection's own file tag.
const TTC_TAG: = *b"ttcf";
/// Byte offset of a collection header's major version. Only versions 1 and 2
/// are defined; anything else is a collection this backend does not
/// understand and refuses rather than guesses at.
const TTC_MAJOR_VERSION: usize = 4;
/// Byte offset of a collection header's font count.
const TTC_NUM_FONTS: usize = 8;
/// Byte offset of a collection header's first table-directory offset.
const TTC_OFFSETS: usize = 12;
/// The three single-font file tags: TrueType outlines, CFF outlines, and the
/// legacy Apple TrueType tag.
const SFNT_TAGS: = ;
/// Whether `font`'s blob is a face this backend can hand to `glifo` without
/// tripping one of its `unwrap`s (see this module's doc).
///
/// Answers the two questions `glifo` asks and does not check: does the blob
/// parse as a font at `font.index`, and does that font carry a `head` table
/// with a usable units-per-em? A zero units-per-em passes the parse and then
/// divides every glyph transform by nothing, so it is refused here alongside a
/// missing table rather than allowed to become a non-finite transform later.
///
/// Deliberately stricter than the parser it stands in front of, never looser:
/// it accepts only a known file tag whose table directory and named records
/// all lie inside the blob, which is a subset of what the parser itself will
/// take. A face this refuses but the parser would have accepted loses its
/// text; a face this accepted but the parser would have rejected would lose
/// the frame, which is the failure worth being conservative about.
pub
/// Where `index`'s table directory starts inside `data`, or `None` when the
/// blob is not a font file this index names one in.
/// Whether `font`'s face carries a `COLR` table, and so could hand `glifo` a
/// colour glyph.
///
/// The gate that keeps colour glyphs off the atlas route — see
/// [`atlas_policy`]'s module doc for why a cached COLR glyph voids the whole
/// atlas page it lands on, and why `glifo` 0.3.0 offers no way to undo that
/// after the fact. A face this answers `true` for has every one of its runs
/// routed to outlines, which is where `text::color` recombines a colour glyph's
/// layers into engine shapes.
///
/// `COLR` alone, because `COLR` alone is what `glifo` looks at: it resolves
/// colour glyphs through `skrifa`'s `color_glyphs()`, which reads COLRv0/v1 and
/// nothing else. A bitmap strike (`CBDT`/`sbix`) reaches the atlas as a
/// pre-rasterized pixmap through the upload queue rather than as recorded
/// commands, so it dirties no page and needs no gate; an `SVG ` table is not a
/// path this backend takes at all.
///
/// Answers per face, not per glyph: `glifo` decides colour-glyph caching from
/// the presence of a cacher, so there is no way to offer a run's outlines the
/// atlas while holding its colour glyphs back. Refusing the whole face is
/// conservative in the direction that costs speed rather than pixels — an
/// emoji font's Latin glyphs, if it has any, rasterize per frame.
pub
/// Where the record for `tag` starts inside the table directory at
/// `directory`, or `None` when the directory names no such table.
///
/// The first record carrying the tag, which is the one the parser resolves it
/// to whether or not the directory is sorted.
/// The units-per-em of the `head` table named by the directory at
/// `directory`, or `None` when there is no readable one.
/// The big-endian `u16` at `at`, or `None` when it does not fit.
/// The big-endian `u32` at `at`, or `None` when it does not fit.