bevyengine / bevyengine/bevy

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

Open
#25,365 2 comments 0 reactions 0 assignees View on GitHub
A-Rendering A-UI C-Bug D-Modest S-Ready-For-Implementation
Dominant language
Rust
Stars
48.2k
Forks
4.8k
Avg merge
3d 22h
Merged PRs (30d)
161

Description

## 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

```ignore
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::()
.init_state::()
.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,
mut next: ResMut>| {
next.set(UiScreen::Connect);
})
),
(
@FeathersButton {
@caption: bsn! {
Text("Quit")
ThemedText
}
}
AccessibleLabel("Quit")
on(|_: On,
mut exit: MessageWriter| {
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,
form: Res| {
info!(
address = %form.address,
login = %form.login,
"Connect requested"
);
})
),

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

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

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

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

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

fn despawn_connect_menu(mut commands: Commands, roots: Query>) {
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::(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::(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.

Contributor guide

Open the contributing guide

Research direction

Start by running the provided Bevy 0.19 reproduction with a persistent Camera3d, Camera2d, state-switched Feathers UI scenes, and Msaa::Off on Camera3d. Inspect the UI rendering and camera redraw behavior around despawning MainMenuRoot and spawning ConnectMenuRoot; done means switching screens no longer leaves the previous UI visibly ghosted without resizing the window.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
computer-graphics, game-dev
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.