fix(engine): restore 60 fps Android card animations + dead-code sweep #166
@@ -252,10 +252,17 @@ fn advance_card_anims(
|
||||
time: Res<Time>,
|
||||
paused: Option<Res<PausedResource>>,
|
||||
mut anims: Query<(Entity, &mut Transform, &mut CardAnim)>,
|
||||
mut redraw: MessageWriter<RequestRedraw>,
|
||||
) {
|
||||
if paused.is_some_and(|p| p.0) {
|
||||
return;
|
||||
}
|
||||
// Keep the winit loop awake at full frame rate while slides (including
|
||||
// staggered deals still in their delay phase) are in flight — required
|
||||
// for Android's reactive_low_power focused_mode.
|
||||
if !anims.is_empty() {
|
||||
redraw.write(RequestRedraw);
|
||||
}
|
||||
let dt = time.delta_secs();
|
||||
for (entity, mut transform, mut anim) in &mut anims {
|
||||
if anim.delay > 0.0 {
|
||||
@@ -558,10 +565,17 @@ fn drive_toast_display(
|
||||
paused: Option<Res<PausedResource>>,
|
||||
mut queue: ResMut<ToastQueue>,
|
||||
mut active: ResMut<ActiveToast>,
|
||||
mut redraw: MessageWriter<RequestRedraw>,
|
||||
) {
|
||||
if paused.is_some_and(|p| p.0) {
|
||||
return;
|
||||
}
|
||||
// Keep the loop ticking while a toast is displayed or queued so the
|
||||
// countdown advances and the despawn frame isn't held hostage by
|
||||
// Android's reactive_low_power wake ceiling.
|
||||
if active.entity.is_some() || !queue.0.is_empty() {
|
||||
redraw.write(RequestRedraw);
|
||||
}
|
||||
let dt = time.delta_secs();
|
||||
|
||||
// Tick down the active toast timer.
|
||||
|
||||
@@ -147,6 +147,7 @@ fn drive_auto_complete(
|
||||
time: Res<Time>,
|
||||
paused: Option<Res<PausedResource>>,
|
||||
mut moves: MessageWriter<MoveRequestEvent>,
|
||||
mut redraw: MessageWriter<RequestRedraw>,
|
||||
) {
|
||||
if !state.active {
|
||||
return;
|
||||
@@ -154,6 +155,10 @@ fn drive_auto_complete(
|
||||
if paused.is_some_and(|p| p.0) {
|
||||
return;
|
||||
}
|
||||
// Keepalive: the step-interval cooldown only advances on frames that
|
||||
// actually run, so keep the winit loop awake for the whole burst under
|
||||
// Android's reactive_low_power focused_mode.
|
||||
redraw.write(RequestRedraw);
|
||||
|
||||
state.cooldown -= time.delta_secs();
|
||||
if state.cooldown > 0.0 {
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
use std::f32::consts::PI;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy::window::RequestRedraw;
|
||||
|
||||
use super::curves::{MotionCurve, sample_curve};
|
||||
use super::timing::compute_duration;
|
||||
@@ -232,10 +233,18 @@ pub(crate) fn advance_card_animations(
|
||||
time: Res<Time>,
|
||||
paused: Option<Res<PausedResource>>,
|
||||
mut q: Query<(Entity, &mut Transform, &mut CardAnimation)>,
|
||||
mut redraw: MessageWriter<RequestRedraw>,
|
||||
) {
|
||||
if paused.is_some_and(|p| p.0) {
|
||||
return;
|
||||
}
|
||||
// Keep the winit event loop awake while any animation (including one
|
||||
// still in its delay phase) needs per-frame ticks. Without this,
|
||||
// Android's reactive_low_power focused_mode only wakes at its 100 ms
|
||||
// ceiling and card slides render at ~10 fps.
|
||||
if !q.is_empty() {
|
||||
redraw.write(RequestRedraw);
|
||||
}
|
||||
let dt = time.delta_secs();
|
||||
|
||||
for (entity, mut transform, mut anim) in &mut q {
|
||||
|
||||
@@ -307,6 +307,49 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Regression test for the v0.40.0 Android animation-lag bug: commit
|
||||
/// 38e4c03 switched Android to `reactive_low_power` focused_mode on the
|
||||
/// premise that animation systems write `RequestRedraw` while active,
|
||||
/// but the writers were never added — card slides rendered at the 100 ms
|
||||
/// wake ceiling (~10 fps). Active animations MUST emit `RequestRedraw`
|
||||
/// every frame; an idle board must not.
|
||||
#[test]
|
||||
fn active_card_animation_requests_redraw() {
|
||||
let mut app = App::new();
|
||||
app.add_plugins(MinimalPlugins)
|
||||
.add_plugins(CardAnimationPlugin);
|
||||
|
||||
// Idle board: no redraw requests.
|
||||
app.update();
|
||||
assert!(
|
||||
app.world().resource::<Messages<RequestRedraw>>().is_empty(),
|
||||
"no RequestRedraw expected while no animation is active"
|
||||
);
|
||||
|
||||
app.world_mut().spawn((
|
||||
Transform::from_translation(Vec3::ZERO),
|
||||
CardAnimation {
|
||||
start: Vec2::ZERO,
|
||||
end: Vec2::new(100.0, 0.0),
|
||||
elapsed: 0.0,
|
||||
duration: 1.0,
|
||||
curve: MotionCurve::Responsive,
|
||||
delay: 0.0,
|
||||
start_z: 0.0,
|
||||
end_z: 0.0,
|
||||
z_lift: 0.0,
|
||||
scale_start: 1.0,
|
||||
scale_end: 1.0,
|
||||
},
|
||||
));
|
||||
app.update();
|
||||
assert!(
|
||||
!app.world().resource::<Messages<RequestRedraw>>().is_empty(),
|
||||
"an active CardAnimation must write RequestRedraw each frame to \
|
||||
sustain the reactive render loop"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn card_animation_instant_snaps_on_zero_duration() {
|
||||
let mut app = App::new();
|
||||
|
||||
@@ -276,10 +276,16 @@ fn tick_shake_anim(
|
||||
time: Res<Time>,
|
||||
paused: Option<Res<PausedResource>>,
|
||||
mut anims: Query<(Entity, &mut Transform, &mut ShakeAnim)>,
|
||||
mut redraw: MessageWriter<RequestRedraw>,
|
||||
) {
|
||||
if paused.is_some_and(|p| p.0) {
|
||||
return;
|
||||
}
|
||||
// Sustain full-rate frames for the shake under Android's
|
||||
// reactive_low_power focused_mode.
|
||||
if !anims.is_empty() {
|
||||
redraw.write(RequestRedraw);
|
||||
}
|
||||
let dt = time.delta_secs();
|
||||
for (entity, mut transform, mut anim) in &mut anims {
|
||||
anim.elapsed += dt;
|
||||
@@ -356,10 +362,16 @@ fn tick_settle_anim(
|
||||
time: Res<Time>,
|
||||
paused: Option<Res<PausedResource>>,
|
||||
mut anims: Query<(Entity, &mut Transform, &mut SettleAnim)>,
|
||||
mut redraw: MessageWriter<RequestRedraw>,
|
||||
) {
|
||||
if paused.is_some_and(|p| p.0) {
|
||||
return;
|
||||
}
|
||||
// Sustain full-rate frames for the settle bounce under Android's
|
||||
// reactive_low_power focused_mode.
|
||||
if !anims.is_empty() {
|
||||
redraw.write(RequestRedraw);
|
||||
}
|
||||
let dt = time.delta_secs();
|
||||
for (entity, mut transform, mut anim) in &mut anims {
|
||||
anim.elapsed += dt;
|
||||
@@ -581,10 +593,16 @@ fn tick_foundation_flourish(
|
||||
(Entity, &mut Sprite, &mut FoundationMarkerFlourish),
|
||||
Without<FoundationFlourish>,
|
||||
>,
|
||||
mut redraw: MessageWriter<RequestRedraw>,
|
||||
) {
|
||||
if paused.is_some_and(|p| p.0) {
|
||||
return;
|
||||
}
|
||||
// Sustain full-rate frames for the flourish under Android's
|
||||
// reactive_low_power focused_mode.
|
||||
if !card_anims.is_empty() || !marker_anims.is_empty() {
|
||||
redraw.write(RequestRedraw);
|
||||
}
|
||||
let dt = time.delta_secs();
|
||||
|
||||
// Advance the King's scale pulse.
|
||||
|
||||
Reference in New Issue
Block a user