bevyengine / bevy

A refreshingly simple data-driven game engine built in Rust
https://bevyengine.org
Apache License 2.0
35.72k stars 3.53k forks source link

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

Open databasedav opened 7 months ago

databasedav commented 7 months ago

Bevy version

0.12.1 and 0.13.0

Relevant system information

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 with my UI library 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:

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)

//! 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 while SIZE=13 cargo run --example grid_bug_maybe produces an "off by 1" image

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

databasedav commented 7 months ago

:sob: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=3d66df009441e2958207015fa4017e15

fn main() {
    let side = 720.;
    let size = 13;
    let cell = side / size as f32;
    println!("{}", cell + cell + cell + cell + cell + cell + cell + cell + cell + cell + cell + cell + cell);
    let cell_float = side / 13.;
    println!("{}", cell_float + cell_float + cell_float + cell_float + cell_float + cell_float + cell_float + cell_float + cell_float + cell_float + cell_float + cell_float + cell_float);
}
720.0001
720.0001
fn main() {
    let side = 720.;
    let size = 13;
    // let cell = side / size as f32;
    // println!("{}", cell + cell + cell + cell + cell + cell + cell + cell + cell + cell + cell + cell + cell);
    let cell_float = side / 13.;
    println!("{}", cell_float + cell_float + cell_float + cell_float + cell_float + cell_float + cell_float + cell_float + cell_float + cell_float + cell_float + cell_float + cell_float);
}
719.9999999999998