Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8339606

AbstractPrimaryTimer should not use arbitrary nano second values for its animation base times

XMLWordPrintable

    • Icon: Bug Bug
    • Resolution: Unresolved
    • Icon: P4 P4
    • tbd
    • jfx22
    • javafx
    • None

      Animation callbacks use `System.nanoTime()` as their "base time" when calculating animation positions for Timelines/Animations/AnimationRunnables.

      However, this is a flawed proposition as the time used for calculating animations will be subject to severe jitter due to scheduling delays of the FX thread (only when the thread is scheduled is System.nanoTime called, which on Windows platform can introduce a 1-30 ms jitter, and on Mac/Linux platforms a bit less due to a scheduler with a smaller quantum).

      Furthermore, animations are supposed to be synced to the frame rate of a monitor, so on a 60 Hz monitor, the base times should all be exact multiples of 1/60 of a second. Adding small "randomness" to this value just means that supposedly pixel perfect animations will be drawn slightly offset (resulting in anti-aliasing) where none is expected.

      I've attached a program that clearly illustrates the problem below.

      The program will show the following:

      - It animates a vertical line from left to right that should be drawn on even positions only (0, 2, 4, 6, ...)
      - There is a "comb" at the top that is spaced exactly 2 pixels apart
      - While animating you should see no interaction with the animated line and the pre-drawn comb
      - There is a plot that shows the calculated X value of the rectangle, and its fractional value (and also plots "frame skips")

      On a correct implementation, you should see nothing out of ordinary. The plot is a single flat line, and there is no interaction with the comb.

      On a faulty implementation, the plot will show large fractions, frame skips, and the animated line will interact with the comb constantly (a pattern is drawn on the comb that looks a bit like a moire pattern).

      I will attach images showing the results of running this on Windows.

      You can also run this program with the System property "com.sun.scenario.animation.fixed.pulse.length" set to "true". This eliminates the moire pattern and fractional values and the animation is perfectly smooth on Windows.

      The reason for this is that with this property active is that pulses will be exact multiples of the frame rate. Setting this property however does not account for hiccups (frame skips) correctly.

      I also have a (hacky) solution for the problem.

      In AbstractPrimaryTimer move the field "nextPulseTime" outside the MainLoop class and put it in the main class. Then change the function `nanos()` to return:

          public long nanos() {
              if (fixedPulseLength > 0) {
                  return debugNanos;
              }

              return paused ? startPauseTime :
                nextPulseTime - totalPausedTime;
          }

      Note that in the above implementation we are no longer calling System.nanoTime, completely eliminating the jitter and randomness of the animation base time. The nextPulseTime value will still use nanoTime to find frame skips, but it will round the value correctly to a multiple of the frame rate. With this fix, there is no observable jitter or randomness anymore, even on the Windows platform where scheduling delays for the FX thread can sometimes reach 20-30 ms.

      Here is the program illustrating the problem:

          package com.sun.scenario.animation;
          
          import javafx.animation.KeyFrame;
          import javafx.animation.KeyValue;
          import javafx.animation.Timeline;
          import javafx.application.Application;
          import javafx.scene.Scene;
          import javafx.scene.canvas.Canvas;
          import javafx.scene.control.Label;
          import javafx.scene.layout.Background;
          import javafx.scene.layout.StackPane;
          import javafx.scene.paint.Color;
          import javafx.scene.shape.Rectangle;
          import javafx.stage.Stage;
          import javafx.util.Duration;
          
          public class PoorAnimationExample {
          
            public static void main(String[] args) {
              //System.setProperty("com.sun.scenario.animation.fixed.pulse.length", "true");
              Application.launch(App.class);
            }
          
            public static class App extends Application {
          
              @Override
              public void start(Stage primaryStage) {
                StackPane pane = new StackPane();
          
                pane.setBackground(Background.fill(Color.BROWN));
          
                Scene scene = new Scene(pane);
          
                primaryStage.setScene(scene);
                primaryStage.setMaximized(true);
                primaryStage.show();
          
                double H = primaryStage.getHeight();
          
                /*
                 * Set up animation
                 */
          
                Canvas canvas = new Canvas(2000, H);
          
                for(int i = 0; i < 2000; i += 2) {
                  canvas.getGraphicsContext2D().strokeLine(i, H / 10, i, H / 8); // create comb
                }
          
                Rectangle r = new Rectangle(1, 10000);
          
                r.setTranslateY(1000);
          
                pane.getChildren().add(r);
                pane.getChildren().add(canvas);
                pane.getChildren().add(new Label(
                  "Value of 'com.sun.scenario.animation.fixed.pulse.length' = " + System.getProperty("com.sun.scenario.animation.fixed.pulse.length") +
                  "\nPlatform = " + System.getProperty("os.name")
                ));
          
                // Set up a timeline that should move the rectangle exactly 2 pixels each frame (60 Hz screen):
                Timeline timeline = new Timeline(
                  new KeyFrame(Duration.ZERO, new KeyValue(r.translateXProperty(), 0)),
                  new KeyFrame(Duration.seconds(60), new KeyValue(r.translateXProperty(), 3600 * 2)) // 60 * 60 Hz
                );
          
                // Listen to the translate property, and illustrate how smooth (or not smooth)
                // the animation is being calculated:
                r.translateXProperty().subscribe((old, v) -> {
                  double likelyValue = Math.round(v.doubleValue() * 1000) / 1000.0 / 2; // Smooth out slight double calculation errors
                  double oldLikelyValue = Math.round(old.doubleValue() * 1000) / 1000.0 / 2;
          
                  double fraction = likelyValue % 1;
          
                  canvas.getGraphicsContext2D().strokeLine(likelyValue, (H / 4), likelyValue, (H / 4) + (H / 2) * fraction);
                  canvas.getGraphicsContext2D().strokeLine(likelyValue * 2, (H/ 10), likelyValue * 2, (H / 9)); // draw the position on the comb
          
                  if(likelyValue - oldLikelyValue > 1.1) { // frame skip
                    canvas.getGraphicsContext2D().strokeLine(likelyValue, (H / 4) - 10, likelyValue, (H / 4) - 5);
                  }
                });
          
                timeline.playFromStart();
              }
            }
          }

            kcr Kevin Rushforth
            jhendrikx John Hendrikx
            Votes:
            0 Vote for this issue
            Watchers:
            4 Start watching this issue

              Created:
              Updated: