Skip to content

Commit ebce9db

Browse files
ickshonperparrett
authored andcommitted
Remove custom rounding (#16097)
# Objective Taffy added layout rounding a while ago but it had a couple of bugs and caused some problems with the fussy `ab_glyph` text implementation. So I disabled Taffy's builtin rounding and added some hacks ad hoc that fixed (some) of those issues. Since then though Taffy's rounding algorithm has improved while we've changed layout a lot and migrated to `cosmic-text` so those hacks don't help any more and in some cases cause significant problems. Also our rounding implementation only rounds to the nearest logical pixel, whereas Taffy rounds to the nearest physical pixel meaning it's much more accurate with high dpi displays. fixes #15197 ## Some examples of layout rounding errors visible in the UI examples These errors are much more obvious at high scale factor, you might not see any problems at a scale factor of 1. `cargo run --example text_wrap_debug` <img width="1000" alt="text_debug_gaps" src="https://github.com/user-attachments/assets/5a584016-b8e2-487b-8842-f0f359077391"> The narrow horizontal and vertical lines are gaps in the layout caused by errors in the coordinate rounding. `cargo run --example text_debug` <img width="1000" alt="text_debug" src="https://github.com/user-attachments/assets/a4b37c02-a2fd-441c-a7bd-cd7a1a72e7dd"> The two text blocks here are aligned right to the same boundary but in this screen shot you can see that the lower block is one pixel off to the left. Because the size of this text node changes between frames with the reported framerate the rounding errors cause it to jump left and right. ## Solution Remove all our custom rounding hacks and reenable Taffy's layout rounding. The gaps in the `text_wrap_debug` example are gone: <img width="1000" alt="text_wrap_debug_fix" src="https://github.com/user-attachments/assets/92d2dd97-30c6-4ac8-99f1-6d65358995a7"> This doesn't fix some of the gaps that occur between borders and content but they seem appear to be a rendering problem as they disappear with `UiAntiAlias::Off` set. ## Testing Run the examples as described above in the `Objective` section. With this PR the problems mentioned shouldn't appear. Also added an example in a separate PR #16096 `layout_rounding_debug` for identifying these issues. ## Migration Guide `UiSurface::get_layout` now also returns the final sizes before rounding. Call `.0` on the `Ok` result to get the previously returned `taffy::Layout` value. --------- Co-authored-by: Rob Parrett <robparrett@gmail.com>
1 parent 503c0a4 commit ebce9db

File tree

2 files changed

+53
-82
lines changed

2 files changed

+53
-82
lines changed

crates/bevy_ui/src/layout/mod.rs

Lines changed: 32 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -296,14 +296,13 @@ with UI components as a child of an entity without UI components, your UI layout
296296
update_uinode_geometry_recursive(
297297
&mut commands,
298298
*root,
299-
&ui_surface,
299+
&mut ui_surface,
300300
None,
301301
&mut node_transform_query,
302302
&ui_children,
303303
inverse_target_scale_factor,
304304
Vec2::ZERO,
305305
Vec2::ZERO,
306-
Vec2::ZERO,
307306
);
308307
}
309308

