bevyengine / bevy

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

UI sometimes renders at half the size #14533

Closed msvbg closed 3 weeks ago

msvbg commented 3 months ago

Bevy version

0.14.0. This issue was not present in 0.13

[Optional] Relevant system information

AdapterInfo { name: "Apple M3 Max", vendor: 0, device: 0, device_type: IntegratedGpu, driver: "", driver_info: "", backend: Metal }

I'm using a Retina screen with double scale factor, which is likely to be part of the problem here.

What you did

Started my game.

What went wrong

Sometimes, but not always, my games renders all UI at half the size.

Additional information

This is probably some sort of system ordering issue since it happens inconsistently and infrequently. Unfortunately my game is quite large at this point and it's difficult to break out a minimally reproducible example.

msvbg commented 3 months ago

It doesn't seem to reproduce with the borders example from the bevy repo at version 0.14.0.

rparrett commented 3 months ago

I have seen this occasionally (wild guess: <10% of the time) with one of my projects: https://github.com/rparrett/taipo. It's pretty rare and my attempts at a minimal repro have been unsuccessful so far.

msvbg commented 3 months ago

It's encouraging to hear that I'm not the only one with this problem! I peeked through your code but you don't seem to be doing anything too crazy with windowing or cameras. We both spawn the camera in an OnEnter(State) schedule instead of at Startup like most examples, which may or may not be something.

msvbg commented 3 months ago

That is indeed it! I can reproduce the issue in the borders example with this tiny change:

//! Example demonstrating bordered UI nodes

use bevy::{color::palettes::css::*, prelude::*};

#[derive(States, PartialEq, Eq, Debug, Clone, Hash, Default)]
enum MyState {
    #[default]
    A,
    B,
}

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .init_state::<MyState>()
        .add_systems(Startup, setup)
        .add_systems(Update, change_state)
        .add_systems(OnEnter(MyState::B), create_camera)
        .run();
}

fn change_state(mut state: ResMut<NextState<MyState>>) {
    state.set(MyState::B);
}

fn create_camera(mut commands: Commands) {
    commands.spawn(Camera2dBundle::default());
}

fn setup(mut commands: Commands) {
    let root = commands
        .spawn(NodeBundle {
            style: Style {
                margin: UiRect::all(Val::Px(25.0)),
                align_self: AlignSelf::Stretch,
                justify_self: JustifySelf::Stretch,
                flex_wrap: FlexWrap::Wrap,
                justify_content: JustifyContent::FlexStart,
                align_items: AlignItems::FlexStart,
                align_content: AlignContent::FlexStart,
                ..Default::default()
            },
            background_color: Color::srgb(0.25, 0.25, 0.25).into(),
            ..Default::default()
        })
        .id();

    // labels for the different border edges
    let border_labels = [
        "None",
        "All",
        "Left",
        "Right",
        "Top",
        "Bottom",
        "Left Right",
        "Top Bottom",
        "Top Left",
        "Bottom Left",
        "Top Right",
        "Bottom Right",
        "Top Bottom Right",
        "Top Bottom Left",
        "Top Left Right",
        "Bottom Left Right",
    ];

    // all the different combinations of border edges
    // these correspond to the labels above
    let borders = [
        UiRect::default(),
        UiRect::all(Val::Px(10.)),
        UiRect::left(Val::Px(10.)),
        UiRect::right(Val::Px(10.)),
        UiRect::top(Val::Px(10.)),
        UiRect::bottom(Val::Px(10.)),
        UiRect::horizontal(Val::Px(10.)),
        UiRect::vertical(Val::Px(10.)),
        UiRect {
            left: Val::Px(10.),
            top: Val::Px(10.),
            ..Default::default()
        },
        UiRect {
            left: Val::Px(10.),
            bottom: Val::Px(10.),
            ..Default::default()
        },
        UiRect {
            right: Val::Px(10.),
            top: Val::Px(10.),
            ..Default::default()
        },
        UiRect {
            right: Val::Px(10.),
            bottom: Val::Px(10.),
            ..Default::default()
        },
        UiRect {
            right: Val::Px(10.),
            top: Val::Px(10.),
            bottom: Val::Px(10.),
            ..Default::default()
        },
        UiRect {
            left: Val::Px(10.),
            top: Val::Px(10.),
            bottom: Val::Px(10.),
            ..Default::default()
        },
        UiRect {
            left: Val::Px(10.),
            right: Val::Px(10.),
            top: Val::Px(10.),
            ..Default::default()
        },
        UiRect {
            left: Val::Px(10.),
            right: Val::Px(10.),
            bottom: Val::Px(10.),
            ..Default::default()
        },
    ];

    for (label, border) in border_labels.into_iter().zip(borders) {
        let inner_spot = commands
            .spawn(NodeBundle {
                style: Style {
                    width: Val::Px(10.),
                    height: Val::Px(10.),
                    ..Default::default()
                },
                background_color: YELLOW.into(),
                ..Default::default()
            })
            .id();
        let border_node = commands
            .spawn((
                NodeBundle {
                    style: Style {
                        width: Val::Px(50.),
                        height: Val::Px(50.),
                        border,
                        margin: UiRect::all(Val::Px(20.)),
                        align_items: AlignItems::Center,
                        justify_content: JustifyContent::Center,
                        ..Default::default()
                    },
                    background_color: MAROON.into(),
                    border_color: RED.into(),
                    ..Default::default()
                },
                Outline {
                    width: Val::Px(6.),
                    offset: Val::Px(6.),
                    color: Color::WHITE,
                },
            ))
            .add_child(inner_spot)
            .id();
        let label_node = commands
            .spawn(TextBundle::from_section(
                label,
                TextStyle {
                    font_size: 9.0,
                    ..Default::default()
                },
            ))
            .id();
        let container = commands
            .spawn(NodeBundle {
                style: Style {
                    flex_direction: FlexDirection::Column,
                    align_items: AlignItems::Center,
                    ..Default::default()
                },
                ..Default::default()
            })
            .push_children(&[border_node, label_node])
            .id();
        commands.entity(root).add_child(container);
    }
}
mockersf commented 3 months ago

OnEnter(State) happens before Startup this is because of https://github.com/bevyengine/bevy/pull/14208 and we want it like that

after startup, states transitions happens in PreUpdate, the same schedule where things are being prepared for being usable, which make it a bad candidate to spawn things in, but I'm not sure what it's conflicting with

alice-i-cecile commented 3 weeks ago

15826 may be related.

alice-i-cecile commented 3 weeks ago

Can you reproduce this on main? We've resolved all of the non-rendering system order ambiugities, so there's a good chance this was incidentally fixed.

msvbg commented 3 weeks ago

Nope, can't reproduce anymore on latest main. Closing!