Skip to content

UI leaves stale rendered content after despawning and replacing a Feathers UI scene #25365

Description

@lonegamedev

Bevy version and features

Bevy version: 0.19.0
Bevy is used with the default features.
The application uses Bevy's feathers feature/plugins.

[Optional] Relevant system information

AdapterInfo { name: "AMD Radeon RX 7900 XTX (RADV NAVI31)", vendor: 4098, device: 29772, device_type: DiscreteGpu, device_pci_bus_id: "0000:03:00.0", driver: "radv", driver_info: "Mesa 26.1.6-arch1.1", backend: Vulkan, subgroup_min_size: 32, subgroup_max_size: 64, transient_saves_memory: false }

What you did

I am building a UI using Bevy 0.19's Feathers/BSN APIs.

There is a persistent Camera3d used for rendering the game world, as well as a separate persistent Camera2d used exclusively for UI rendering.

The Camera2d is created once and remains alive while UI screens are switched using a Bevy State. Each UI screen is spawned on OnEnter and despawned on OnExit.

use bevy::{
    feathers::{
        FeathersPlugins,
        controls::{ButtonVariant, FeathersButton, FeathersTextInput, FeathersTextInputContainer},
        dark_theme::create_dark_theme,
        theme::{ThemedText, UiTheme},
    },
    input_focus::{AutoFocus, tab_navigation::TabGroup},
    prelude::*,
    text::{EditableText, TextEditChange},
    ui_widgets::Activate,
};

#[derive(States, Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
enum UiScreen {
    #[default]
    MainMenu,
    Connect,
}

#[derive(Component, Clone, Copy, Default)]
struct MainMenuRoot;

#[derive(Component, Clone, Copy, Default)]
struct ConnectMenuRoot;

#[derive(Component, Clone, Copy, Default)]
struct ServerAddressInput;

#[derive(Component, Clone, Copy, Default)]
struct LoginInput;

#[derive(Component, Clone, Copy, Default)]
struct PasswordInput;

#[derive(Resource, Debug, Clone)]
pub struct ConnectionForm {
    pub address: String,
    pub login: String,
    pub password: String,
}

impl Default for ConnectionForm {
    fn default() -> Self {
        Self {
            address: "127.0.0.1:25565".into(),
            login: String::new(),
            password: String::new(),
        }
    }
}

pub struct UiPlugin;

impl Plugin for UiPlugin {
    fn build(&self, app: &mut App) {
        app.add_plugins(FeathersPlugins)
            .insert_resource(UiTheme(create_dark_theme()))
            .init_resource::<ConnectionForm>()
            .init_state::<UiScreen>()
            .add_systems(Startup, ui_scene.spawn())
            .add_systems(OnEnter(UiScreen::MainMenu), main_menu.spawn())
            .add_systems(OnExit(UiScreen::MainMenu), despawn_main_menu)
            .add_systems(OnEnter(UiScreen::Connect), connect_menu.spawn())
            .add_systems(OnExit(UiScreen::Connect), despawn_connect_menu);
    }
}

fn ui_scene() -> impl SceneList {
    bsn_list![(
        Camera2d
        Camera {
            order: 1,
            clear_color: ClearColorConfig::None,
        }
    )]
}

fn main_menu() -> impl Scene {
    bsn! {
        (
            Node {
                width: percent(100),
                height: percent(100),
                display: Display::Flex,
                flex_direction: FlexDirection::Column,
                align_items: AlignItems::Center,
                justify_content: JustifyContent::Center,
                row_gap: px(12),
            }
            MainMenuRoot
            TabGroup

            Children [
                (
                    @FeathersButton {
                        @caption: bsn! {
                            Text("Start")
                            ThemedText
                        }
                        @variant: ButtonVariant::Primary,
                    }
                    AccessibleLabel("Start")
                    AutoFocus
                    on(|_: On<Activate>,
                        mut next: ResMut<NextState<UiScreen>>| {
                        next.set(UiScreen::Connect);
                    })
                ),
                (
                    @FeathersButton {
                        @caption: bsn! {
                            Text("Quit")
                            ThemedText
                        }
                    }
                    AccessibleLabel("Quit")
                    on(|_: On<Activate>,
                        mut exit: MessageWriter<AppExit>| {
                        exit.write(AppExit::Success);
                    })
                ),
            ]
        )
    }
}

