bevyengine / bevyengine/bevy

UI `Grid` `Style.grid_template_columns` `GridTrackRepetition::AutoFill` "off by 1" when particular floats passed to `px`, seems float precision related

Open
#12,152 1 comment 0 reactions 0 assignees View on GitHub
A-UI C-Bug
Dominant language
Rust
Stars
48.2k
Forks
4.8k
Avg merge
3d 16h
Merged PRs (30d)
171

Description

## Bevy version

0.12.1 and 0.13.0

## Relevant system information

- rust version: stable `cargo 1.76.0 (c84b36747 2024-01-18)`
- os: Ubuntu 23.04

```ignore
AdapterInfo { name: "NVIDIA GeForce RTX 2080 SUPER", vendor: 4318, device: 7809, device_type: DiscreteGpu, driver: "NVIDIA", driver_info: "535.129.03", backend: Vulkan }
SystemInfo { os: "Linux 23.04 Ubuntu", kernel: "6.2.0-1005-lowlatency", cpu: "13th Gen Intel(R) Core(TM) i7-13700K", core_count: "16", memory: "62.6 GiB" }
```

## What you did

Discovered this issue when implementing [snake](https://github.com/databasedav/haalka/blob/main/examples/snake/src/main.rs) with my UI library [`haalka`](https://github.com/databasedav/haalka). This version allows the grid to be resized dynamically, and I noticed that the grid "broke" with certain sizes, which one can see in the video I posted on [discord](https://discord.com/channels/691052431525675048/743663673393938453/1201448618956177528):

snake video

https://github.com/bevyengine/bevy/assets/31483365/36089696-d290-414f-91e9-0353ae2dc46f


## What went wrong

Internally, I'm just updating the `Style.grid_template_columns` with the appropriate `RepeatedGridTrack::px(GridTrackRepetition::AutoFill, cell_width)` as the "grid size", the number of cells on each axis, is changed; since the width of the parent container is fixed to `720`, `cell_width` is always `720 / grid_size`.

As can be seen in the snake video, only particular grid sizes trigger the breakage (e.g. 26 and 31), which makes me suspect that there's some float precision shenanigans going on in `taffy` or something. What happens to the cells as a result should be clear from the repro below.

## Additional information

I've created a minimal repro here https://github.com/databasedav/bevy/blob/grid_bug_maybe2/examples/ui/grid_bug_maybe.rs (adapted from the `grid` example)
```rust
//! minimal repro for potential grid layout bug
use bevy::prelude::*;

const DEFAULT_SIDE: f32 = 720.; // try changing this number too, it's the resulting cell size that's the issue ...
const SIDE: Option<&str> = option_env!("SIDE");
const DEFAULT_SIZE: usize = 13; // or 26, 31, 51, 57, 58, but other stuff is fine ...
const SIZE: Option<&str> = option_env!("SIZE");

fn side() -> f32 {
SIDE.and_then(|side| side.parse().ok()).unwrap_or(DEFAULT_SIDE)
}

fn size() -> usize {
SIZE.and_then(|size| size.parse().ok()).unwrap_or(DEFAULT_SIZE)
}

fn cell_size() -> f32 {
side() / size() as f32
}

fn main() {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
resolution: [1000., 1000.].into(),
position: WindowPosition::Centered(MonitorSelection::Primary),
..default()
}),
..default()
}))
.add_systems(Startup, spawn_layout)
.run();
}

fn spawn_layout(mut commands: Commands) {
commands.spawn(Camera2dBundle::default());
commands
.spawn(NodeBundle {
style: Style {
display: Display::Grid,
justify_self: JustifySelf::Center,
align_self: AlignSelf::Center,
..default()
},
background_color: BackgroundColor(Color::WHITE),
..default()
})
.with_children(|builder| {
builder
.spawn(NodeBundle {
style: Style {
display: Display::Grid,
grid_template_columns: RepeatedGridTrack::px(
GridTrackRepetition::AutoFill,
cell_size(),
),
width: Val::Px(side()),
height: Val::Px(side()),
..default()
},
..default()
})
.with_children(|builder| {
for i in 0..size() {
for j in 0..size() {
item_rect(builder, i, j);
}
}
});
});
}

fn item_rect(builder: &mut ChildBuilder, i: usize, j: usize) {
builder
.spawn(NodeBundle {
style: Style {
display: Display::Grid,
padding: UiRect::all(Val::Px(1.)),
width: Val::Px(cell_size()),
height: Val::Px(cell_size()),
..default()
},
background_color: BackgroundColor(Color::BLACK),
..default()
})
.with_children(|builder| {
builder
.spawn(NodeBundle {
background_color: BackgroundColor(Color::RED),
..default()
})
.with_children(|builder| {
builder.spawn(TextBundle::from_section(
format!("{},{}", i, j),
TextStyle {
font_size: 14. * 14. / size() as f32,
color: Color::WHITE,
..default()
},
));
});
});
}
```
which conveniently takes the grid size as an environment variable. For example running `SIZE=12 cargo run --example grid_bug_maybe` produces the expected
![image](https://github.com/bevyengine/bevy/assets/31483365/34db538f-ed44-4b44-ba1c-0e9ae28d1cb4)
while `SIZE=13 cargo run --example grid_bug_maybe` produces an "off by 1"
![image](https://github.com/bevyengine/bevy/assets/31483365/f23076f9-d564-4954-a885-910dc2e83279)

The sizes that produce this behavior that I've identified by just increasing the grid size for a bit in my snake game are (but not probably not limited to) `13, 26, 31, 51, 57, 58`, and they all break the same way using the repro.

I've also noticed that changing the `SIDE`, the width in pixels of each axis, does affect the breaking, so the culprit must be the resulting `cell_size`, e.g. a grid size of `13` works fine with sides of `740` `SIDE=740 SIZE=13 cargo run --example grid_bug_maybe`
![image](https://github.com/bevyengine/bevy/assets/31483365/a30bc954-8fb6-41dc-8f5e-39bbc7780c0a)

Contributor guide

Open the contributing guide

Research direction

Start by running the minimal reproduction in examples/ui/grid_bug_maybe.rs with the listed SIZE and SIDE values, then inspect how Style.grid_template_columns and RepeatedGridTrack::px handle AutoFill. Compare the working and failing grid sizes, especially the resulting cell_size values. Done means the reproduced grids no longer have the off-by-one layout while preserving the expected behavior for the other cases.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
game-dev
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.