tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit ea7162586cdb9856bf687054064de252ad955730
parent d4b368388187f412d7207d8212f045e2149e3977
Author: Glenn Watson <git@chillybin.org>
Date:   Sun, 14 Dec 2025 21:01:12 +0000

Bug 2005618 - Pack subpixel positioned glyphs in to one atlas alloc r=gfx-reviewers,lsalzman

Instead of rasterizing and caching glyphs keys based on subpixel
offset, rasterize all variants at once and pack them in to a single
texture cache allocation.

In future, this will simplify some of the snapping work and reduce
CPU load, by allowing the selection of a subpixel offset glyph
image to be deferred to vertex shading.

It also reduces CPU time handling cache lookups and evictions.

Testing on a number of sites showed that the typical distribution
of subpixel offsets in the glyph cache was ~25% for each variant,
showing that this should not introduce a large amount of extra
glyph rasterizations for most content.

Differential Revision: https://phabricator.services.mozilla.com/D276073

Diffstat:
Mgfx/wr/webrender/res/ps_text_run.glsl | 17+++++++++++++++--
Mgfx/wr/webrender/src/batch.rs | 3+++
Mgfx/wr/webrender/src/glyph_cache.rs | 7++++---
Mgfx/wr/webrender/src/gpu_types.rs | 17++++++++++++++---
Mgfx/wr/webrender/src/resource_cache.rs | 21++++++++++++++++-----
Mgfx/wr/wr_glyph_rasterizer/src/platform/macos/font.rs | 1+
Mgfx/wr/wr_glyph_rasterizer/src/platform/unix/font.rs | 1+
Mgfx/wr/wr_glyph_rasterizer/src/platform/windows/font.rs | 1+
Mgfx/wr/wr_glyph_rasterizer/src/rasterizer.rs | 158+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mgfx/wr/wrench/reftests/text/reftest.list | 6+++---
10 files changed, 209 insertions(+), 23 deletions(-)