fn connect_menu() -> impl Scene {
    bsn! {
        (
            Node {
                width: percent(100),
                height: percent(100),
                display: Display::Flex,
                flex_direction: FlexDirection::Column,
                align_items: AlignItems::Center,
                justify_content: JustifyContent::Center,
                row_gap: px(12),
            }
            ConnectMenuRoot
            TabGroup

            Children [
                (
                    Node {
                        display: Display::Flex,
                        flex_direction: FlexDirection::Column,
                        row_gap: px(4),
                        width: px(320),
                    }
                    Children [
                        (
                            Text("Server address")
                            ThemedText
                        ),
                        (
                            @FeathersTextInputContainer
                            Children [
                                (
                                    @FeathersTextInput {
                                        @visible_width: {Some(32.0)},
                                    }
                                    ServerAddressInput
                                    EditableText::new("127.0.0.1:25565")
                                    AccessibleLabel("Server address")
                                    AutoFocus
                                    on(update_server_address)
                                )
                            ]
                        )
                    ]
                ),

                (
                    Node {
                        display: Display::Flex,
                        flex_direction: FlexDirection::Column,
                        row_gap: px(4),
                        width: px(320),
                    }
                    Children [
                        (
                            Text("Login")
                            ThemedText
                        ),
                        (
                            @FeathersTextInputContainer
                            Children [
                                (
                                    @FeathersTextInput {
                                        @visible_width: {Some(32.0)},
                                    }
                                    LoginInput
                                    EditableText::new("")
                                    AccessibleLabel("Login")
                                    on(update_login)
                                )
                            ]
                        )
                    ]
                ),

                (
                    Node {
                        display: Display::Flex,
                        flex_direction: FlexDirection::Column,
                        row_gap: px(4),
                        width: px(320),
                    }
                    Children [
                        (
                            Text("Password")
                            ThemedText
                        ),
                        (
                            @FeathersTextInputContainer
                            Children [
                                (
                                    @FeathersTextInput {
                                        @visible_width: {Some(32.0)},
                                    }
                                    PasswordInput
                                    EditableText::new("")
                                    AccessibleLabel("Password")
                                    on(update_password)
                                )
                            ]
                        )
                    ]
                ),

                (
                    @FeathersButton {
                        @caption: bsn! {
                            Text("Connect")
                            ThemedText
                        }
                        @variant: ButtonVariant::Primary,
                    }
                    AccessibleLabel("Connect")
                    on(|_: On<Activate>,
                        form: Res<ConnectionForm>| {
                        info!(
                            address = %form.address,
                            login = %form.login,
                            "Connect requested"
                        );
                    })
                ),

                (
                    @FeathersButton {
                        @caption: bsn! {
                            Text("Back")
                            ThemedText
                        }
                    }
                    AccessibleLabel("Back")
                    on(|_: On<Activate>,
                        mut next: ResMut<NextState<UiScreen>>| {
                        next.set(UiScreen::MainMenu);
                    })
                ),
            ]
        )
    }
}

fn update_server_address(
    _change: On<TextEditChange>,
    input: Single<&EditableText, With<ServerAddressInput>>,
    mut form: ResMut<ConnectionForm>,
) {
    form.address = input.value().to_string();
}

fn update_login(
    _change: On<TextEditChange>,
    input: Single<&EditableText, With<LoginInput>>,
    mut form: ResMut<ConnectionForm>,
) {
    form.login = input.value().to_string();
}

fn update_password(
    _change: On<TextEditChange>,
    input: Single<&EditableText, With<PasswordInput>>,
    mut form: ResMut<ConnectionForm>,
) {
    form.password = input.value().to_string();
}

fn despawn_main_menu(mut commands: Commands, roots: Query<Entity, With<MainMenuRoot>>) {
    for entity in &roots {
        commands.entity(entity).despawn();
    }
}

fn despawn_connect_menu(mut commands: Commands, roots: Query<Entity, With<ConnectMenuRoot>>) {
    for entity in &roots {
        commands.entity(entity).despawn();
    }
}

fn main() {
    App::new()
        .add_plugins((DefaultPlugins, UiPlugin))
        .add_systems(Startup, scene.spawn())
        .run();
}

fn scene() -> impl SceneList {
    bsn_list! [
        (
            #CircularBase
            Mesh3d(asset_value(Circle::new(4.0)))
            MeshMaterial3d::<StandardMaterial>(asset_value(Color::WHITE))
            Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2))
        ),
        (
            #Cube
            Mesh3d(asset_value(Cuboid::new(1.0, 1.0, 1.0)))
            MeshMaterial3d::<StandardMaterial>(asset_value(Color::srgb_u8(124, 144, 255)))
            Transform::from_xyz(0.0, 0.5, 0.0)
        ),
        (
            PointLight {
                shadow_maps_enabled: true,
            }
            Transform::from_xyz(4.0, 8.0, 4.0)
        ),
        (
            Camera3d
            Msaa::Off
            template_value(Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y))
        )
    ]
}

What went wrong

After the MainMenuRoot hierarchy is despawned, the next frame should contain only the currently existing ConnectMenuRoot UI.

The new UI is rendered correctly, but the old UI appears to remain on screen as a visual "ghost".

Additional information

The issue is caused by Msaa::Off on Camera3d.
Changing size of window clears "ghost" - so I guess redraw is not triggered.
Removing additional camera also fixes it...perhaps this is not a bug, just skill issue. Up to you.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-RenderingDrawing game state to the screenA-UIGraphical user interfaces, styles, layouts, and widgetsC-BugAn unexpected or incorrect behaviorD-ModestA "normal" level of difficulty; suitable for simple features or challenging fixesS-Ready-For-ImplementationThis issue is ready for an implementation PR. Go for it!

    Type

    No type

    Projects

    • Status
      Needs SME Triage
    • Status
      Needs SME Triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions