Skip to main content

webrender/prim_store/
text_run.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use api::{ColorF, FontInstanceFlags, GlyphInstance, RasterSpace, Shadow, GlyphIndex};
6use api::units::{LayoutToWorldTransform, DevicePixelScale};
7use api::units::*;
8use crate::scene_building::{CreateShadow, IsVisible};
9use glyph_rasterizer::{FontInstance, FontTransform, GlyphKey, SubpixelDirection, FONT_SIZE_LIMIT};
10use crate::intern;
11use crate::internal_types::LayoutPrimitiveInfo;
12use crate::picture::SurfaceInfo;
13use crate::prim_store::PrimitiveScratchBuffer;
14use crate::prim_store::{PrimitiveStore, PrimKeyCommonData, PrimTemplateCommonData};
15use crate::renderer::{GpuBufferAddress, GpuBufferBuilderF, MAX_VERTEX_TEXTURE_WIDTH};
16use crate::resource_cache::ResourceCache;
17use crate::util::MatrixHelpers;
18use crate::prim_store::{InternablePrimitive, PrimitiveKind, LayoutPointAu};
19use crate::spatial_tree::{SpatialTree, SpatialNodeIndex};
20use std::ops;
21
22use super::storage;
23
24#[cfg_attr(feature = "capture", derive(Serialize))]
25#[cfg_attr(feature = "replay", derive(Deserialize))]
26#[derive(Debug, Clone, Eq, MallocSizeOf, PartialEq, Hash)]
27pub struct GlyphInstanceAu {
28    pub index: GlyphIndex,
29    pub point: LayoutPointAu,
30}
31
32/// A run of glyphs, with associated font information.
33#[cfg_attr(feature = "capture", derive(Serialize))]
34#[cfg_attr(feature = "replay", derive(Deserialize))]
35#[derive(Debug, Clone, Eq, MallocSizeOf, PartialEq, Hash)]
36pub struct TextRunKey {
37    pub common: PrimKeyCommonData,
38    pub font: FontInstance,
39    /// Glyph pen positions, each relative to the *normalized* prim rect
40    /// origin (`prim_info.rect.min`). Storing relative to the normalized
41    /// origin keeps the intern key stable across pre-scroll offset changes,
42    /// since the external scroll offset cancels: both the glyph position and
43    /// the prim origin are normalized the same way (see `add_text`).
44    pub glyphs: Vec<GlyphInstanceAu>,
45    pub shadow: bool,
46    pub requested_raster_space: RasterSpace,
47}
48
49impl TextRunKey {
50    pub fn new(
51        info: &LayoutPrimitiveInfo,
52        text_run: TextRun,
53    ) -> Self {
54        let glyphs = text_run
55            .glyphs
56            .iter()
57            .map(|glyph| {
58                GlyphInstanceAu {
59                    index: glyph.index,
60                    point: glyph.point.to_au(),
61                }
62            })
63            .collect();
64
65        TextRunKey {
66            common: info.into(),
67            font: text_run.font,
68            glyphs,
69            shadow: text_run.shadow,
70            requested_raster_space: text_run.requested_raster_space,
71        }
72    }
73}
74
75impl intern::InternDebug for TextRunKey {}
76
77#[cfg_attr(feature = "capture", derive(Serialize))]
78#[cfg_attr(feature = "replay", derive(Deserialize))]
79#[derive(MallocSizeOf)]
80pub struct TextRunTemplate {
81    pub common: PrimTemplateCommonData,
82    pub font: FontInstance,
83    /// Glyph pen positions, each relative to the normalized prim rect origin.
84    /// See [`TextRunKey::glyphs`]. At frame time the normalized local glyph
85    /// position is `prim_rect.min + glyph.point`; `request_resources` then
86    /// transforms and device-snaps each glyph to produce the device-space
87    /// offsets handed to the shader.
88    pub glyphs: Vec<GlyphInstance>,
89    pub shadow: bool,
90    pub requested_raster_space: RasterSpace,
91}
92
93impl ops::Deref for TextRunTemplate {
94    type Target = PrimTemplateCommonData;
95    fn deref(&self) -> &Self::Target {
96        &self.common
97    }
98}
99
100impl ops::DerefMut for TextRunTemplate {
101    fn deref_mut(&mut self) -> &mut Self::Target {
102        &mut self.common
103    }
104}
105
106impl From<TextRunKey> for TextRunTemplate {
107    fn from(item: TextRunKey) -> Self {
108        let common = PrimTemplateCommonData::with_key_common(item.common);
109        let glyphs = item
110            .glyphs
111            .iter()
112            .map(|glyph| {
113                GlyphInstance {
114                    index: glyph.index,
115                    point: LayoutPoint::from_au(glyph.point),
116                }
117            })
118            .collect();
119
120        TextRunTemplate {
121            common,
122            font: item.font,
123            glyphs,
124            shadow: item.shadow,
125            requested_raster_space: item.requested_raster_space,
126        }
127    }
128}
129
130impl TextRunTemplate {
131    /// Write the per-instance GPU blocks for this run: the premultiplied
132    /// font color followed by the per-glyph offsets (two glyphs packed per
133    /// block). The offsets are device-space in device mode and raster-space in
134    /// local-raster mode (see `request_resources`). Corresponds to
135    /// `fetch_glyph` / `fetch_text_run` in the shader.
136    fn write_prim_gpu_blocks(
137        &self,
138        glyph_offsets: &[DeviceVector2D],
139        gpu_buffer: &mut GpuBufferBuilderF,
140    ) -> GpuBufferAddress {
141        let num_blocks = (glyph_offsets.len() + 1) / 2 + 1;
142        assert!(num_blocks <= MAX_VERTEX_TEXTURE_WIDTH);
143        let mut writer = gpu_buffer.write_blocks(num_blocks);
144        writer.push_one(ColorF::from(self.font.color).premultiplied());
145
146        let mut gpu_block = [0.0; 4];
147        for (i, src) in glyph_offsets.iter().enumerate() {
148            // Two glyphs are packed per GPU block.
149            if (i & 1) == 0 {
150                gpu_block[0] = src.x;
151                gpu_block[1] = src.y;
152            } else {
153                gpu_block[2] = src.x;
154                gpu_block[3] = src.y;
155                writer.push_one(gpu_block);
156            }
157        }
158
159        // Ensure the last block is added in the case
160        // of an odd number of glyphs.
161        if (glyph_offsets.len() & 1) != 0 {
162            writer.push_one(gpu_block);
163        }
164
165        writer.finish()
166    }
167}
168
169pub type TextRunDataHandle = intern::Handle<TextRun>;
170
171#[derive(Debug, MallocSizeOf)]
172#[cfg_attr(feature = "capture", derive(Serialize))]
173#[cfg_attr(feature = "replay", derive(Deserialize))]
174pub struct TextRun {
175    pub font: FontInstance,
176    /// Glyph pen positions, each relative to the normalized prim rect origin.
177    /// See [`TextRunKey::glyphs`].
178    pub glyphs: Vec<GlyphInstance>,
179    pub shadow: bool,
180    pub requested_raster_space: RasterSpace,
181}
182
183impl intern::Internable for TextRun {
184    type Key = TextRunKey;
185    type StoreData = TextRunTemplate;
186    type InternData = ();
187    const PROFILE_COUNTER: usize = crate::profiler::INTERNED_TEXT_RUNS;
188}
189
190impl InternablePrimitive for TextRun {
191    fn into_key(
192        self,
193        info: &LayoutPrimitiveInfo,
194    ) -> TextRunKey {
195        TextRunKey::new(
196            info,
197            self,
198        )
199    }
200
201    fn make_instance_kind(
202        _key: TextRunKey,
203        data_handle: TextRunDataHandle,
204        _prim_store: &mut PrimitiveStore,
205    ) -> PrimitiveKind {
206        PrimitiveKind::TextRun {
207            data_handle,
208        }
209    }
210}
211
212impl CreateShadow for TextRun {
213    fn create_shadow(
214        &self,
215        shadow: &Shadow,
216        blur_is_noop: bool,
217        current_raster_space: RasterSpace,
218    ) -> Self {
219        let mut font = FontInstance {
220            color: shadow.color.into(),
221            ..self.font.clone()
222        };
223        if shadow.blur_radius > 0.0 {
224            font.disable_subpixel_aa();
225        }
226
227        let requested_raster_space = if blur_is_noop {
228            current_raster_space
229        } else {
230            RasterSpace::Local(1.0)
231        };
232
233        TextRun {
234            font,
235            glyphs: self.glyphs.clone(),
236            shadow: true,
237            requested_raster_space,
238        }
239    }
240}
241
242impl IsVisible for TextRun {
243    fn is_visible(&self) -> bool {
244        self.font.color.a > 0
245    }
246}
247
248/// Per-frame scratch data for a TextRun primitive. Holds the snapshot
249/// of font + glyph state captured each frame in `request_resources` and
250/// read by batching. Pushed once per visible TextRun per frame.
251#[derive(Debug)]
252#[cfg_attr(feature = "capture", derive(Serialize))]
253pub struct TextRunScratch {
254    /// Per-frame font instance derived from the specified font + this
255    /// frame's transform + raster space. Carries subpixel direction,
256    /// flags, and the device-space size.
257    pub used_font: FontInstance,
258    /// Range of glyph keys allocated for this run this frame, indexing
259    /// into PrimitiveFrameScratch.glyph_keys.
260    pub glyph_keys_range: storage::Range<GlyphKey>,
261    /// Normalized prim local rect for this run. `.min` is the run anchor:
262    /// the shader transforms it to device space and adds the per-glyph
263    /// device offsets. Stored here so batching emits the identical anchor
264    /// in `PrimitiveHeader.local_rect` that `request_resources` used to
265    /// compute those offsets.
266    pub local_rect: LayoutRect,
267    /// Per-instance GPU buffer address for the color block followed by the
268    /// per-glyph offset blocks (two glyphs per block). In device mode these are
269    /// glyph pen positions snapped to the device grid, relative to the
270    /// transformed anchor; in local-raster mode they are absolute snapped
271    /// raster-space positions. Per-instance because they depend on this frame's
272    /// transform.
273    pub gpu_address: GpuBufferAddress,
274    /// Raster scale used when rasterizing the glyphs (1.0 in device mode; the
275    /// local/zoom scale or oversize-clamp scale in local-raster mode). Passed
276    /// to the shader so it can map raster space back to local.
277    pub raster_scale: f32,
278    /// Whether this run uses local-raster mode (see `request_resources`).
279    pub local_raster: bool,
280}
281
282impl TextRunTemplate {
283    /// Build a per-frame `(used_font, raster_scale)` pair for this text run.
284    /// The result is fresh per frame; nothing persists on the template.
285    fn compute_font_instance(
286        specified_font: &FontInstance,
287        surface: &SurfaceInfo,
288        transform: &LayoutToWorldTransform,
289        allow_subpixel: bool,
290        raster_space: RasterSpace,
291        has_bitmap_strikes: bool,
292    ) -> (FontInstance, f32) {
293        // If local raster space is specified, include that in the scale
294        // of the glyphs that get rasterized.
295        // TODO(gw): Once we support proper local space raster modes, this
296        //           will implicitly be part of the device pixel ratio for
297        //           the (cached) local space surface, and so this code
298        //           will no longer be required.
299        // A bitmap-strike run rasterizes with an identity glyph shape (see below),
300        // so the only way the transform's scale can reach the rasterizer is through
301        // the raster scale. Without it the glyph is rendered at its untransformed
302        // size and the shader scales that, which on the backends that resample at
303        // rasterization time (Core Text, DirectWrite) is visibly soft when scaling
304        // up (bug 2064316). Only screen raster space needs this: a requested - or
305        // zoom/animation derived - local raster space carries its own scale, which
306        // is deliberately decoupled from the current transform.
307        let raster_scale_input = if has_bitmap_strikes && raster_space == RasterSpace::Screen {
308            transform
309                .coplanar_scale_factors()
310                .map_or(1.0, |(sx, sy)| sx.max(sy))
311        } else {
312            raster_space.local_scale().unwrap_or(1.0)
313        }
314        .max(0.001);
315
316        let dps = surface.device_pixel_scale.0;
317        let font_size = specified_font.size.to_f32_px();
318
319        // Small floating point error can accumulate in the raster * device_pixel scale.
320        // Round that to the nearest 100th of a scale factor to remove this error while
321        // still allowing reasonably accurate scale factors when a pinch-zoom is stopped
322        // at a fractional amount.
323        let quantized_scale = (dps * raster_scale_input * 100.0).round() / 100.0;
324        let mut device_font_size = font_size * quantized_scale;
325
326        // Check there is a valid transform that doesn't exceed the font size limit.
327        // Ensure the font is supposed to be rasterized in screen-space.
328        // Only support transforms that can be coerced to simple 2D transforms.
329        // Add texture padding to the rasterized glyph buffer when one anticipates
330        // the glyph will need to be scaled when rendered.
331        // Color bitmap glyphs (Apple Color Emoji and any CBDT/sbix font with
332        // embedded bitmap strikes) can't have any part of the transform baked into
333        // their rasterization: the platform backends force an identity glyph shape
334        // for bitmap fonts, so a rotation or skew is dropped outright and the scale
335        // survives only as the scalar `RasterizedGlyph::scale`, which the device
336        // path multiplies into the atlas footprint. That scalar cannot express a
337        // transform scale at all, so on the device path a scaled emoji is painted
338        // at its untransformed size (too small when scaling up, too big and clipped
339        // when scaling down - bugs 2061411, 2062234), and a rotated one is drawn
340        // axis-aligned at its transformed pen (bug 2055177). Before bug 2044211 the
341        // GPU routed these by glyph format - `GlyphFormat::Bitmap`/`ColorBitmap`
342        // have no `Transformed` variant, so bitmap glyphs never reached the
343        // transform shader and always had the transform applied in local space.
344        // `has_bitmap_strikes` restores that: the caller computes it from the real
345        // font face (the EMBEDDED_BITMAPS instance flag alone is unreliable - on
346        // unix it only reflects a fontconfig preference and is set even for
347        // ordinary outline fonts) and it is true only for a genuine bitmap font
348        // under a transform that isn't a pure translation. Route those through the
349        // local-raster fallback so the shader applies the full transform to the
350        // emoji image, the same way vector glyphs and the manual
351        // filter/isolated-surface workaround do. A pure translation is fine on the
352        // device path, where the scalar scale already lands the glyph at the right
353        // size, so that - the common case - is left untouched.
354        let (use_subpixel_aa, transform_glyphs, texture_padding, oversized) = if raster_space != RasterSpace::Screen ||
355            transform.has_perspective_component() || !transform.has_2d_inverse() ||
356            has_bitmap_strikes
357        {
358            (false, false, true, device_font_size > FONT_SIZE_LIMIT)
359        } else if transform.exceeds_2d_scale((FONT_SIZE_LIMIT / device_font_size) as f64) {
360            (false, false, true, true)
361        } else {
362            (true, !transform.is_simple_2d_translation(), false, false)
363        };
364
365        let mut raster_scale = raster_scale_input;
366        let font_transform = if transform_glyphs {
367            // Get the font transform matrix (skew / scale) from the complete transform.
368            // Fold in the device pixel scale.
369            raster_scale = 1.0;
370            FontTransform::from(transform)
371        } else {
372            if oversized {
373                // Font sizes larger than the limit need to be scaled, thus can't use subpixels.
374                // In this case we adjust the font size and raster space to ensure
375                // we rasterize at the limit, to minimize the amount of scaling.
376                raster_scale = FONT_SIZE_LIMIT / (font_size * dps);
377                device_font_size = FONT_SIZE_LIMIT;
378            }
379            // else: keep raster_scale = raster_scale_input. We may have
380            // changed from RasterSpace::Screen due to a transform with
381            // perspective or without a 2D inverse, or it may have been
382            // RasterSpace::Local all along.
383
384            // Rasterize the glyph without any transform.
385            FontTransform::identity()
386        };
387
388        let mut flags = specified_font.flags;
389        if transform_glyphs {
390            flags |= FontInstanceFlags::TRANSFORM_GLYPHS;
391        }
392        if texture_padding {
393            flags |= FontInstanceFlags::TEXTURE_PADDING;
394        }
395
396        // Construct used font instance from the specified font instance
397        let mut used_font = FontInstance {
398            transform: font_transform,
399            size: device_font_size.into(),
400            flags,
401            ..specified_font.clone()
402        };
403
404        // If using local space glyphs, we don't want subpixel AA.
405        if !allow_subpixel || !use_subpixel_aa {
406            used_font.disable_subpixel_aa();
407
408            // Disable subpixel positioning for oversized glyphs to avoid
409            // thrashing the glyph cache with many subpixel variations of
410            // big glyph textures. A possible subpixel positioning error
411            // is small relative to the maximum font size and thus should
412            // not be very noticeable.
413            if oversized {
414                used_font.disable_subpixel_position();
415            }
416        }
417
418        (used_font, raster_scale)
419    }
420
421    /// Gets the raster space to use when rendering this primitive.
422    /// Usually this would be the requested raster space. However, if
423    /// the primitive's spatial node or one of its ancestors is being pinch zoomed
424    /// then we round it. This prevents us rasterizing glyphs for every minor
425    /// change in zoom level, as that would be too expensive.
426    fn get_raster_space_for_prim(
427        &self,
428        prim_spatial_node_index: SpatialNodeIndex,
429        low_quality_pinch_zoom: bool,
430        device_pixel_scale: DevicePixelScale,
431        spatial_tree: &SpatialTree,
432    ) -> RasterSpace {
433        let prim_spatial_node = spatial_tree.get_spatial_node(prim_spatial_node_index);
434        if prim_spatial_node.is_ancestor_or_self_zooming {
435            if low_quality_pinch_zoom {
436                // In low-quality mode, we set the scale to be 1.0. However, the device-pixel
437                // scale selected for the zoom will be taken into account in the caller to this
438                // function when it's converted from local -> device pixels. Since in this mode
439                // the device-pixel scale is constant during the zoom, this gives the desired
440                // performance while also allowing the scale to be adjusted to a new factor at
441                // the end of a pinch-zoom.
442                RasterSpace::Local(1.0)
443            } else {
444                let root_spatial_node_index = spatial_tree.root_reference_frame_index();
445
446                // For high-quality mode, we quantize the exact scale factor as before. However,
447                // we want to _undo_ the effect of the device-pixel scale on the picture cache
448                // tiles (which changes now that they are raster roots). Divide the rounded value
449                // by the device-pixel scale so that the local -> device conversion has no effect.
450                let scale_factors = spatial_tree
451                    .get_relative_transform(prim_spatial_node_index, root_spatial_node_index)
452                    .scale_factors();
453
454                // Round the scale up to the nearest power of 2, but don't exceed 8.
455                let scale = scale_factors.0.max(scale_factors.1).min(8.0).max(1.0);
456                let rounded_up = 2.0f32.powf(scale.log2().ceil());
457
458                RasterSpace::Local(rounded_up / device_pixel_scale.0)
459            }
460        } else {
461            // Assume that if we have a RasterSpace::Local, it is frequently changing, in which
462            // case we want to undo the device-pixel scale, as we do above.
463            match self.requested_raster_space {
464                RasterSpace::Local(scale) => RasterSpace::Local(scale / device_pixel_scale.0),
465                RasterSpace::Screen => RasterSpace::Screen,
466            }
467        }
468    }
469
470    pub fn request_resources(
471        &self,
472        local_rect: LayoutRect,
473        transform: &LayoutToWorldTransform,
474        surface: &SurfaceInfo,
475        spatial_node_index: SpatialNodeIndex,
476        allow_subpixel: bool,
477        low_quality_pinch_zoom: bool,
478        resource_cache: &mut ResourceCache,
479        gpu_buffer: &mut GpuBufferBuilderF,
480        spatial_tree: &SpatialTree,
481        scratch: &mut PrimitiveScratchBuffer,
482    ) -> storage::Index<TextRunScratch> {
483        let raster_space = self.get_raster_space_for_prim(
484            spatial_node_index,
485            low_quality_pinch_zoom,
486            surface.device_pixel_scale,
487            spatial_tree,
488        );
489
490        // Bitmap fonts need the local-raster fallback under any transform that
491        // isn't a pure translation - a scale as much as a rotation or skew
492        // (bugs 2055177, 2061411). Gate on the cheap transform and instance-flag
493        // checks first so that untransformed text - the common case - does no
494        // extra work, and only then consult the cached per-font bitmap-strike
495        // info (a lock-free lookup populated when the font was added).
496        let has_bitmap_strikes = !transform.is_simple_2d_translation()
497            && self.font.flags.contains(FontInstanceFlags::EMBEDDED_BITMAPS)
498            && resource_cache.font_has_bitmap_strikes(self.font.font_key);
499
500        let (used_font, raster_scale) = Self::compute_font_instance(
501            &self.font,
502            surface,
503            transform,
504            allow_subpixel,
505            raster_space,
506            has_bitmap_strikes,
507        );
508
509        let subpx_dir = used_font.get_subpx_dir();
510        let dps = surface.device_pixel_scale;
511
512        // Two glyph-positioning modes:
513        //
514        // * Device mode (screen raster space, axis-aligned or 2D rotated/skewed
515        //   `TRANSFORM_GLYPHS`): the glyph is rasterized at the final device
516        //   scale and positioned by snapping its device position to the device
517        //   grid. The per-glyph offsets handed to the shader are device-space.
518        //
519        // * Local-raster mode (everything `compute_font_instance` marks with
520        //   `TEXTURE_PADDING` — local raster space / pinch-zoom, oversized
521        //   glyphs, perspective — and any non-screen raster space): the glyph is
522        //   rasterized at `raster_scale` with an identity transform and the
523        //   shader scales/positions it in local space, letting `write_vertex`
524        //   apply the (possibly animated/perspective) transform. Device snapping
525        //   is intentionally avoided here to prevent glyphs wiggling under
526        //   animation. The per-glyph offsets are absolute snapped *raster-space*
527        //   positions.
528        //
529        // Transposed / flipped (vertical writing-mode) glyphs need no special
530        // handling: the transpose/flip is baked into the glyph's rasterization
531        // transform (so the bitmap, `res.offset` and uv rect are already
532        // oriented) and the pen positions are laid out by the caller, so they
533        // ride the device path like any other run.
534        let local_raster = raster_space != RasterSpace::Screen
535            || used_font.flags.contains(FontInstanceFlags::TEXTURE_PADDING);
536
537        let snap_bias = match subpx_dir {
538            SubpixelDirection::None => DeviceVector2D::new(0.5, 0.5),
539            SubpixelDirection::Horizontal => DeviceVector2D::new(0.125, 0.5),
540            SubpixelDirection::Vertical => DeviceVector2D::new(0.5, 0.125),
541        };
542
543        // World-space run anchor and reference-frame origin (device mode only).
544        let anchor_world = transform.transform_point2d(local_rect.min);
545        let reference_world = transform.transform_point2d(LayoutPoint::zero());
546
547        let mut glyph_offsets: Vec<DeviceVector2D> = Vec::new();
548        let glyph_keys_range = if local_raster {
549            // Local-raster mode: snap each glyph in raster space (no device
550            // snap), store the absolute snapped raster position. The shader maps
551            // raster space -> local (by `res.scale / (raster_scale * dps)`) and
552            // `write_vertex` applies the transform.
553            let glyph_raster_scale = raster_scale * dps.0;
554            glyph_offsets.reserve(self.glyphs.len());
555
556            scratch.frame.glyph_keys.extend(self.glyphs.iter().map(|src| {
557                let pos = local_rect.min + src.point.to_vector();
558                let raster_pos = DevicePoint::new(pos.x * glyph_raster_scale, pos.y * glyph_raster_scale);
559                let snapped = (raster_pos + snap_bias).floor();
560                glyph_offsets.push(snapped.to_vector());
561                GlyphKey::new(src.index, raster_pos, subpx_dir)
562            }))
563        } else if let (Some(anchor_world), Some(reference_world)) = (anchor_world, reference_world) {
564            // Device mode.
565            let anchor_device = anchor_world * dps;
566
567            // Snap the *reference frame* origin to the device grid and shift all
568            // glyphs by that delta. We snap the frame origin (the transform
569            // translation) rather than the prim rect origin so that the prim's
570            // own sub-pixel layout offset stays as content within the frame,
571            // while a fractional transform on the frame — a fractionally placed
572            // offscreen surface, or fractional scrolling — snaps away
573            // consistently (e.g. translate(7.49) and translate(7.0) produce the
574            // same aligned frame). The snap uses the full device position
575            // (translation, and thus live scroll, included), which is what fixes
576            // the external-scroll-offset / fractional-scroll artifacts the old
577            // path had. Mirrors the old `snapped_reference_frame_relative_offset`.
578            let reference_device = reference_world * dps;
579            let snap_shift = reference_device.round() - reference_device;
580            glyph_offsets.reserve(self.glyphs.len());
581
582            scratch.frame.glyph_keys.extend(self.glyphs.iter().map(|src| {
583                // Glyph pen position in absolute device space, with the
584                // reference-frame snap applied.
585                let glyph_world = transform
586                    .transform_point2d(local_rect.min + src.point.to_vector())
587                    .unwrap_or(anchor_world);
588                let device_pen = glyph_world * dps + snap_shift;
589
590                // Snap the per-glyph device position to the grid and store it
591                // relative to the unsnapped anchor; the shader re-adds the
592                // unsnapped anchor, recovering this snapped position.
593                let snapped = (device_pen + snap_bias).floor();
594                glyph_offsets.push(snapped - anchor_device);
595
596                // Subpixel offset comes from the fractional part of `device_pen`
597                // (reference-frame aligned), so it reflects the glyph's position
598                // within the snapped frame.
599                GlyphKey::new(src.index, device_pen, subpx_dir)
600            }))
601        } else {
602            // Degenerate transform (no 2D inverse for the anchor): draw nothing.
603            scratch.frame.glyph_keys.extend(std::iter::empty())
604        };
605
606        resource_cache.request_glyphs(
607            used_font.clone(),
608            &scratch.frame.glyph_keys[glyph_keys_range],
609            gpu_buffer,
610        );
611
612        let gpu_address = self.write_prim_gpu_blocks(&glyph_offsets, gpu_buffer);
613
614        scratch.frame.text_runs.push(TextRunScratch {
615            used_font,
616            glyph_keys_range,
617            local_rect,
618            gpu_address,
619            raster_scale,
620            local_raster,
621        })
622    }
623}
624
625/// These are linux only because FontInstancePlatformOptions varies in size by platform.
626#[test]
627#[cfg(target_os = "linux")]
628fn test_struct_sizes() {
629    use std::mem;
630    // The sizes of these structures are critical for performance on a number of
631    // talos stress tests. If you get a failure here on CI, there's two possibilities:
632    // (a) You made a structure smaller than it currently is. Great work! Update the
633    //     test expectations and move on.
634    // (b) You made a structure larger. This is not necessarily a problem, but should only
635    //     be done with care, and after checking if talos performance regresses badly.
636    assert_eq!(mem::size_of::<TextRun>(), 80, "TextRun size changed");
637    assert_eq!(mem::size_of::<TextRunTemplate>(), 88, "TextRunTemplate size changed");
638    assert_eq!(mem::size_of::<TextRunKey>(), 80, "TextRunKey size changed");
639}