advanceAndApply() never settles after hitting max iterations (60 fps with nothing changing)
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Aptitud para principiantes
- 30/100
- Tipo de issue
- Error
- Claridad
- Bien especificado
- Estado de actividad
- Activo
- Área
- frontend, performance
Línea de trabajo
Start in runtime/src/animation/state_machine_instance.cpp: StateMachineLayerInstance::advance() stops at maxIterations and returns false, while advanceAndApply() calls tryChangeState() again and sets keepGoing. Reproduce with the two-transition Any State cycle described in the issue, then check whether the layer keeps reporting true after the cap. Done means the layer settles, the log line stops repeating each frame, and the looping sample files still behave the same. The maintainers still need to decide between fix A and A′.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Submission checklist
- I have confirmed the issue is present in the latest version of the
riveFlutter package - I have searched the documentation and forums and could not find an answer
- I have searched existing issues and this is not a duplicate
Description
A state machine layer cycling through zero-duration transitions hits the 100-iteration cap and returns false, but StateMachineInstance::advanceAndApply() then calls tryChangeState() again, re-enters the same cycle and sets keepGoing = true. So advanceAndApply() returns true on every frame while nothing on screen changes (Artboard::didChange() stays false). FlutterRiveRenderBox only stops its ticker on false, so the app renders 60 fps of an unchanged picture forever.
The root cause in our case is the file (an Any State transition whose condition stays true targets a state that exits immediately) and we are fixing it. But the runtime makes it much worse:
- It undoes its own livelock detection: the extra
tryChangeState()re-armskeepGoingfor a cycle that cannot produce anything new, so the guard never stops anything from one frame to the next. - It waits 100 iterations for a cycle that repeats on its 2nd iteration (~200
StateInstanceallocations per frame). - It reports it only on stderr, which doesn't reach Android logcat; the editor doesn't flag the construct either.
Measurements
Device (Galaxy Z Flip6, Android 16, profile, 60 Hz), a tab bar with five Rive icons, one of them with the construct, at rest, 10 s windows:
| mode | fps | GPU busy | process CPU |
|---|---|---|---|
| stock | 59.3 | 55 % | 80 % |
| same tab bar, icons replaced by empty boxes | 0 | 0 % | 1 % |
C++ harness with a ticker that stops on false (600 frames): the icon is advanced on 483 frames with the shipped runtime vs 69 with fix A below; "exceeded max iterations" lines drop from 840 to 6; didChange and dirtied components are identical frame by frame. Across 56 files, 51 produce identical output and the 5 looping ones settle.
Root cause (runtime/src/animation/state_machine_instance.cpp, rive_native 0.1.11, unchanged in 0.2.0-dev.3)
StateMachineLayerInstance::advance():for (i = 0; updateState(); i++); atmaxIterationsit prints and returnsfalse.advanceAndApply()outer loop:if (tryChangeState()) { advance(0, false); keepGoing = true; }finds the same always-true transition again (canChangeState()only rejects the current state).
Proposed fix A (prototype, ~40 lines)
At the cap, if the cycle had no observable side effect (no event fired, no listener action, no random transition, no spilled time, no scripted condition), updateState() returns false for the rest of that advance(). Everything else still drives keepGoing (component dirt, data binding, nested artboards, other layers), and inputs / view model writes / pointer / layout already restart a stopped ticker. A cycle with side effects keeps looping as today.
Refinement A′: detect the cycle when a state is re-entered within the same frame with no side effect, instead of after 100 iterations (also removes the allocations and the log spam).
Diff against rive_native 0.1.11
--- a/runtime/src/animation/state_machine_instance.cpp
+++ b/runtime/src/animation/state_machine_instance.cpp
@@ -230,6 +230,8 @@ public:
}
m_currentState->advance(seconds, m_stateMachineInstance);
updateMix(seconds);
+ m_settledInCycle = false;
+ m_cycleIsOpaque = false;
if (m_stateFrom != nullptr && m_mix < 1.0f && !m_holdAnimationFrom)
{
@@ -249,6 +251,13 @@ public:
if (i == maxIterations)
{
+ // The layer is cycling through zero-duration transitions whose
+ // conditions stay true. When the cycle has no side effect the
+ // host can observe (no event, no listener action, no random
+ // pick, no spilled time, no scripted condition), re-running it
+ // next frame yields the same result: let the layer settle until
+ // an input, a view model property or a pointer wakes it.
+ m_settledInCycle = !m_cycleIsOpaque;
auto stateMachineName =
m_stateMachineInstance->stateMachine() == nullptr
? "[SM Not found]"
@@ -323,6 +332,10 @@ public:
bool updateState()
{
+ if (m_settledInCycle)
+ {
+ return false;
+ }
// Don't allow changing state while a transition is taking place
// (we're mixing one state onto another) if enableEarlyExit is not true.
if (isTransitioning() && !m_transition->enableEarlyExit())
@@ -347,6 +360,7 @@ public:
{
if (event->occurs() == occurs)
{
+ m_cycleIsOpaque = true;
event->perform(m_stateMachineInstance);
}
}
@@ -360,6 +374,7 @@ public:
{
if (action->matchesScheduledOccurrence(occurs))
{
+ m_cycleIsOpaque = true;
action->perform(m_stateMachineInstance,
ListenerInvocation::none());
}
@@ -409,6 +424,19 @@ public:
return;
}
+ // A scripted condition can change its answer without any input changing,
+ // so a cycle that evaluates one is never treated as settled.
+ void markOpaqueConditions(const StateTransition* transition)
+ {
+ for (size_t c = 0; c < transition->conditionCount(); c++)
+ {
+ if (transition->condition(c)->is<ScriptedTransitionCondition>())
+ {
+ m_cycleIsOpaque = true;
+ }
+ }
+ }
+
StateTransition* findRandomTransition(StateInstance* stateFromInstance)
{
uint32_t totalWeight = 0;
@@ -420,6 +448,7 @@ public:
if (canChangeState(transition->stateTo()))
{
+ markOpaqueConditions(transition);
auto allowed = transition->allowed(stateFromInstance,
m_stateMachineInstance,
this);
@@ -456,6 +485,7 @@ public:
(double)transition->evaluatedRandomWeight();
if (currentWeight + transitionWeight > randomWeight)
{
+ m_cycleIsOpaque = true;
transition->useLayerInConditions(m_stateMachineInstance,
this);
return transition;
@@ -484,6 +514,7 @@ public:
if (canChangeState(transition->stateTo()))
{
+ markOpaqueConditions(transition);
auto allowed = transition->allowed(stateFromInstance,
m_stateMachineInstance,
this);
@@ -619,6 +650,10 @@ public:
advanceTime = instance->spilledTime();
}
}
+ if (advanceTime != 0.0f)
+ {
+ m_cycleIsOpaque = true;
+ }
m_currentState->advance(advanceTime, m_stateMachineInstance);
}
m_mix = 0.0f;
@@ -705,6 +740,10 @@ private:
bool m_stateMachineChangedOnAdvance = false;
bool m_waitingForExit = false;
+ /// The layer hit maxIterations this frame on a cycle without observable
+ /// side effects; updateState() stops re-entering it until next advance.
+ bool m_settledInCycle = false;
+ bool m_cycleIsOpaque = false;
/// Used to ensure a specific animation is applied on the next apply.
const LinearAnimation* m_holdAnimation = nullptr;
float m_holdTime = 0.0f;
Questions
- Is returning
trueafter the cap intended? What change would the extratryChangeState()catch that doesn't already trigger an advance? - Would you accept fix A or A′? Did we miss a source of non-determinism?
- Could "exceeded max iterations" reach the host (callback /
__android_log_print) and the editor (warning on this construct)?
Reproduction steps / code
- View model: enum
iconState(notSelected,selected), boollightMode. - Animations:
Inactive_light/Inactive_dark(static pose),Active(500 ms). - One layer:
Any State ──[iconState == notSelected && lightMode]──▶ Inactive_light
Any State ──[iconState == notSelected && !lightMode]─▶ Inactive_dark
Any State ──[iconState changed to selected]──────────▶ Active
Inactive_light ──(no condition)──▶ Exit
Inactive_dark ──(no condition)──▶ Exit
- Show it with
RiveWidget,iconState = notSelected, don't touch it.
Observed: stderr "exceeded max iterations" twice per frame, advanceAndApply() true forever, 60 fps with nothing changing. Expected: the layer settles. Removing the two → Exit transitions makes it settle today.
Rive Flutter package version
rive 0.14.11 (rive_native 0.1.11); same code in rive_native 0.2.0-dev.3.
Flutter version
Flutter 3.47.3 (stable), Dart 3.13.3
Device
Samsung Galaxy Z Flip6 (SM-F741B); C++ harness on macOS.
OS version
Android 16
Additional context
Shared C++ runtime, so every platform is affected; it hurts most on Flutter because FlutterRiveRenderBox stops its ticker as soon as the runtime answers false. A separate issue covers the Flutter draw gate on didChange (parity with iOS/web): #665
- Lenguaje dominante
- Dart
- Estrellas
- 1.5k
- Forks
- 244
- Métricas de merge de PR
- Sin PR fusionados en 30 d
Preparar el entorno
Este proyecto no incluye contenedor de desarrollo, Dockerfile ni guía de contribución, así que la configuración corre por tu cuenta: empieza por su README y consulta nuestra guía para la primera contribución para los pasos generales.
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de rive-app/rive-flutter
-
bug known
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
rive-app/rive-flutter#617 · 6 comentarios · 15 reacciones ·
-
known
Dificultad 4/5 3-5 días Aptitud para principiantes 48/100
rive-app/rive-flutter#669 ·
-
known
Dificultad 4/5 3-5 días Aptitud para principiantes 35/100
rive-app/rive-flutter#668 · 1 comentario ·
-
Dificultad 4/5 3-5 días Aptitud para principiantes 45/100
rive-app/rive-flutter#667 ·
-
needs-repro
Dificultad 4/5 3-5 días Aptitud para principiantes 18/100
rive-app/rive-flutter#663 · 1 comentario ·
Todos los issues de rive-app/rive-flutter
Issues similares
-
文字サイズ最大で服薬タブの日付の見出しとカレンダータブの月の見出しが切れるPosiblemente ocupada Un pull request vinculado a esta issue está abierto o ya se fusionó. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
bannzai/medicalarm#572 ·
Los mantenedores suelen responder en 1 día
-
in triage
Dificultad 2/5 1-3 horas Aptitud para principiantes 77/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 69/100
Los mantenedores suelen responder en 4 días
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
dart-lang/ai#746 · 2 comentarios ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
google/skills_lint.dart#220 ·
Los mantenedores suelen responder en 1 día