Skip to content

MLD301: Updater applies a fixed per-frame step without dt scaling

qual reports MLD301 when a callback registered through Mobject.add_updater applies a relative mutation with a provably fixed step (shift, rotate, scale, increment_value with arguments built only from numeric literals and Manim constants such as RIGHT or PI) while declaring no parameter named dt.

  • Default severity: warning
  • Minimum confidence: high
  • Implementation phase: 4
  • Fix: none

Manim inspects the callback signature and calls it as (mobject, dt) only when a parameter literally named dt exists; otherwise the callback runs once per rendered frame with no time information (DESIGN 3.3). A fixed relative step then advances per frame: the same scene moves twice as fast at 60 FPS as at 30 FPS, so rendering with a different frame-rate profile changes the animation itself.

The rule does not forbid one-argument updaters. Absolute dependencies (next_to, move_to, tracker-driven values) are correct without dt, and any step the analyzer cannot prove constant (a closure variable, a ValueTracker read, a branch-dependent coefficient) stays silent. Scene-level updaters (Scene.add_updater) have a different (dt) contract and are out of scope.

Wrong:

dot.add_updater(lambda m: m.shift(0.1 * RIGHT))   # 6 units/s at 60 FPS, 3 at 30
square.add_updater(lambda m: m.rotate(0.05))      # rotation rate tracks FPS

Right:

dot.add_updater(lambda m, dt: m.shift(dt * RIGHT))  # units per second
dot.add_updater(lambda m: m.next_to(driver))        # absolute dependency