@@ -315,7 +314,7 @@ with UI components as a child of an entity without UI components, your UI layout
315314
fn update_uinode_geometry_recursive(
316315
commands: &mut Commands,
317316
entity: Entity,
318-
ui_surface: &UiSurface,
317+
ui_surface: &mut UiSurface,
319318
root_size: Option<Vec2>,
320319
node_transform_query: &mut Query<(
321320
&mut ComputedNode,
@@ -329,7 +328,6 @@ with UI components as a child of an entity without UI components, your UI layout
329328
inverse_target_scale_factor: f32,
330329
parent_size: Vec2,
331330
parent_scroll_position: Vec2,
332-
mut absolute_location: Vec2,
333331
) {
334332
if let Ok((
335333
mut node,
@@ -340,28 +338,24 @@ with UI components as a child of an entity without UI components, your UI layout
340338
maybe_scroll_position,
341339
)) = node_transform_query.get_mut(entity)
342340
{
343-
let Ok(layout) = ui_surface.get_layout(entity) else {
341+
let Ok((layout, unrounded_unscaled_size)) = ui_surface.get_layout(entity) else {
344342
return;
345343
};
346344

347345
let layout_size =
348346
inverse_target_scale_factor * Vec2::new(layout.size.width, layout.size.height);
347+
let unrounded_size = inverse_target_scale_factor * unrounded_unscaled_size;
349348
let layout_location =
350349
inverse_target_scale_factor * Vec2::new(layout.location.x, layout.location.y);
351350

352-
absolute_location += layout_location;
353-
354-
let rounded_size = approx_round_layout_coords(absolute_location + layout_size)
355-
- approx_round_layout_coords(absolute_location);
356-
357-
let rounded_location =
358-
approx_round_layout_coords(layout_location - parent_scroll_position)
359-
+ 0.5 * (rounded_size - parent_size);
351+
// The position of the center of the node, stored in the node's transform
352+
let node_center =
353+
layout_location - parent_scroll_position + 0.5 * (layout_size - parent_size);
360354

361355
// only trigger change detection when the new values are different
362-
if node.size != rounded_size || node.unrounded_size != layout_size {
363-
node.size = rounded_size;
364-
node.unrounded_size = layout_size;
356+
if node.size != layout_size || node.unrounded_size != unrounded_size {
357+
node.size = layout_size;
358+
node.unrounded_size = unrounded_size;
365359
}
366360

367361
let taffy_rect_to_border_rect = |rect: taffy::Rect<f32>| BorderRect {
@@ -402,8 +396,8 @@ with UI components as a child of an entity without UI components, your UI layout
402396
.max(0.);
403397
}
404398

405-
if transform.translation.truncate() != rounded_location {
406-
transform.translation = rounded_location.extend(0.);
399+
if transform.translation.truncate() != node_center {
400+
transform.translation = node_center.extend(0.);
407401
}
408402

409403
let scroll_position: Vec2 = maybe_scroll_position
@@ -423,11 +417,9 @@ with UI components as a child of an entity without UI components, your UI layout
423417
})
424418
.unwrap_or_default();
425419

426-
let round_content_size = approx_round_layout_coords(
427-
Vec2::new(layout.content_size.width, layout.content_size.height)
428-
* inverse_target_scale_factor,
429-
);
430-
let max_possible_offset = (round_content_size - rounded_size).max(Vec2::ZERO);
420+
let content_size = Vec2::new(layout.content_size.width, layout.content_size.height)
421+
* inverse_target_scale_factor;
422+
let max_possible_offset = (content_size - layout_size).max(Vec2::ZERO);
431423
let clamped_scroll_position = scroll_position.clamp(Vec2::ZERO, max_possible_offset);
432424

433425
if clamped_scroll_position != scroll_position {
@@ -445,36 +437,14 @@ with UI components as a child of an entity without UI components, your UI layout
445437
node_transform_query,
446438
ui_children,
447439
inverse_target_scale_factor,
448-
rounded_size,
440+
layout_size,
449441
clamped_scroll_position,
450-
absolute_location,
451442
);
452443
}
453444
}
454445
}
455446
}
456447