diff --git a/gfx/wr/webrender/res/ps_text_run.glsl b/gfx/wr/webrender/res/ps_text_run.glsl @@ -101,8 +101,11 @@ void main() { PictureTask task = fetch_picture_task(ph.picture_task_address); int glyph_index = instance.segment_index; - int subpx_dir = (instance.flags >> 8) & 0xff; - int color_mode = instance.flags & 0xff; + int color_mode = instance.flags & 0xF; + int subpx_offset_x = (instance.flags >> 4) & 0x3; + int subpx_offset_y = (instance.flags >> 6) & 0x3; + int subpx_dir = (instance.flags >> 8) & 0x3; + int is_packed_glyph = (instance.flags >> 10) & 0x1; // Note that the reference frame relative offset is stored in the prim local // rect size during batching, instead of the actual size of the primitive. @@ -118,6 +121,16 @@ void main() { GlyphResource res = fetch_glyph_resource(instance.resource_address); + // For multi-variant glyphs, adjust the UV rect to select the correct quarter + // of the packed texture based on subpixel offset. + // This must happen before geometry calculations since the glyph rect size depends on the UV rect. + if (is_packed_glyph != 0) { + int variant_index = (subpx_dir == SUBPX_DIR_HORIZONTAL) ? subpx_offset_x : subpx_offset_y; + float quarter_width = (res.uv_rect.z - res.uv_rect.x) * 0.25; + res.uv_rect.x = res.uv_rect.x + float(variant_index) * quarter_width; + res.uv_rect.z = res.uv_rect.x + quarter_width; + } + vec2 snap_bias = get_snap_bias(subpx_dir); // Glyph space refers to the pixel space used by glyph rasterization during frame diff --git a/gfx/wr/webrender/src/batch.rs b/gfx/wr/webrender/src/batch.rs @@ -2015,6 +2015,9 @@ impl BatchBuilder { glyph.index_in_text_run, glyph.uv_rect_address, color_mode, + glyph.subpx_offset_x, + glyph.subpx_offset_y, + glyph.is_packed_glyph, )); } }, diff --git a/gfx/wr/webrender/src/glyph_cache.rs b/gfx/wr/webrender/src/glyph_cache.rs @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use api::{FontKey, FontInstanceKey, IdNamespace}; -use glyph_rasterizer::{FontInstance, GlyphFormat, GlyphKey, GlyphRasterizer}; +use glyph_rasterizer::{FontInstance, GlyphFormat, GlyphCacheKey, GlyphRasterizer}; use crate::internal_types::{FrameId, FrameStamp, FastHashMap}; use crate::resource_cache::ResourceClassCache; use std::sync::Arc; @@ -16,6 +16,7 @@ use crate::texture_cache::TextureCacheHandle; pub struct CachedGlyphInfo { pub format: GlyphFormat, pub texture_cache_handle: TextureCacheHandle, + pub is_packed_glyph: bool, } #[cfg_attr(feature = "capture", derive(Serialize))] @@ -62,7 +63,7 @@ pub struct GlyphKeyCacheInfo { last_frame_used: FrameId, } -pub type GlyphKeyCache = ResourceClassCache<GlyphKey, GlyphCacheEntry, GlyphKeyCacheInfo>; +pub type GlyphKeyCache = ResourceClassCache<GlyphCacheKey, GlyphCacheEntry, GlyphKeyCacheInfo>; impl GlyphKeyCache { pub fn eviction_notice(&self) -> &EvictionNotice { @@ -73,7 +74,7 @@ impl GlyphKeyCache { self.clear(); } - pub fn add_glyph(&mut self, key: GlyphKey, value: GlyphCacheEntry) { + pub fn add_glyph(&mut self, key: GlyphCacheKey, value: GlyphCacheEntry) { self.insert(key, value); } diff --git a/gfx/wr/webrender/src/gpu_types.rs b/gfx/wr/webrender/src/gpu_types.rs @@ -561,14 +561,25 @@ impl GlyphInstance { glyph_index_in_text_run: i32, glyph_uv_rect: GpuBufferAddress, color_mode: ShaderColorMode, + subpx_offset_x: u8, + subpx_offset_y: u8, + is_packed_glyph: bool, ) -> PrimitiveInstanceData { + // Pack subpixel offsets and multi-variant flag into upper 16 bits of data[2] + // After instance.flags extraction (>> 16), shader sees: + // bits 0-3: color_mode, bits 4-5: subpx_offset_x, bits 6-7: subpx_offset_y, + // bits 8-9: subpx_dir, bit 10: is_packed_glyph + let packed_flags = (((is_packed_glyph as u32) & 0x1) << 26) + | (((subpx_dir as u32) & 0x3) << 24) + | (((subpx_offset_y as u32) & 0x3) << 22) + | (((subpx_offset_x as u32) & 0x3) << 20) + | (((color_mode as u32) & 0xF) << 16); + PrimitiveInstanceData { data: [ self.prim_header_index.0 as i32, clip_task.0 as i32, - (subpx_dir as u32 as i32) << 24 - | (color_mode as u32 as i32) << 16 - | glyph_index_in_text_run, + packed_flags as i32 | glyph_index_in_text_run, glyph_uv_rect.as_int(), ], } diff --git a/gfx/wr/webrender/src/resource_cache.rs b/gfx/wr/webrender/src/resource_cache.rs @@ -67,6 +67,9 @@ pub struct GlyphFetchResult { pub offset: DevicePoint, pub size: DeviceIntSize, pub scale: f32, + pub subpx_offset_x: u8, + pub subpx_offset_y: u8, + pub is_packed_glyph: bool, } // These coordinates are always in texels. @@ -1283,7 +1286,8 @@ impl ResourceCache { font, glyph_keys, |key| { - if let Some(entry) = glyph_key_cache.try_get(key) { + let cache_key = key.cache_key(); + if let Some(entry) = glyph_key_cache.try_get(&cache_key) { match entry { GlyphCacheEntry::Cached(ref glyph) => { if !texture_cache.request(&glyph.texture_cache_handle, gpu_buffer) { @@ -1298,7 +1302,7 @@ impl ResourceCache { } }; - glyph_key_cache.add_glyph(*key, GlyphCacheEntry::Pending); + glyph_key_cache.add_glyph(cache_key, GlyphCacheEntry::Pending); true } @@ -1332,9 +1336,10 @@ impl ResourceCache { debug_assert!(fetch_buffer.is_empty()); for (loop_index, key) in glyph_keys.iter().enumerate() { - let (cache_item, glyph_format) = match *glyph_key_cache.get(key) { + let cache_key = key.cache_key(); + let (cache_item, glyph_format, is_packed_glyph) = match *glyph_key_cache.get(&cache_key) { GlyphCacheEntry::Cached(ref glyph) => { - (self.texture_cache.get(&glyph.texture_cache_handle), glyph.format) + (self.texture_cache.get(&glyph.texture_cache_handle), glyph.format, glyph.is_packed_glyph) } GlyphCacheEntry::Blank | GlyphCacheEntry::Pending => continue, }; @@ -1347,12 +1352,16 @@ impl ResourceCache { current_texture_id = cache_item.texture_id; current_glyph_format = glyph_format; } + let (subpx_offset_x, subpx_offset_y) = key.subpixel_offset(); fetch_buffer.push(GlyphFetchResult { index_in_text_run: loop_index as i32, uv_rect_address: gpu_buffer.resolve_handle(cache_item.uv_rect_handle), offset: DevicePoint::new(cache_item.user_data[0], cache_item.user_data[1]), size: cache_item.uv_rect.size(), scale: cache_item.user_data[2], + subpx_offset_x: subpx_offset_x as u8, + subpx_offset_y: subpx_offset_y as u8, + is_packed_glyph, }); } @@ -1504,6 +1513,7 @@ impl ResourceCache { self.glyph_rasterizer.resolve_glyphs( |job, can_use_r8_format| { let GlyphRasterJob { font, key, result } = job; + let cache_key = key.cache_key(); let glyph_key_cache = cached_glyphs.get_glyph_key_cache_for_font_mut(&*font); let glyph_info = match result { Err(_) => GlyphCacheEntry::Blank, @@ -1536,10 +1546,11 @@ impl ResourceCache { GlyphCacheEntry::Cached(CachedGlyphInfo { texture_cache_handle, format: glyph.format, + is_packed_glyph: glyph.is_packed_glyph, }) } }; - glyph_key_cache.insert(key, glyph_info); + glyph_key_cache.insert(cache_key, glyph_info); }, profile, ); diff --git a/gfx/wr/wr_glyph_rasterizer/src/platform/macos/font.rs b/gfx/wr/wr_glyph_rasterizer/src/platform/macos/font.rs @@ -787,6 +787,7 @@ impl FontContext { GlyphType::Vector => font.get_glyph_format(), }, bytes: rasterized_pixels, + is_packed_glyph: false, })}) } } diff --git a/gfx/wr/wr_glyph_rasterizer/src/platform/unix/font.rs b/gfx/wr/wr_glyph_rasterizer/src/platform/unix/font.rs @@ -1123,6 +1123,7 @@ impl FontContext { scale, format: glyph_format, bytes: final_buffer, + is_packed_glyph: false, }) } } diff --git a/gfx/wr/wr_glyph_rasterizer/src/platform/windows/font.rs b/gfx/wr/wr_glyph_rasterizer/src/platform/windows/font.rs @@ -638,6 +638,7 @@ impl FontContext { scale: (if bitmaps { y_scale.recip() } else { 1.0 }) as f32, format, bytes: bgra_pixels, + is_packed_glyph: false, }) } } diff --git a/gfx/wr/wr_glyph_rasterizer/src/rasterizer.rs b/gfx/wr/wr_glyph_rasterizer/src/rasterizer.rs @@ -1098,11 +1098,27 @@ impl Into<f64> for SubpixelOffset { } } +impl SubpixelOffset { + fn to_f32(self) -> f32 { + match self { + SubpixelOffset::Zero => 0.0, + SubpixelOffset::Quarter => 0.25, + SubpixelOffset::Half => 0.5, + SubpixelOffset::ThreeQuarters => 0.75, + } + } +} + #[derive(Copy, Clone, Hash, PartialEq, Eq, Debug, Ord, PartialOrd)] #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] pub struct GlyphKey(u32); +#[derive(Copy, Clone, Hash, PartialEq, Eq, Debug, Ord, PartialOrd)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct GlyphCacheKey(u32); + impl GlyphKey { pub fn new( index: u32, @@ -1116,22 +1132,49 @@ impl GlyphKey { }; let sox = SubpixelOffset::quantize(dx); let soy = SubpixelOffset::quantize(dy); - assert_eq!(0, index & 0xF0000000); + assert_eq!(0, index & 0xFC000000); - GlyphKey(index | (sox as u32) << 28 | (soy as u32) << 30) + GlyphKey(index | (sox as u32) << 26 | (soy as u32) << 28 | (subpx_dir as u32) << 30) } pub fn index(&self) -> GlyphIndex { - self.0 & 0x0FFFFFFF + self.0 & 0x03FFFFFF } - fn subpixel_offset(&self) -> (SubpixelOffset, SubpixelOffset) { - let x = (self.0 >> 28) as u8 & 3; - let y = (self.0 >> 30) as u8 & 3; + pub fn subpixel_offset(&self) -> (SubpixelOffset, SubpixelOffset) { + let x = (self.0 >> 26) as u8 & 3; + let y = (self.0 >> 28) as u8 & 3; unsafe { (mem::transmute(x), mem::transmute(y)) } } + + pub fn subpixel_dir(&self) -> SubpixelDirection { + let dir = (self.0 >> 30) as u8 & 3; + unsafe { + mem::transmute(dir as u32) + } + } + + pub fn cache_key(&self) -> GlyphCacheKey { + let index = self.index(); + let subpx_dir = self.subpixel_dir(); + assert_eq!(0, index & 0xFC000000); + GlyphCacheKey(index | (subpx_dir as u32) << 30) + } +} + +impl GlyphCacheKey { + pub fn index(&self) -> GlyphIndex { + self.0 & 0x03FFFFFF + } + + pub fn subpixel_dir(&self) -> SubpixelDirection { + let dir = ((self.0 >> 30) & 3) as u32; + unsafe { + mem::transmute(dir) + } + } } #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] @@ -1290,6 +1333,7 @@ pub struct RasterizedGlyph { pub scale: f32, pub format: GlyphFormat, pub bytes: Vec<u8>, + pub is_packed_glyph: bool, } impl RasterizedGlyph { @@ -1667,6 +1711,69 @@ pub type GlyphRasterResult = Result<RasterizedGlyph, GlyphRasterError>; #[cfg_attr(feature = "replay", derive(Deserialize))] pub struct GpuGlyphCacheKey(pub u32); +fn pack_glyph_variants_horizontal(variants: &[RasterizedGlyph]) -> RasterizedGlyph { + // Pack 4 glyph variants horizontally into a single texture. + // Normalize both left and top offsets via padding so all variants can use the same base offsets. + + let min_left = variants.iter().map(|v| v.left.floor()).fold(f32::INFINITY, f32::min); + let max_top = variants.iter().map(|v| v.top.floor()).fold(f32::NEG_INFINITY, f32::max); + + // Slot width must accommodate the widest variant plus left padding + let slot_width = variants.iter() + .map(|v| v.width + (v.left.floor() - min_left) as i32) + .max().unwrap(); + + // Slot height must accommodate the tallest variant plus top padding + let slot_height = variants.iter() + .map(|v| v.height + (max_top - v.top.floor()) as i32) + .max().unwrap(); + + let packed_width = slot_width * 4; + let bpp = 4; + + let mut packed_bytes = vec![0u8; (packed_width * slot_height * bpp) as usize]; + + for (variant_idx, variant) in variants.iter().enumerate() { + // Compute padding needed to normalize both left and top offsets + let left_pad = (variant.left.floor() - min_left) as i32; + let top_pad = (max_top - variant.top.floor()) as i32; + let slot_x = variant_idx as i32 * slot_width; + + for src_y in 0..variant.height { + let dst_y = src_y + top_pad; + if dst_y >= slot_height { + break; + } + + let dst_x = slot_x + left_pad; + if dst_x < 0 { + continue; + } + + let src_row_start = (src_y * variant.width * bpp) as usize; + let src_row_end = src_row_start + (variant.width * bpp) as usize; + let dst_row_start = (dst_y * packed_width * bpp + dst_x * bpp) as usize; + let dst_row_end = dst_row_start + (variant.width * bpp) as usize; + + if dst_row_end <= packed_bytes.len() && src_row_end <= variant.bytes.len() { + packed_bytes[dst_row_start..dst_row_end] + .copy_from_slice(&variant.bytes[src_row_start..src_row_end]); + } + } + } + + RasterizedGlyph { + top: max_top, + left: min_left, + width: packed_width, + height: slot_height, + scale: variants[0].scale, + format: variants[0].format, + bytes: packed_bytes, + is_packed_glyph: true, + } +} + fn process_glyph( context: &mut FontContext, can_use_r8_format: bool, @@ -1674,7 +1781,44 @@ fn process_glyph( key: GlyphKey, ) -> GlyphRasterJob { profile_scope!("glyph-raster"); - let result = context.rasterize_glyph(&font, &key); + + let subpx_dir = key.subpixel_dir(); + + let result = if subpx_dir == SubpixelDirection::None { + context.rasterize_glyph(&font, &key) + } else { + let offsets = [ + SubpixelOffset::Zero, + SubpixelOffset::Quarter, + SubpixelOffset::Half, + SubpixelOffset::ThreeQuarters, + ]; + + let mut variants = Vec::with_capacity(4); + for offset in &offsets { + let variant_key = GlyphKey::new( + key.index(), + match subpx_dir { + SubpixelDirection::Horizontal => DevicePoint::new(offset.to_f32(), 0.0), + SubpixelDirection::Vertical => DevicePoint::new(0.0, offset.to_f32()), + SubpixelDirection::None => DevicePoint::zero(), + }, + subpx_dir, + ); + + match context.rasterize_glyph(&font, &variant_key) { + Ok(glyph) => variants.push(glyph), + Err(e) => return GlyphRasterJob { + font: font, + key: key.clone(), + result: Err(e), + }, + } + } + + Ok(pack_glyph_variants_horizontal(&variants)) + }; + let mut job = GlyphRasterJob { font: font, key: key.clone(), diff --git a/gfx/wr/wrench/reftests/text/reftest.list b/gfx/wr/wrench/reftests/text/reftest.list @@ -19,8 +19,8 @@ skip_on(android,device) fuzzy(1,3692) fuzzy-if(platform(win),2,5585) fuzzy-if(pl fuzzy(2,405) fuzzy-if(platform(swgl),2,1510) == split-batch.yaml split-batch-ref.yaml # Next 3 tests affected by bug 1548099 on Android skip_on(android) == shadow-red.yaml shadow-red-ref.yaml -skip_on(android) fuzzy(1,999) fuzzy-if(platform(swgl),2,1324) == shadow-grey.yaml shadow-grey-ref.yaml -skip_on(android) fuzzy(1,834) fuzzy-if(platform(swgl),2,1538) == shadow-grey-transparent.yaml shadow-grey-ref.yaml +skip_on(android) fuzzy(1,999) fuzzy-if(platform(swgl),2,1331) == shadow-grey.yaml shadow-grey-ref.yaml +skip_on(android) fuzzy(1,834) fuzzy-if(platform(swgl),2,1549) == shadow-grey-transparent.yaml shadow-grey-ref.yaml == subtle-shadow.yaml subtle-shadow-ref.yaml fuzzy(1,64) == shadow-atomic.yaml shadow-atomic-ref.yaml fuzzy(1,64) == shadow-clip-rect.yaml shadow-atomic-ref.yaml @@ -57,7 +57,7 @@ fuzzy(1,1) platform(linux) == two-shadows.yaml two-shadows.png == shadow-fast-clip.yaml shadow-fast-clip-ref.yaml skip_on(android,device) fuzzy-if(platform(win),1,18) == shadow-partial-glyph.yaml shadow-partial-glyph-ref.yaml # Fails on Pixel2 fuzzy(2,212) platform(linux) == shadow-transforms.yaml shadow-transforms.png -fuzzy(2,370) platform(linux) == raster-space.yaml raster-space.png +fuzzy(2,372) platform(linux) == raster-space.yaml raster-space.png skip_on(android) skip_on(mac,>=10.14) != allow-subpixel.yaml allow-subpixel-ref.yaml # Android: we don't enable sub-px aa on this platform. != large-glyphs.yaml blank.yaml != large-line-decoration.yaml blank.yaml