Skip to content

Commit dba7c74

Browse files
authored
Don't update the text buffer in text_system (#21966)
# Objective Text is shaped in `measure_text_system`. When the schedule reaches `text_system` there's no need to reupdate the cosmic-text buffer a second time. `text_system` should only be updating any stale `TextLayoutInfo` components. ## Solution * Add a new method `update_text_layout_info` to `TextPipeline`. This method updates the given `TextLayoutInfo` without performing any shaping. * Call `update_text_layout_info` instead of `queue_text` from `text_system`. * Only query for `TextFont`, instead of the full `TextUiReader`. # The next step is to remove `TextPipeline::queue_text`. I didn't do that here as it's a fairly large refactor and I have a bunch of other open text PRs I'd like to get merged first. ## Testing #### yellow = this PR, red = main ``` cargo run --example many_glyphs --release --features trace_tracy,debug -- --no-text2d --recompute-text ``` <img width="1599" height="851" alt="Screenshot 2025-11-28 130411" src="https://github.com/user-attachments/assets/d21a42bf-8ea9-48b9-8cf3-24f742cb4174" /> ``` cargo run --example many_buttons --release --features trace_tracy,debug -- --text --respawn ``` <img width="1597" height="767" alt="Screenshot 2025-11-28 145240" src="https://github.com/user-attachments/assets/75d2d989-37fc-4804-9c6d-6056413d91ef" />
1 parent ecbb562 commit dba7c74

File tree

2 files changed

+233
-53
lines changed

2 files changed

+233
-53
lines changed

crates/bevy_text/src/pipeline.rs

