/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use api::{ColorF, FontInstanceFlags, GlyphInstance, RasterSpace, Shadow, GlyphIndex}; use api::units::{LayoutToWorldTransform, DevicePixelScale}; use api::units::*; usecrate::scene_building::{CreateShadow, IsVisible}; use glyph_rasterizer::{FontInstance, FontTransform, GlyphKey, SubpixelDirection, FONT_SIZE_LIMIT}; usecrate::intern; usecrate::internal_types::LayoutPrimitiveInfo; usecrate::picture::SurfaceInfo; usecrate::prim_store::PrimitiveScratchBuffer; usecrate::prim_store::{PrimitiveStore, PrimKeyCommonData, PrimTemplateCommonData}; usecrate::renderer::{GpuBufferAddress, GpuBufferBuilderF, MAX_VERTEX_TEXTURE_WIDTH}; usecrate::resource_cache::ResourceCache; usecrate::util::MatrixHelpers; usecrate::prim_store::{InternablePrimitive, PrimitiveKind, LayoutPointAu}; usecrate::spatial_tree::{SpatialTree, SpatialNodeIndex}; use std::ops;
/// A run of glyphs, with associated font information. #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] #[derive(Debug, Clone, Eq, MallocSizeOf, PartialEq, Hash)] pubstruct TextRunKey { pub common: PrimKeyCommonData, pub font: FontInstance, /// Glyph pen positions, each relative to the *normalized* prim rect /// origin (`prim_info.rect.min`). Storing relative to the normalized /// origin keeps the intern key stable across pre-scroll offset changes, /// since the external scroll offset cancels: both the glyph position and /// the prim origin are normalized the same way (see `add_text`). pub glyphs: Vec<GlyphInstanceAu>, pub shadow: bool, pub requested_raster_space: RasterSpace,
}
#[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] #[derive(MallocSizeOf)] pubstruct TextRunTemplate { pub common: PrimTemplateCommonData, pub font: FontInstance, /// Glyph pen positions, each relative to the normalized prim rect origin. /// See [`TextRunKey::glyphs`]. At frame time the normalized local glyph /// position is `prim_rect.min + glyph.point`; `request_resources` then /// transforms and device-snaps each glyph to produce the device-space /// offsets handed to the shader. pub glyphs: Vec<GlyphInstance>, pub shadow: bool, pub requested_raster_space: RasterSpace,
}
impl ops::Deref for TextRunTemplate { type Target = PrimTemplateCommonData; fn deref(&self) -> &Self::Target {
&self.common
}
}
impl TextRunTemplate { /// Write the per-instance GPU blocks for this run: the premultiplied /// font color followed by the per-glyph offsets (two glyphs packed per /// block). The offsets are device-space in device mode and raster-space in /// local-raster mode (see `request_resources`). Corresponds to /// `fetch_glyph` / `fetch_text_run` in the shader. fn write_prim_gpu_blocks(
&self,
glyph_offsets: &[DeviceVector2D],
gpu_buffer: &mut GpuBufferBuilderF,
) -> GpuBufferAddress { let num_blocks = (glyph_offsets.len() + 1) / 2 + 1;
assert!(num_blocks <= MAX_VERTEX_TEXTURE_WIDTH); letmut writer = gpu_buffer.write_blocks(num_blocks);
writer.push_one(ColorF::from(self.font.color).premultiplied());
letmut gpu_block = [0.0; 4]; for (i, src) in glyph_offsets.iter().enumerate() { // Two glyphs are packed per GPU block. if (i & 1) == 0 {
gpu_block[0] = src.x;
gpu_block[1] = src.y;
} else {
gpu_block[2] = src.x;
gpu_block[3] = src.y;
writer.push_one(gpu_block);
}
}
// Ensure the last block is added in the case // of an odd number of glyphs. if (glyph_offsets.len() & 1) != 0 {
writer.push_one(gpu_block);
}
/// Per-frame scratch data for a TextRun primitive. Holds the snapshot /// of font + glyph state captured each frame in `request_resources` and /// read by batching. Pushed once per visible TextRun per frame. #[derive(Debug)] #[cfg_attr(feature = "capture", derive(Serialize))] pubstruct TextRunScratch { /// Per-frame font instance derived from the specified font + this /// frame's transform + raster space. Carries subpixel direction, /// flags, and the device-space size. pub used_font: FontInstance, /// Range of glyph keys allocated for this run this frame, indexing /// into PrimitiveFrameScratch.glyph_keys. pub glyph_keys_range: storage::Range<GlyphKey>, /// Normalized prim local rect for this run. `.min` is the run anchor: /// the shader transforms it to device space and adds the per-glyph /// device offsets. Stored here so batching emits the identical anchor /// in `PrimitiveHeader.local_rect` that `request_resources` used to /// compute those offsets. pub local_rect: LayoutRect, /// Per-instance GPU buffer address for the color block followed by the /// per-glyph offset blocks (two glyphs per block). In device mode these are /// glyph pen positions snapped to the device grid, relative to the /// transformed anchor; in local-raster mode they are absolute snapped /// raster-space positions. Per-instance because they depend on this frame's /// transform. pub gpu_address: GpuBufferAddress, /// Raster scale used when rasterizing the glyphs (1.0 in device mode; the /// local/zoom scale or oversize-clamp scale in local-raster mode). Passed /// to the shader so it can map raster space back to local. pub raster_scale: f32, /// Whether this run uses local-raster mode (see `request_resources`). pub local_raster: bool,
}
impl TextRunTemplate { /// Build a per-frame `(used_font, raster_scale)` pair for this text run. /// The result is fresh per frame; nothing persists on the template. fn compute_font_instance(
specified_font: &FontInstance,
surface: &SurfaceInfo,
transform: &LayoutToWorldTransform,
allow_subpixel: bool,
raster_space: RasterSpace,
) -> (FontInstance, f32) { // If local raster space is specified, include that in the scale // of the glyphs that get rasterized. // TODO(gw): Once we support proper local space raster modes, this // will implicitly be part of the device pixel ratio for // the (cached) local space surface, and so this code // will no longer be required. let raster_scale_input = raster_space.local_scale().unwrap_or(1.0).max(0.001);
let dps = surface.device_pixel_scale.0; let font_size = specified_font.size.to_f32_px();
// Small floating point error can accumulate in the raster * device_pixel scale. // Round that to the nearest 100th of a scale factor to remove this error while // still allowing reasonably accurate scale factors when a pinch-zoom is stopped // at a fractional amount. let quantized_scale = (dps * raster_scale_input * 100.0).round() / 100.0; letmut device_font_size = font_size * quantized_scale;
// Check there is a valid transform that doesn't exceed the font size limit. // Ensure the font is supposed to be rasterized in screen-space. // Only support transforms that can be coerced to simple 2D transforms. // Add texture padding to the rasterized glyph buffer when one anticipates // the glyph will need to be scaled when rendered. let (use_subpixel_aa, transform_glyphs, texture_padding, oversized) = if raster_space != RasterSpace::Screen ||
transform.has_perspective_component() || !transform.has_2d_inverse()
{
(false, false, true, device_font_size > FONT_SIZE_LIMIT)
} elseif transform.exceeds_2d_scale((FONT_SIZE_LIMIT / device_font_size) as f64) {
(false, false, true, true)
} else {
(true, !transform.is_simple_2d_translation(), false, false)
};
letmut raster_scale = raster_scale_input; let font_transform = if transform_glyphs { // Get the font transform matrix (skew / scale) from the complete transform. // Fold in the device pixel scale.
raster_scale = 1.0;
FontTransform::from(transform)
} else { if oversized { // Font sizes larger than the limit need to be scaled, thus can't use subpixels. // In this case we adjust the font size and raster space to ensure // we rasterize at the limit, to minimize the amount of scaling.
raster_scale = FONT_SIZE_LIMIT / (font_size * dps);
device_font_size = FONT_SIZE_LIMIT;
} // else: keep raster_scale = raster_scale_input. We may have // changed from RasterSpace::Screen due to a transform with // perspective or without a 2D inverse, or it may have been // RasterSpace::Local all along.
// Rasterize the glyph without any transform.
FontTransform::identity()
};
letmut flags = specified_font.flags; if transform_glyphs {
flags |= FontInstanceFlags::TRANSFORM_GLYPHS;
} if texture_padding {
flags |= FontInstanceFlags::TEXTURE_PADDING;
}
// Construct used font instance from the specified font instance letmut used_font = FontInstance {
transform: font_transform,
size: device_font_size.into(),
flags,
..specified_font.clone()
};
// If using local space glyphs, we don't want subpixel AA. if !allow_subpixel || !use_subpixel_aa {
used_font.disable_subpixel_aa();
// Disable subpixel positioning for oversized glyphs to avoid // thrashing the glyph cache with many subpixel variations of // big glyph textures. A possible subpixel positioning error // is small relative to the maximum font size and thus should // not be very noticeable. if oversized {
used_font.disable_subpixel_position();
}
}
(used_font, raster_scale)
}
/// Gets the raster space to use when rendering this primitive. /// Usually this would be the requested raster space. However, if /// the primitive's spatial node or one of its ancestors is being pinch zoomed /// then we round it. This prevents us rasterizing glyphs for every minor /// change in zoom level, as that would be too expensive. fn get_raster_space_for_prim(
&self,
prim_spatial_node_index: SpatialNodeIndex,
low_quality_pinch_zoom: bool,
device_pixel_scale: DevicePixelScale,
spatial_tree: &SpatialTree,
) -> RasterSpace { let prim_spatial_node = spatial_tree.get_spatial_node(prim_spatial_node_index); if prim_spatial_node.is_ancestor_or_self_zooming { if low_quality_pinch_zoom { // In low-quality mode, we set the scale to be 1.0. However, the device-pixel // scale selected for the zoom will be taken into account in the caller to this // function when it's converted from local -> device pixels. Since in this mode // the device-pixel scale is constant during the zoom, this gives the desired // performance while also allowing the scale to be adjusted to a new factor at // the end of a pinch-zoom.
RasterSpace::Local(1.0)
} else { let root_spatial_node_index = spatial_tree.root_reference_frame_index();
// For high-quality mode, we quantize the exact scale factor as before. However, // we want to _undo_ the effect of the device-pixel scale on the picture cache // tiles (which changes now that they are raster roots). Divide the rounded value // by the device-pixel scale so that the local -> device conversion has no effect. let scale_factors = spatial_tree
.get_relative_transform(prim_spatial_node_index, root_spatial_node_index)
.scale_factors();
// Round the scale up to the nearest power of 2, but don't exceed 8. let scale = scale_factors.0.max(scale_factors.1).min(8.0).max(1.0); let rounded_up = 2.0f32.powf(scale.log2().ceil());
RasterSpace::Local(rounded_up / device_pixel_scale.0)
}
} else { // Assume that if we have a RasterSpace::Local, it is frequently changing, in which // case we want to undo the device-pixel scale, as we do above. matchself.requested_raster_space {
RasterSpace::Local(scale) => RasterSpace::Local(scale / device_pixel_scale.0),
RasterSpace::Screen => RasterSpace::Screen,
}
}
}
let subpx_dir = used_font.get_subpx_dir(); let dps = surface.device_pixel_scale;
// Two glyph-positioning modes: // // * Device mode (screen raster space, axis-aligned or 2D rotated/skewed // `TRANSFORM_GLYPHS`): the glyph is rasterized at the final device // scale and positioned by snapping its device position to the device // grid. The per-glyph offsets handed to the shader are device-space. // // * Local-raster mode (everything `compute_font_instance` marks with // `TEXTURE_PADDING` — local raster space / pinch-zoom, oversized // glyphs, perspective — and any non-screen raster space): the glyph is // rasterized at `raster_scale` with an identity transform and the // shader scales/positions it in local space, letting `write_vertex` // apply the (possibly animated/perspective) transform. Device snapping // is intentionally avoided here to prevent glyphs wiggling under // animation. The per-glyph offsets are absolute snapped *raster-space* // positions. // // Transposed / flipped (vertical writing-mode) glyphs need no special // handling: the transpose/flip is baked into the glyph's rasterization // transform (so the bitmap, `res.offset` and uv rect are already // oriented) and the pen positions are laid out by the caller, so they // ride the device path like any other run. let local_raster = raster_space != RasterSpace::Screen
|| used_font.flags.contains(FontInstanceFlags::TEXTURE_PADDING);
let snap_bias = match subpx_dir {
SubpixelDirection::None => DeviceVector2D::new(0.5, 0.5),
SubpixelDirection::Horizontal => DeviceVector2D::new(0.125, 0.5),
SubpixelDirection::Vertical => DeviceVector2D::new(0.5, 0.125),
};
// World-space run anchor (device mode only). let anchor_world = transform.transform_point2d(local_rect.min);
letmut glyph_offsets: Vec<DeviceVector2D> = Vec::new(); let glyph_keys_range = if local_raster { // Local-raster mode: snap each glyph in raster space (no device // snap), store the absolute snapped raster position. The shader maps // raster space -> local (by `res.scale / (raster_scale * dps)`) and // `write_vertex` applies the transform. let glyph_raster_scale = raster_scale * dps.0;
glyph_offsets.reserve(self.glyphs.len());
// Snap the *reference frame* origin (the prim spatial node's local // origin) to the device grid against the ROOT, and shift all glyphs // by that delta. We snap the frame origin rather than the prim rect // origin so the prim's own sub-pixel layout offset stays as content // within the frame, while a fractional transform on the frame — a // fractionally placed offscreen surface, or fractional scrolling — // snaps away consistently (e.g. translate(7.49) and translate(7.0) // produce the same aligned frame). // // We snap against root rather than the surface's own raster space. // Device-mode text always sits in a root-coordinate-system surface // (rotated / scaled raster roots make their text local-raster, // handled above), so root is the correct device grid: this aligns // glyphs even when the surface is a non-root tile cache (sticky / // scrolled / fixed) that composites at a fractional device offset, // where snapping against the cache's own node would be a no-op. The // full relative transform handles a rotation between the prim's node // and root (e.g. doubly-rotated upright text). let root_index = spatial_tree.root_reference_frame_index(); let snap_shift = match spatial_tree
.get_relative_transform(spatial_node_index, root_index)
.into_transform()
.transform_point2d(LayoutPoint::zero())
{
Some(p) => { let reference_device = DevicePoint::new(p.x * dps.0, p.y * dps.0);
reference_device.round() - reference_device
}
None => DeviceVector2D::zero(),
};
glyph_offsets.reserve(self.glyphs.len());
scratch.frame.glyph_keys.extend(self.glyphs.iter().map(|src| { // Glyph pen position in absolute device space, with the // reference-frame snap applied. let glyph_world = transform
.transform_point2d(local_rect.min + src.point.to_vector())
.unwrap_or(anchor_world); let device_pen = glyph_world * dps + snap_shift;
// Snap the per-glyph device position to the grid and store it // relative to the unsnapped anchor; the shader re-adds the // unsnapped anchor, recovering this snapped position. let snapped = (device_pen + snap_bias).floor();
glyph_offsets.push(snapped - anchor_device);
// Subpixel offset comes from the fractional part of `device_pen` // (reference-frame aligned), so it reflects the glyph's position // within the snapped frame.
GlyphKey::new(src.index, device_pen, subpx_dir)
}))
} else { // Degenerate transform (no 2D inverse for the anchor): draw nothing.
scratch.frame.glyph_keys.extend(std::iter::empty())
};
/// These are linux only because FontInstancePlatformOptions varies in size by platform. #[test] #[cfg(target_os = "linux")] fn test_struct_sizes() { use std::mem; // The sizes of these structures are critical for performance on a number of // talos stress tests. If you get a failure here on CI, there's two possibilities: // (a) You made a structure smaller than it currently is. Great work! Update the // test expectations and move on. // (b) You made a structure larger. This is not necessarily a problem, but should only // be done with care, and after checking if talos performance regresses badly.
assert_eq!(mem::size_of::<TextRun>(), 80, "TextRun size changed");
assert_eq!(mem::size_of::<TextRunTemplate>(), 88, "TextRunTemplate size changed");
assert_eq!(mem::size_of::<TextRunKey>(), 80, "TextRunKey size changed");
}
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.15 Sekunden
(vorverarbeitet am 2026-08-26)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.