Skip to content

Commit

Permalink
Remove custom rounding (#16097)
Browse files Browse the repository at this point in the history
# 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 <[email protected]>
  • Loading branch information
ickshonpe and rparrett authored Oct 28, 2024
1 parent af27907 commit 05d9068
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 82 deletions.
103 changes: 32 additions & 71 deletions crates/bevy_ui/src/layout/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,14 +296,13 @@ with UI components as a child of an entity without UI components, your UI layout
update_uinode_geometry_recursive(
&mut commands,
*root,
&ui_surface,
&mut ui_surface,
None,
&mut node_transform_query,
&ui_children,
inverse_target_scale_factor,
Vec2::ZERO,
Vec2::ZERO,
Vec2::ZERO,
);
}

Expand All @@ -315,7 +314,7 @@ with UI components as a child of an entity without UI components, your UI layout
fn update_uinode_geometry_recursive(
commands: &mut Commands,
entity: Entity,
ui_surface: &UiSurface,
ui_surface: &mut UiSurface,
root_size: Option<Vec2>,
node_transform_query: &mut Query<(
&mut ComputedNode,
Expand All @@ -329,7 +328,6 @@ with UI components as a child of an entity without UI components, your UI layout
inverse_target_scale_factor: f32,
parent_size: Vec2,
parent_scroll_position: Vec2,
mut absolute_location: Vec2,
) {
if let Ok((
mut node,
Expand All @@ -340,28 +338,24 @@ with UI components as a child of an entity without UI components, your UI layout
maybe_scroll_position,
)) = node_transform_query.get_mut(entity)
{
let Ok(layout) = ui_surface.get_layout(entity) else {
let Ok((layout, unrounded_unscaled_size)) = ui_surface.get_layout(entity) else {
return;
};

let layout_size =
inverse_target_scale_factor * Vec2::new(layout.size.width, layout.size.height);
let unrounded_size = inverse_target_scale_factor * unrounded_unscaled_size;
let layout_location =
inverse_target_scale_factor * Vec2::new(layout.location.x, layout.location.y);

absolute_location += layout_location;

let rounded_size = approx_round_layout_coords(absolute_location + layout_size)
- approx_round_layout_coords(absolute_location);

let rounded_location =
approx_round_layout_coords(layout_location - parent_scroll_position)
+ 0.5 * (rounded_size - parent_size);
// The position of the center of the node, stored in the node's transform
let node_center =
layout_location - parent_scroll_position + 0.5 * (layout_size - parent_size);

// only trigger change detection when the new values are different
if node.size != rounded_size || node.unrounded_size != layout_size {
node.size = rounded_size;
node.unrounded_size = layout_size;
if node.size != layout_size || node.unrounded_size != unrounded_size {
node.size = layout_size;
node.unrounded_size = unrounded_size;
}

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

if transform.translation.truncate() != rounded_location {
transform.translation = rounded_location.extend(0.);
if transform.translation.truncate() != node_center {
transform.translation = node_center.extend(0.);
}

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

let round_content_size = approx_round_layout_coords(
Vec2::new(layout.content_size.width, layout.content_size.height)
* inverse_target_scale_factor,
);
let max_possible_offset = (round_content_size - rounded_size).max(Vec2::ZERO);
let content_size = Vec2::new(layout.content_size.width, layout.content_size.height)
* inverse_target_scale_factor;
let max_possible_offset = (content_size - layout_size).max(Vec2::ZERO);
let clamped_scroll_position = scroll_position.clamp(Vec2::ZERO, max_possible_offset);

if clamped_scroll_position != scroll_position {
Expand All @@ -445,36 +437,14 @@ with UI components as a child of an entity without UI components, your UI layout
node_transform_query,
ui_children,
inverse_target_scale_factor,
rounded_size,
layout_size,
clamped_scroll_position,
absolute_location,
);
}
}
}
}

#[inline]
/// Round `value` to the nearest whole integer, with ties (values with a fractional part equal to 0.5) rounded towards positive infinity.
fn approx_round_ties_up(value: f32) -> f32 {
(value + 0.5).floor()
}

#[inline]
/// Rounds layout coordinates by rounding ties upwards.
///
/// Rounding ties up avoids gaining a pixel when rounding bounds that span from negative to positive.
///
/// Example: The width between bounds of -50.5 and 49.5 before rounding is 100, using:
/// - `f32::round`: width becomes 101 (rounds to -51 and 50).
/// - `round_ties_up`: width is 100 (rounds to -50 and 50).
fn approx_round_layout_coords(value: Vec2) -> Vec2 {
Vec2 {
x: approx_round_ties_up(value.x),
y: approx_round_ties_up(value.y),
}
}

#[cfg(test)]
mod tests {
use taffy::TraversePartialTree;
Expand All @@ -493,7 +463,7 @@ mod tests {
use bevy_hierarchy::{
despawn_with_children_recursive, BuildChildren, ChildBuild, Children, Parent,
};
use bevy_math::{vec2, Rect, UVec2, Vec2};
use bevy_math::{Rect, UVec2, Vec2};
use bevy_render::{
camera::{ManualTextureViews, OrthographicProjection},
prelude::Camera,
Expand All @@ -510,21 +480,10 @@ mod tests {
};

use crate::{
layout::{approx_round_layout_coords, ui_surface::UiSurface},
prelude::*,
ui_layout_system,
update::update_target_camera_system,
ContentSize, LayoutContext,
layout::ui_surface::UiSurface, prelude::*, ui_layout_system,
update::update_target_camera_system, ContentSize, LayoutContext,
};

#[test]
fn round_layout_coords_must_round_ties_up() {
assert_eq!(
approx_round_layout_coords(vec2(-50.5, 49.5)),
vec2(-50., 50.)
);
}

// these window dimensions are easy to convert to and from percentage values
const WINDOW_WIDTH: f32 = 1000.;
const WINDOW_HEIGHT: f32 = 100.;
Expand Down Expand Up @@ -598,10 +557,10 @@ mod tests {
world.entity_mut(ui_root).add_child(ui_child);

ui_schedule.run(&mut world);
let ui_surface = world.resource::<UiSurface>();
let mut ui_surface = world.resource_mut::<UiSurface>();

for ui_entity in [ui_root, ui_child] {
let layout = ui_surface.get_layout(ui_entity).unwrap();
let layout = ui_surface.get_layout(ui_entity).unwrap().0;
assert_eq!(layout.size.width, WINDOW_WIDTH);
assert_eq!(layout.size.height, WINDOW_HEIGHT);
}
Expand Down Expand Up @@ -955,11 +914,12 @@ mod tests {
.get_single(world)
.expect("missing MovingUiNode");
assert_eq!(expected_camera_entity, target_camera_entity);
let ui_surface = world.resource::<UiSurface>();
let mut ui_surface = world.resource_mut::<UiSurface>();

let layout = ui_surface
.get_layout(ui_node_entity)
.expect("failed to get layout");
.expect("failed to get layout")
.0;

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

ui_schedule.run(&mut world);

let ui_surface = world.resource::<UiSurface>();
let layout = ui_surface.get_layout(ui_entity).unwrap();
let mut ui_surface = world.resource_mut::<UiSurface>();
let layout = ui_surface.get_layout(ui_entity).unwrap().0;

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

ui_schedule.run(&mut world);

let ui_surface = world.resource::<UiSurface>();
let mut ui_surface = world.resource_mut::<UiSurface>();
let ui_node = ui_surface.entity_to_taffy[&ui_entity];

// a node with a content size should have taffy context
assert!(ui_surface.taffy.get_node_context(ui_node).is_some());
let layout = ui_surface.get_layout(ui_entity).unwrap();
let layout = ui_surface.get_layout(ui_entity).unwrap().0;
assert_eq!(layout.size.width, content_size.x);
assert_eq!(layout.size.height, content_size.y);

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

ui_schedule.run(&mut world);

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

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

for r in [2, 3, 5, 7, 11, 13, 17, 19, 21, 23, 29, 31].map(|n| (n as f32).recip()) {
let mut s = r;
// This fails with very small / unrealistic scale values
let mut s = 1. - r;
while s <= 5. {
world.resource_mut::<UiScale>().0 = s;
ui_schedule.run(&mut world);
Expand Down
32 changes: 21 additions & 11 deletions crates/bevy_ui/src/layout/ui_surface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use bevy_ecs::{
entity::{Entity, EntityHashMap},
prelude::Resource,
};
use bevy_math::UVec2;
use bevy_math::{UVec2, Vec2};
use bevy_utils::default;

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

impl Default for UiSurface {
fn default() -> Self {
let mut taffy: TaffyTree<NodeMeasure> = TaffyTree::new();
taffy.disable_rounding();
let taffy: TaffyTree<NodeMeasure> = TaffyTree::new();
Self {
entity_to_taffy: Default::default(),
camera_entity_to_taffy: Default::default(),
Expand Down Expand Up @@ -276,14 +275,25 @@ impl UiSurface {

/// Get the layout geometry for the taffy node corresponding to the ui node [`Entity`].
/// Does not compute the layout geometry, `compute_window_layouts` should be run before using this function.
pub fn get_layout(&self, entity: Entity) -> Result<&taffy::Layout, LayoutError> {
if let Some(taffy_node) = self.entity_to_taffy.get(&entity) {
self.taffy
.layout(*taffy_node)
.map_err(LayoutError::TaffyError)
} else {
Err(LayoutError::InvalidHierarchy)
}
/// On success returns a pair consisiting of the final resolved layout values after rounding
/// and the size of the node after layout resolution but before rounding.
pub fn get_layout(&mut self, entity: Entity) -> Result<(taffy::Layout, Vec2), LayoutError> {
let Some(taffy_node) = self.entity_to_taffy.get(&entity) else {
return Err(LayoutError::InvalidHierarchy);
};

let layout = self
.taffy
.layout(*taffy_node)
.cloned()
.map_err(LayoutError::TaffyError)?;

self.taffy.disable_rounding();
let taffy_size = self.taffy.layout(*taffy_node).unwrap().size;
let unrounded_size = Vec2::new(taffy_size.width, taffy_size.height);
self.taffy.enable_rounding();

Ok((layout, unrounded_size))
}
}

Expand Down

0 comments on commit 05d9068

Please sign in to comment.