commit b04f143b82586e02dcc070e7b5087621b2734bc9
parent b4bce53405fd136b68e6fcf6523f56985d895f2e
Author: Glenn Watson <git@chillybin.org>
Date: Mon, 15 Dec 2025 21:17:48 +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:
11 files changed, 213 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
diff --git a/testing/web-platform/meta/css/css-highlight-api/highlight-text-across-elements.html.ini b/testing/web-platform/meta/css/css-highlight-api/highlight-text-across-elements.html.ini
@@ -0,0 +1,3 @@
+[highlight-text-across-elements.html]
+ fuzzy: maxDifference=0-1; totalPixels=0-7
+
+\ No newline at end of file