457-
#[inline]
458-
/// Round `value` to the nearest whole integer, with ties (values with a fractional part equal to 0.5) rounded towards positive infinity.
459-
fn approx_round_ties_up(value: f32) -> f32 {
460-
(value + 0.5).floor()
461-
}
462-
463-
#[inline]
464-
/// Rounds layout coordinates by rounding ties upwards.
465-
///
466-
/// Rounding ties up avoids gaining a pixel when rounding bounds that span from negative to positive.
467-
///
468-
/// Example: The width between bounds of -50.5 and 49.5 before rounding is 100, using:
469-
/// - `f32::round`: width becomes 101 (rounds to -51 and 50).
470-
/// - `round_ties_up`: width is 100 (rounds to -50 and 50).
471-
fn approx_round_layout_coords(value: Vec2) -> Vec2 {
472-
Vec2 {
473-
x: approx_round_ties_up(value.x),
474-
y: approx_round_ties_up(value.y),
475-
}
476-
}
477-
478448
#[cfg(test)]
479449
mod tests {
480450
use taffy::TraversePartialTree;
@@ -493,7 +463,7 @@ mod tests {
493463
use bevy_hierarchy::{
494464
despawn_with_children_recursive, BuildChildren, ChildBuild, Children, Parent,
495465
};
496-
use bevy_math::{vec2, Rect, UVec2, Vec2};
466+
use bevy_math::{Rect, UVec2, Vec2};
497467
use bevy_render::{
498468
camera::{ManualTextureViews, OrthographicProjection},
499469
prelude::Camera,
@@ -510,21 +480,10 @@ mod tests {
510480
};
511481

512482
use crate::{
513-
layout::{approx_round_layout_coords, ui_surface::UiSurface},
514-
prelude::*,
515-
ui_layout_system,
516-
update::update_target_camera_system,
517-
ContentSize, LayoutContext,
483+
layout::ui_surface::UiSurface, prelude::*, ui_layout_system,
484+
update::update_target_camera_system, ContentSize, LayoutContext,
518485
};
519486

520-
#[test]
521-
fn round_layout_coords_must_round_ties_up() {
522-
assert_eq!(
523-
approx_round_layout_coords(vec2(-50.5, 49.5)),
524-
vec2(-50., 50.)
525-
);
526-
}
527-
528487
// these window dimensions are easy to convert to and from percentage values
529488
const WINDOW_WIDTH: f32 = 1000.;
530489
const WINDOW_HEIGHT: f32 = 100.;
@@ -598,10 +557,10 @@ mod tests {
598557
world.entity_mut(ui_root).add_child(ui_child);
599558

600559
ui_schedule.run(&mut world);
601-
let ui_surface = world.resource::<UiSurface>();
560+
let mut ui_surface = world.resource_mut::<UiSurface>();
602561

603562
for ui_entity in [ui_root, ui_child] {
604-
let layout = ui_surface.get_layout(ui_entity).unwrap();
563+
let layout = ui_surface.get_layout(ui_entity).unwrap().0;
605564
assert_eq!(layout.size.width, WINDOW_WIDTH);
606565
assert_eq!(layout.size.height, WINDOW_HEIGHT);
607566
}
@@ -955,11 +914,12 @@ mod tests {
955914
.get_single(world)
956915
.expect("missing MovingUiNode");
957916
assert_eq!(expected_camera_entity, target_camera_entity);
958-
let ui_surface = world.resource::<UiSurface>();
917+
let mut ui_surface = world.resource_mut::<UiSurface>();
959918

960919
let layout = ui_surface
961920
.get_layout(ui_node_entity)
962-
.expect("failed to get layout");
921+
.expect("failed to get layout")
922+
.0;
963923

964924
// negative test for #12255
965925
assert_eq!(Vec2::new(layout.location.x, layout.location.y), new_pos);
@@ -1043,8 +1003,8 @@ mod tests {
10431003

10441004
ui_schedule.run(&mut world);
10451005

1046-
let ui_surface = world.resource::<UiSurface>();
1047-
let layout = ui_surface.get_layout(ui_entity).unwrap();
1006+
let mut ui_surface = world.resource_mut::<UiSurface>();
1007+
let layout = ui_surface.get_layout(ui_entity).unwrap().0;
10481008

10491009
// the node should takes its size from the fixed size measure func
10501010
assert_eq!(layout.size.width, content_size.x);
@@ -1068,25 +1028,25 @@ mod tests {
10681028

10691029
ui_schedule.run(&mut world);
10701030

1071-
let ui_surface = world.resource::<UiSurface>();
1031+
let mut ui_surface = world.resource_mut::<UiSurface>();
10721032
let ui_node = ui_surface.entity_to_taffy[&ui_entity];
10731033

10741034
// a node with a content size should have taffy context
10751035
assert!(ui_surface.taffy.get_node_context(ui_node).is_some());
1076-
let layout = ui_surface.get_layout(ui_entity).unwrap();
1036+
let layout = ui_surface.get_layout(ui_entity).unwrap().0;
10771037
assert_eq!(layout.size.width, content_size.x);
10781038
assert_eq!(layout.size.height, content_size.y);
10791039

10801040
world.entity_mut(ui_entity).remove::<ContentSize>();
10811041

10821042
ui_schedule.run(&mut world);
10831043

1084-
let ui_surface = world.resource::<UiSurface>();
1044+
let mut ui_surface = world.resource_mut::<UiSurface>();
10851045
// a node without a content size should not have taffy context
10861046
assert!(ui_surface.taffy.get_node_context(ui_node).is_none());
10871047

10881048
// Without a content size, the node has no width or height constraints so the length of both dimensions is 0.
1089-
let layout = ui_surface.get_layout(ui_entity).unwrap();
1049+
let layout = ui_surface.get_layout(ui_entity).unwrap().0;
10901050
assert_eq!(layout.size.width, 0.);
10911051
assert_eq!(layout.size.height, 0.);
10921052
}
@@ -1123,7 +1083,8 @@ mod tests {
11231083
.collect::<Vec<Entity>>();
11241084

11251085
for r in [2, 3, 5, 7, 11, 13, 17, 19, 21, 23, 29, 31].map(|n| (n as f32).recip()) {
1126-
let mut s = r;
1086+
// This fails with very small / unrealistic scale values
1087+
let mut s = 1. - r;
11271088
while s <= 5. {
11281089
world.resource_mut::<UiScale>().0 = s;
11291090
ui_schedule.run(&mut world);

crates/bevy_ui/src/layout/ui_surface.rs

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use bevy_ecs::{
66
entity::{Entity, EntityHashMap},
77
prelude::Resource,
88
};
9-
use bevy_math::UVec2;
9+
use bevy_math::{UVec2, Vec2};
1010
use bevy_utils::default;
1111

1212
use crate::{layout::convert, LayoutContext, LayoutError, Measure, MeasureArgs, Node, NodeMeasure};
@@ -51,8 +51,7 @@ impl fmt::Debug for UiSurface {
5151

5252
impl Default for UiSurface {
5353
fn default() -> Self {
54-
let mut taffy: TaffyTree<NodeMeasure> = TaffyTree::new();
55-
taffy.disable_rounding();
54+
let taffy: TaffyTree<NodeMeasure> = TaffyTree::new();
5655
Self {
5756
entity_to_taffy: Default::default(),
5857
camera_entity_to_taffy: Default::default(),
@@ -276,14 +275,25 @@ impl UiSurface {
276275

277276
/// Get the layout geometry for the taffy node corresponding to the ui node [`Entity`].
278277
/// Does not compute the layout geometry, `compute_window_layouts` should be run before using this function.
279-
pub fn get_layout(&self, entity: Entity) -> Result<&taffy::Layout, LayoutError> {
280-
if let Some(taffy_node) = self.entity_to_taffy.get(&entity) {
281-
self.taffy
282-
.layout(*taffy_node)
283-
.map_err(LayoutError::TaffyError)
284-
} else {
285-
Err(LayoutError::InvalidHierarchy)
286-
}
278+
/// On success returns a pair consisiting of the final resolved layout values after rounding
279+
/// and the size of the node after layout resolution but before rounding.
280+
pub fn get_layout(&mut self, entity: Entity) -> Result<(taffy::Layout, Vec2), LayoutError> {
281+
let Some(taffy_node) = self.entity_to_taffy.get(&entity) else {
282+
return Err(LayoutError::InvalidHierarchy);
283+
};
284+
285+
let layout = self
286+
.taffy
287+
.layout(*taffy_node)
288+
.cloned()
289+
.map_err(LayoutError::TaffyError)?;
290+
291+
self.taffy.disable_rounding();
292+
let taffy_size = self.taffy.layout(*taffy_node).unwrap().size;
293+
let unrounded_size = Vec2::new(taffy_size.width, taffy_size.height);
294+
self.taffy.enable_rounding();
295+
296+
Ok((layout, unrounded_size))
287297
}
288298
}
289299

0 commit comments

Comments
 (0)