Lines changed: 188 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ use bevy_asset::{AssetId, Assets};
44
use bevy_color::Color;
55
use bevy_derive::{Deref, DerefMut};
66
use bevy_ecs::{
7-
component::Component, entity::Entity, reflect::ReflectComponent, resource::Resource,
8-
system::ResMut,
7+
component::Component,
8+
entity::Entity,
9+
reflect::ReflectComponent,
10+
resource::Resource,
11+
system::{Query, ResMut},
912
};
1013
use bevy_image::prelude::*;
1114
use bevy_log::{once, warn};
@@ -498,6 +501,189 @@ impl TextPipeline {
498501
.cloned()
499502
.map(|(id, _)| id)
500503
}
504+
505+
/// Update [`TextLayoutInfo`] with the new [`PositionedGlyph`] layout.
506+
pub fn update_text_layout_info<'a>(
507+
&mut self,
508+
layout_info: &mut TextLayoutInfo,
509+
text_font_query: Query<&'a TextFont>,
510+
scale_factor: f64,
511+
font_atlas_set: &mut FontAtlasSet,
512+
texture_atlases: &mut Assets<TextureAtlasLayout>,
513+
textures: &mut Assets<Image>,
514+
computed: &mut ComputedTextBlock,
515+
font_system: &mut CosmicFontSystem,
516+
swash_cache: &mut SwashCache,
517+
bounds: TextBounds,
518+
) -> Result<(), TextError> {
519+
layout_info.glyphs.clear();
520+
layout_info.run_geometry.clear();
521+
layout_info.size = Default::default();
522+
523+
self.glyph_info.clear();
524+
525+
for text_font in text_font_query.iter_many(computed.entities.iter().map(|e| e.entity)) {
526+
let mut section_info = (
527+
text_font.font.id(),
528+
text_font.font_smoothing,
529+
text_font.font_size,
530+
0.0,
531+
0.0,
532+
0.0,
533+
);
534+
535+
if let Some((id, _)) = self.map_handle_to_font_id.get(&section_info.0) {
536+
let weight = font_system
537+
.db()
538+
.face(*id)
539+
.map(|f| f.weight)
540+
.unwrap_or(cosmic_text::Weight::NORMAL);
541+
if let Some(font) = font_system.get_font(*id, weight) {
542+
let swash = font.as_swash();
543+
let metrics = swash.metrics(&[]);
544+
let upem = metrics.units_per_em as f32;
545+
let scalar = section_info.2 * scale_factor as f32 / upem;
546+
section_info.3 = (metrics.strikeout_offset * scalar).round();
547+
section_info.4 = (metrics.stroke_size * scalar).round().max(1.);
548+
section_info.5 = (metrics.underline_offset * scalar).round();
549+
}
550+
}
551+
self.glyph_info.push(section_info);
552+
}
553+
554+
let buffer = &mut computed.buffer;
555+
buffer.set_size(font_system, bounds.width, bounds.height);
556+
let box_size = buffer_dimensions(buffer);
557+
558+
let result = buffer.layout_runs().try_for_each(|run| {
559+
let mut current_section: Option<usize> = None;
560+
let mut start = 0.;
561+
let mut end = 0.;
562+
let result = run
563+
.glyphs
564+
.iter()
565+
.map(move |layout_glyph| (layout_glyph, run.line_y, run.line_i))
566+
.try_for_each(|(layout_glyph, line_y, line_i)| {
567+
match current_section {
568+
Some(section) => {
569+
if section != layout_glyph.metadata {
570+
layout_info.run_geometry.push(RunGeometry {
571+
span_index: section,
572+
bounds: Rect::new(
573+
start,
574+
run.line_top,
575+
end,
576+
run.line_top + run.line_height,
577+
),
578+
strikethrough_y: (run.line_y - self.glyph_info[section].3)
579+
.round(),
580+
strikethrough_thickness: self.glyph_info[section].4,
581+
underline_y: (run.line_y - self.glyph_info[section].5).round(),
582+
underline_thickness: self.glyph_info[section].4,
583+
});
584+
start = end.max(layout_glyph.x);
585+
current_section = Some(layout_glyph.metadata);
586+
}
587+
end = layout_glyph.x + layout_glyph.w;
588+
}
589+
None => {
590+
current_section = Some(layout_glyph.metadata);
591+
start = layout_glyph.x;
592+
end = start + layout_glyph.w;
593+
}
594+
}
595+
596+
let mut temp_glyph;
597+
let span_index = layout_glyph.metadata;
598+
let font_id = self.glyph_info[span_index].0;
599+
let font_smoothing = self.glyph_info[span_index].1;
600+
601+
let layout_glyph = if font_smoothing == FontSmoothing::None {
602+
// If font smoothing is disabled, round the glyph positions and sizes,
603+
// effectively discarding all subpixel layout.
604+
temp_glyph = layout_glyph.clone();
605+
temp_glyph.x = temp_glyph.x.round();
606+
temp_glyph.y = temp_glyph.y.round();
607+
temp_glyph.w = temp_glyph.w.round();
608+
temp_glyph.x_offset = temp_glyph.x_offset.round();
609+
temp_glyph.y_offset = temp_glyph.y_offset.round();
610+
temp_glyph.line_height_opt = temp_glyph.line_height_opt.map(f32::round);
611+
612+
&temp_glyph
613+
} else {
614+
layout_glyph
615+
};
616+
617+
let physical_glyph = layout_glyph.physical((0., 0.), 1.);
618+
619+
let font_atlases = font_atlas_set
620+
.entry(FontAtlasKey(
621+
font_id,
622+
physical_glyph.cache_key.font_size_bits,
623+
font_smoothing,
624+
))
625+
.or_default();
626+
627+
let atlas_info = get_glyph_atlas_info(font_atlases, physical_glyph.cache_key)
628+
.map(Ok)
629+
.unwrap_or_else(|| {
630+
add_glyph_to_atlas(
631+
font_atlases,
632+
texture_atlases,
633+
textures,
634+
&mut font_system.0,
635+
&mut swash_cache.0,
636+
layout_glyph,
637+
font_smoothing,
638+
)
639+
})?;
640+
641+
let texture_atlas = texture_atlases.get(atlas_info.texture_atlas).unwrap();
642+
let location = atlas_info.location;
643+
let glyph_rect = texture_atlas.textures[location.glyph_index];
644+
let left = location.offset.x as f32;
645+
let top = location.offset.y as f32;
646+
let glyph_size = UVec2::new(glyph_rect.width(), glyph_rect.height());
647+
648+
// offset by half the size because the origin is center
649+
let x = glyph_size.x as f32 / 2.0 + left + physical_glyph.x as f32;
650+
let y =
651+
line_y.round() + physical_glyph.y as f32 - top + glyph_size.y as f32 / 2.0;
652+
653+
let position = Vec2::new(x, y);
654+
655+
let pos_glyph = PositionedGlyph {
656+
position,
657+
size: glyph_size.as_vec2(),
658+
atlas_info,
659+
span_index,
660+
byte_index: layout_glyph.start,
661+
byte_length: layout_glyph.end - layout_glyph.start,
662+
line_index: line_i,
663+
};
664+
layout_info.glyphs.push(pos_glyph);
665+
Ok(())
666+
});
667+
if let Some(section) = current_section {
668+
layout_info.run_geometry.push(RunGeometry {
669+
span_index: section,
670+
bounds: Rect::new(start, run.line_top, end, run.line_top + run.line_height),
671+
strikethrough_y: (run.line_y - self.glyph_info[section].3).round(),
672+
strikethrough_thickness: self.glyph_info[section].4,
673+
underline_y: (run.line_y - self.glyph_info[section].5).round(),
674+
underline_thickness: self.glyph_info[section].4,
675+
});
676+
}
677+
678+
result
679+
});
680+
681+
// Check result.
682+
result?;
683+
684+
layout_info.size = box_size;
685+
Ok(())
686+
}
501687
}
502688

503689
/// Render information for a corresponding text block.

crates/bevy_ui/src/widget/text.rs

