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 ) -> (FontInstance, f32) {
292 // If local raster space is specified, include that in the scale
293 // of the glyphs that get rasterized.
294 // TODO(gw): Once we support proper local space raster modes, this
295 // will implicitly be part of the device pixel ratio for
296 // the (cached) local space surface, and so this code
297 // will no longer be required.
298 let raster_scale_input = raster_space.local_scale().unwrap_or(1.0).max(0.001);
299
300 let dps = surface.device_pixel_scale.0;
301 let font_size = specified_font.size.to_f32_px();
302
303 // Small floating point error can accumulate in the raster * device_pixel scale.
304 // Round that to the nearest 100th of a scale factor to remove this error while
305 // still allowing reasonably accurate scale factors when a pinch-zoom is stopped
306 // at a fractional amount.
307 let quantized_scale = (dps * raster_scale_input * 100.0).round() / 100.0;
308 let mut device_font_size = font_size * quantized_scale;
309
310 // Check there is a valid transform that doesn't exceed the font size limit.
311 // Ensure the font is supposed to be rasterized in screen-space.
312 // Only support transforms that can be coerced to simple 2D transforms.
313 // Add texture padding to the rasterized glyph buffer when one anticipates
314 // the glyph will need to be scaled when rendered.
315 let (use_subpixel_aa, transform_glyphs, texture_padding, oversized) = if raster_space != RasterSpace::Screen ||
316 transform.has_perspective_component() || !transform.has_2d_inverse()
317 {
318 (false, false, true, device_font_size > FONT_SIZE_LIMIT)
319 } else if transform.exceeds_2d_scale((FONT_SIZE_LIMIT / device_font_size) as f64) {
320 (false, false, true, true)
321 } else {
322 (true, !transform.is_simple_2d_translation(), false, false)
323 };
324
325 let mut raster_scale = raster_scale_input;
326 let font_transform = if transform_glyphs {
327 // Get the font transform matrix (skew / scale) from the complete transform.
328 // Fold in the device pixel scale.
329 raster_scale = 1.0;
330 FontTransform::from(transform)
331 } else {
332 if oversized {
333 // Font sizes larger than the limit need to be scaled, thus can't use subpixels.
334 // In this case we adjust the font size and raster space to ensure
335 // we rasterize at the limit, to minimize the amount of scaling.
336 raster_scale = FONT_SIZE_LIMIT / (font_size * dps);
337 device_font_size = FONT_SIZE_LIMIT;
338 }
339 // else: keep raster_scale = raster_scale_input. We may have
340 // changed from RasterSpace::Screen due to a transform with
341 // perspective or without a 2D inverse, or it may have been
342 // RasterSpace::Local all along.
343
344 // Rasterize the glyph without any transform.
345 FontTransform::identity()
346 };
347
348 let mut flags = specified_font.flags;
349 if transform_glyphs {
350 flags |= FontInstanceFlags::TRANSFORM_GLYPHS;
351 }
352 if texture_padding {
353 flags |= FontInstanceFlags::TEXTURE_PADDING;
354 }
355
356 // Construct used font instance from the specified font instance
357 let mut used_font = FontInstance {
358 transform: font_transform,
359 size: device_font_size.into(),
360 flags,
361 ..specified_font.clone()
362 };
363
364 // If using local space glyphs, we don't want subpixel AA.
365 if !allow_subpixel || !use_subpixel_aa {
366 used_font.disable_subpixel_aa();
367
368 // Disable subpixel positioning for oversized glyphs to avoid
369 // thrashing the glyph cache with many subpixel variations of
370 // big glyph textures. A possible subpixel positioning error
371 // is small relative to the maximum font size and thus should
372 // not be very noticeable.
373 if oversized {
374 used_font.disable_subpixel_position();
375 }
376 }
377
378 (used_font, raster_scale)
379 }
380
381 /// Gets the raster space to use when rendering this primitive.
382 /// Usually this would be the requested raster space. However, if
383 /// the primitive's spatial node or one of its ancestors is being pinch zoomed
384 /// then we round it. This prevents us rasterizing glyphs for every minor
385 /// change in zoom level, as that would be too expensive.
386 fn get_raster_space_for_prim(
387 &self,
388 prim_spatial_node_index: SpatialNodeIndex,
389 low_quality_pinch_zoom: bool,
390 device_pixel_scale: DevicePixelScale,
391 spatial_tree: &SpatialTree,
392 ) -> RasterSpace {
393 let prim_spatial_node = spatial_tree.get_spatial_node(prim_spatial_node_index);
394 if prim_spatial_node.is_ancestor_or_self_zooming {
395 if low_quality_pinch_zoom {
396 // In low-quality mode, we set the scale to be 1.0. However, the device-pixel
397 // scale selected for the zoom will be taken into account in the caller to this
398 // function when it's converted from local -> device pixels. Since in this mode
399 // the device-pixel scale is constant during the zoom, this gives the desired
400 // performance while also allowing the scale to be adjusted to a new factor at
401 // the end of a pinch-zoom.
402 RasterSpace::Local(1.0)
403 } else {
404 let root_spatial_node_index = spatial_tree.root_reference_frame_index();
405
406 // For high-quality mode, we quantize the exact scale factor as before. However,
407 // we want to _undo_ the effect of the device-pixel scale on the picture cache
408 // tiles (which changes now that they are raster roots). Divide the rounded value
409 // by the device-pixel scale so that the local -> device conversion has no effect.
410 let scale_factors = spatial_tree
411 .get_relative_transform(prim_spatial_node_index, root_spatial_node_index)
412 .scale_factors();
413
414 // Round the scale up to the nearest power of 2, but don't exceed 8.
415 let scale = scale_factors.0.max(scale_factors.1).min(8.0).max(1.0);
416 let rounded_up = 2.0f32.powf(scale.log2().ceil());
417
418 RasterSpace::Local(rounded_up / device_pixel_scale.0)
419 }
420 } else {
421 // Assume that if we have a RasterSpace::Local, it is frequently changing, in which
422 // case we want to undo the device-pixel scale, as we do above.
423 match self.requested_raster_space {
424 RasterSpace::Local(scale) => RasterSpace::Local(scale / device_pixel_scale.0),
425 RasterSpace::Screen => RasterSpace::Screen,
426 }
427 }
428 }
429
430 pub fn request_resources(
431 &self,
432 local_rect: LayoutRect,
433 transform: &LayoutToWorldTransform,
434 surface: &SurfaceInfo,
435 spatial_node_index: SpatialNodeIndex,
436 allow_subpixel: bool,
437 low_quality_pinch_zoom: bool,
438 resource_cache: &mut ResourceCache,
439 gpu_buffer: &mut GpuBufferBuilderF,
440 spatial_tree: &SpatialTree,
441 scratch: &mut PrimitiveScratchBuffer,
442 ) -> storage::Index<TextRunScratch> {
443 let raster_space = self.get_raster_space_for_prim(
444 spatial_node_index,
445 low_quality_pinch_zoom,
446 surface.device_pixel_scale,
447 spatial_tree,
448 );
449
450 let (used_font, raster_scale) = Self::compute_font_instance(
451 &self.font,
452 surface,
453 transform,
454 allow_subpixel,
455 raster_space,
456 );
457
458 let subpx_dir = used_font.get_subpx_dir();
459 let dps = surface.device_pixel_scale;
460
461 // Two glyph-positioning modes:
462 //
463 // * Device mode (screen raster space, axis-aligned or 2D rotated/skewed
464 // `TRANSFORM_GLYPHS`): the glyph is rasterized at the final device
465 // scale and positioned by snapping its device position to the device
466 // grid. The per-glyph offsets handed to the shader are device-space.
467 //
468 // * Local-raster mode (everything `compute_font_instance` marks with
469 // `TEXTURE_PADDING` — local raster space / pinch-zoom, oversized
470 // glyphs, perspective — and any non-screen raster space): the glyph is
471 // rasterized at `raster_scale` with an identity transform and the
472 // shader scales/positions it in local space, letting `write_vertex`
473 // apply the (possibly animated/perspective) transform. Device snapping
474 // is intentionally avoided here to prevent glyphs wiggling under
475 // animation. The per-glyph offsets are absolute snapped *raster-space*
476 // positions.
477 //
478 // Transposed / flipped (vertical writing-mode) glyphs need no special
479 // handling: the transpose/flip is baked into the glyph's rasterization
480 // transform (so the bitmap, `res.offset` and uv rect are already
481 // oriented) and the pen positions are laid out by the caller, so they
482 // ride the device path like any other run.
483 let local_raster = raster_space != RasterSpace::Screen
484 || used_font.flags.contains(FontInstanceFlags::TEXTURE_PADDING);
485
486 let snap_bias = match subpx_dir {
487 SubpixelDirection::None => DeviceVector2D::new(0.5, 0.5),
488 SubpixelDirection::Horizontal => DeviceVector2D::new(0.125, 0.5),
489 SubpixelDirection::Vertical => DeviceVector2D::new(0.5, 0.125),
490 };
491
492 // World-space run anchor and reference-frame origin (device mode only).
493 let anchor_world = transform.transform_point2d(local_rect.min);
494 let reference_world = transform.transform_point2d(LayoutPoint::zero());
495
496 let mut glyph_offsets: Vec<DeviceVector2D> = Vec::new();
497 let glyph_keys_range = if local_raster {
498 // Local-raster mode: snap each glyph in raster space (no device
499 // snap), store the absolute snapped raster position. The shader maps
500 // raster space -> local (by `res.scale / (raster_scale * dps)`) and
501 // `write_vertex` applies the transform.
502 let glyph_raster_scale = raster_scale * dps.0;
503 glyph_offsets.reserve(self.glyphs.len());
504
505 scratch.frame.glyph_keys.extend(self.glyphs.iter().map(|src| {
506 let pos = local_rect.min + src.point.to_vector();
507 let raster_pos = DevicePoint::new(pos.x * glyph_raster_scale, pos.y * glyph_raster_scale);
508 let snapped = (raster_pos + snap_bias).floor();
509 glyph_offsets.push(snapped.to_vector());
510 GlyphKey::new(src.index, raster_pos, subpx_dir)
511 }))
512 } else if let (Some(anchor_world), Some(reference_world)) = (anchor_world, reference_world) {
513 // Device mode.
514 let anchor_device = anchor_world * dps;
515
516 // Snap the *reference frame* origin to the device grid and shift all
517 // glyphs by that delta. We snap the frame origin (the transform
518 // translation) rather than the prim rect origin so that the prim's
519 // own sub-pixel layout offset stays as content within the frame,
520 // while a fractional transform on the frame — a fractionally placed
521 // offscreen surface, or fractional scrolling — snaps away
522 // consistently (e.g. translate(7.49) and translate(7.0) produce the
523 // same aligned frame). The snap uses the full device position
524 // (translation, and thus live scroll, included), which is what fixes
525 // the external-scroll-offset / fractional-scroll artifacts the old
526 // path had. Mirrors the old `snapped_reference_frame_relative_offset`.
527 let reference_device = reference_world * dps;
528 let snap_shift = reference_device.round() - reference_device;
529 glyph_offsets.reserve(self.glyphs.len());
530
531 scratch.frame.glyph_keys.extend(self.glyphs.iter().map(|src| {
532 // Glyph pen position in absolute device space, with the
533 // reference-frame snap applied.
534 let glyph_world = transform
535 .transform_point2d(local_rect.min + src.point.to_vector())
536 .unwrap_or(anchor_world);
537 let device_pen = glyph_world * dps + snap_shift;
538
539 // Snap the per-glyph device position to the grid and store it
540 // relative to the unsnapped anchor; the shader re-adds the
541 // unsnapped anchor, recovering this snapped position.
542 let snapped = (device_pen + snap_bias).floor();
543 glyph_offsets.push(snapped - anchor_device);
544
545 // Subpixel offset comes from the fractional part of `device_pen`
546 // (reference-frame aligned), so it reflects the glyph's position
547 // within the snapped frame.
548 GlyphKey::new(src.index, device_pen, subpx_dir)
549 }))
550 } else {
551 // Degenerate transform (no 2D inverse for the anchor): draw nothing.
552 scratch.frame.glyph_keys.extend(std::iter::empty())
553 };
554
555 resource_cache.request_glyphs(
556 used_font.clone(),
557 &scratch.frame.glyph_keys[glyph_keys_range],
558 gpu_buffer,
559 );
560
561 let gpu_address = self.write_prim_gpu_blocks(&glyph_offsets, gpu_buffer);
562
563 scratch.frame.text_runs.push(TextRunScratch {
564 used_font,
565 glyph_keys_range,
566 local_rect,
567 gpu_address,
568 raster_scale,
569 local_raster,
570 })
571 }
572}
573
574/// These are linux only because FontInstancePlatformOptions varies in size by platform.
575#[test]
576#[cfg(target_os = "linux")]
577fn test_struct_sizes() {
578 use std::mem;
579 // The sizes of these structures are critical for performance on a number of
580 // talos stress tests. If you get a failure here on CI, there's two possibilities:
581 // (a) You made a structure smaller than it currently is. Great work! Update the
582 // test expectations and move on.
583 // (b) You made a structure larger. This is not necessarily a problem, but should only
584 // be done with care, and after checking if talos performance regresses badly.
585 assert_eq!(mem::size_of::<TextRun>(), 80, "TextRun size changed");
586 assert_eq!(mem::size_of::<TextRunTemplate>(), 88, "TextRunTemplate size changed");
587 assert_eq!(mem::size_of::<TextRunKey>(), 80, "TextRunKey size changed");
588}