Lines changed: 45 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -319,72 +319,66 @@ pub fn measure_text_system(
319319
/// It does not modify or observe existing ones. The exception is when adding new glyphs to a [`bevy_text::FontAtlas`].
320320
pub fn text_system(
321321
mut textures: ResMut<Assets<Image>>,
322-
fonts: Res<Assets<Font>>,
323322
mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>,
324323
mut font_atlas_set: ResMut<FontAtlasSet>,
325324
mut text_pipeline: ResMut<TextPipeline>,
326325
mut text_query: Query<(
327-
Entity,
328326
Ref<ComputedNode>,
329327
&TextLayout,
330328
&mut TextLayoutInfo,
331329
&mut TextNodeFlags,
332330
&mut ComputedTextBlock,
333331
)>,
334-
mut text_reader: TextUiReader,
332+
text_font_query: Query<&TextFont>,
335333
mut font_system: ResMut<CosmicFontSystem>,
336334
mut swash_cache: ResMut<SwashCache>,
337335
) {
338-
for (entity, node, block, text_layout_info, mut text_flags, mut computed) in &mut text_query {
339-
// Skip the text node if it is waiting for a new measure func
340-
if text_flags.needs_measure_fn {
341-
continue;
342-
}
343-
344-
if !(node.is_changed() || text_flags.needs_recompute) {
345-
continue;
346-
}
336+
for (node, block, mut text_layout_info, mut text_flags, mut computed) in &mut text_query {
337+
if node.is_changed() || text_flags.needs_recompute {
338+
// Skip the text node if it is waiting for a new measure func
339+
if text_flags.needs_measure_fn {
340+
continue;
341+
}
347342

348-
let physical_node_size = if block.linebreak == LineBreak::NoWrap {
349-
// With `NoWrap` set, no constraints are placed on the width of the text.
350-
TextBounds::UNBOUNDED
351-
} else {
352-
// `scale_factor` is already multiplied by `UiScale`
353-
TextBounds::new(node.unrounded_size.x, node.unrounded_size.y)
354-
};
343+
let scale_factor = node.inverse_scale_factor().recip().into();
344+
let physical_node_size = if block.linebreak == LineBreak::NoWrap {
345+
// With `NoWrap` set, no constraints are placed on the width of the text.
346+
TextBounds::UNBOUNDED
347+
} else {
348+
// `scale_factor` is already multiplied by `UiScale`
349+
TextBounds::new(node.unrounded_size.x, node.unrounded_size.y)
350+
};
355351

356-
let text_layout_info = text_layout_info.into_inner();
357-
match text_pipeline.queue_text(
358-
text_layout_info,
359-
&fonts,
360-
text_reader.iter(entity),
361-
node.inverse_scale_factor.recip() as f64,
362-
block,
363-
physical_node_size,
364-
&mut font_atlas_set,
365-
&mut texture_atlases,
366-
&mut textures,
367-
&mut computed,
368-
&mut font_system,
369-
&mut swash_cache,
370-
) {
371-
Err(TextError::NoSuchFont) => {
372-
// There was an error processing the text layout, try again next frame
373-
text_flags.needs_recompute = true;
374-
}
375-
Err(
376-
e @ (TextError::FailedToAddGlyph(_)
377-
| TextError::FailedToGetGlyphImage(_)
378-
| TextError::MissingAtlasLayout
379-
| TextError::MissingAtlasTexture
380-
| TextError::InconsistentAtlasState),
381-
) => {
382-
panic!("Fatal error when processing text: {e}.");
383-
}
384-
Ok(()) => {
385-
text_layout_info.scale_factor = node.inverse_scale_factor.recip();
386-
text_layout_info.size *= node.inverse_scale_factor;
387-
text_flags.needs_recompute = false;
352+
match text_pipeline.update_text_layout_info(
353+
&mut text_layout_info,
354+
text_font_query,
355+
scale_factor,
356+
&mut font_atlas_set,
357+
&mut texture_atlases,
358+
&mut textures,
359+
&mut computed,
360+
&mut font_system,
361+
&mut swash_cache,
362+
physical_node_size,
363+
) {
364+
Err(TextError::NoSuchFont) => {
365+
// There was an error processing the text layout, try again next frame
366+
text_flags.needs_recompute = true;
367+
}
368+
Err(
369+
e @ (TextError::FailedToAddGlyph(_)
370+
| TextError::FailedToGetGlyphImage(_)
371+
| TextError::MissingAtlasLayout
372+
| TextError::MissingAtlasTexture
373+
| TextError::InconsistentAtlasState),
374+
) => {
375+
panic!("Fatal error when processing text: {e}.");
376+
}
377+
Ok(()) => {
378+
text_layout_info.scale_factor = scale_factor as f32;
379+
text_layout_info.size *= node.inverse_scale_factor();
380+
text_flags.needs_recompute = false;
381+
}
388382
}
389383
}
390384
}

0 commit comments

Comments
 (0)