JavaFX AnimationTimer running at way more than 60 fps

Viewed 1027

Short description: Running a simple JavaFX program that illustrates AnimationTimer results in my PC almost locking up. The program runs fine on other PCs and on my PC when booted with other versions of Linux. Detailed description follows.

While proofreading a new version of an online Java textbook I ran into a problem running a sample program from the book that illustrates JavaFX AnimationTimer. The program source is SimpleAnimationTimer.java. See below. It is a very simple program that simply displays the current frame number and the elapsed time in seconds. The expected output is to see "60 frames per second"; that is, the displayed frame number divided by the displayed elapsed time should be 60.

When compiled and run on my PC it floods the application window so much that I am almost unable to move my mouse to another window so that I can kill the running program. When I notified the book's author, he responded that the program runs fine on all his PCs (Ubuntu, Linux Mint, Mac, Windows). My PC is running Fedora 27.

I downloaded and ran the latest Linux Mint from a live USB, then tested the program there using the same Java as is used for Fedora 27. The program runs without error there.

Next I rebooted an older Fedora 24 and tested the program there. It ran without error.

I downloaded and ran Fedora 28 from a live USB. The program has the problem there.

Summary of my testing environment and results.

Java version is 1.8.0_181. It is the one from Oracle.

Linux Mint 19 kernel 4.15.0-20-generic. No problems.

Fedora 24 kernel 4.11.12. No problems.

Fedora 27 kernel 4.17.17. Has the problem.

Fedora 28 kernel 4.16.3. Has the problem.

It appears that something changed in Linux between kernel versions 4.15 and 4.16 that is causing the problem.

The original SimpleAnimationStarter.java source was modified to stop after 60 frames. The Test02SimpleAnimationStarter.java source, shown below, does this. When this program was run, the displayed frame number was 60, as expected, and the displayed elapsed time was 0.3 seconds, not the expected 1 second.

At the present time I am at a loss as to what to do. Is this a JavaFX problem? Is this a Linux kernel problem? Something else?

Any suggestions/comments will be greatly appreciated. Also, it would be nice to see results when running the two programs on other platforms -- Windows, Mac, other Linux versions.

WARNING: Run the Test02 version first! Make sure the elapsed time is one second.

Source follows. First SimpleAnimationStarter.java then Test02SimpleAnimationStarter.java.

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.layout.BorderPane;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;

/**
 *  This file can be used to create very simple animations.  Just fill in
 *  the definition of drawFrame with the code to draw one frame of the 
 *  animation, and possibly change a few of the values in the rest of
 *  the program as noted below.
 */
public class SimpleAnimationStarter extends Application {

    /**
     * Draws one frame of an animation. This subroutine should be called
     * about 60 times per second.  It is responsible for redrawing the
     * entire drawing area. The parameter g is used for drawing. The frameNumber 
     * starts at zero and increases by 1 each time this subroutine is called.  
     * The parameter elapsedSeconds gives the number of seconds since the animation
     * was started.  By using frameNumber and/or elapsedSeconds in the drawing
     * code, you can make a picture that changes over time.  That's an animation.
     * The parameters width and height give the size of the drawing area, in pixels.  
     */
    public void drawFrame(GraphicsContext g, int frameNumber, double elapsedSeconds, int width, int height) {

        /* NOTE:  To get a different animation, just erase the contents of this 
         * subroutine and substitute your own. 
         */

        g.setFill(Color.WHITE);
        g.fillRect(0, 0, width, height); // First, fill the entire image with a background color!

        g.setFill(Color.BLACK);
        g.fillText( "Frame number " + frameNumber, 40, 50 );
        g.fillText( String.format("Elapsed Time: %1.1f seconds", elapsedSeconds), 40, 80);

    }

    //------ Implementation details: DO NOT EXPECT TO UNDERSTAND THIS ------


    private int frameNum;
    private long startTime;

    public void start(Stage stage) {
        int width = 800;   // The width of the image.  You can modify this value!
        int height = 600;  // The height of the image. You can modify this value!
        Canvas canvas = new Canvas(width,height);
        drawFrame(canvas.getGraphicsContext2D(), 0, 0, width, height);
        BorderPane root = new BorderPane(canvas);
        root.setStyle("-fx-border-width: 4px; -fx-border-color: #444");
        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.setTitle("Simple Animation"); // STRING APPEARS IN WINDOW TITLEBAR!
        stage.show();
        stage.setResizable(false);
        AnimationTimer anim = new AnimationTimer() {
            public void handle(long now) {
                if (startTime < 0)
                    startTime = now;
                frameNum++;
                drawFrame(canvas.getGraphicsContext2D(), frameNum, (now-startTime)/1e9, width, height);
            }
        };
        startTime = -1;
        anim.start();
    } 

    public static void main(String[] args) {
        launch();
    }

} // end SimpleAnimationStarter



import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.layout.BorderPane;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;

/**
 *  This file can be used to create very simple animations.  Just fill in
 *  the definition of drawFrame with the code to draw one frame of the 
 *  animation, and possibly change a few of the values in the rest of
 *  the program as noted below.
 */
public class Test02SimpleAnimationStarter extends Application {

    /**
     * Draws one frame of an animation. This subroutine should be called
     * about 60 times per second.  It is responsible for redrawing the
     * entire drawing area. The parameter g is used for drawing. The frameNumber 
     * starts at zero and increases by 1 each time this subroutine is called.  
     * The parameter elapsedSeconds gives the number of seconds since the animation
     * was started.  By using frameNumber and/or elapsedSeconds in the drawing
     * code, you can make a picture that changes over time.  That's an animation.
     * The parameters width and height give the size of the drawing area, in pixels.  
     */
    public void drawFrame(GraphicsContext g, int frameNumber, double elapsedSeconds, int width, int height) {

        /* NOTE:  To get a different animation, just erase the contents of this 
         * subroutine and substitute your own. 
         */

        g.setFill(Color.WHITE);
        g.fillRect(0, 0, width, height); // First, fill the entire image with a background color!

        g.setFill(Color.BLACK);
        g.fillText( "Frame number " + frameNumber, 40, 50 );
        g.fillText( String.format("Elapsed Time: %1.1f seconds", elapsedSeconds), 40, 80);

    }

    //------ Implementation details: DO NOT EXPECT TO UNDERSTAND THIS ------


    private int frameNum;
    private long startTime;

    AnimationTimer anim = null;  // Moved outside start() so handle() can call anim.stop()

    public void start(Stage stage) {
        int width = 800;   // The width of the image.  You can modify this value!
        int height = 600;  // The height of the image. You can modify this value!
        Canvas canvas = new Canvas(width,height);
        drawFrame(canvas.getGraphicsContext2D(), 0, 0, width, height);
        BorderPane root = new BorderPane(canvas);
        root.setStyle("-fx-border-width: 4px; -fx-border-color: #444");
        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.setTitle("Simple Animation"); // STRING APPEARS IN WINDOW TITLEBAR!
        stage.show();
        stage.setResizable(false);
//        AnimationTimer anim = new AnimationTimer() {            
        anim = new AnimationTimer() {
            // Constants: change NUM_SECS and/or framesPerSec
            private final int NUM_SECS = 1; // Desired number of seconds.
            private final int framesPerSec = 60; // Expected frames per second.
            private final int NUM_FRAMES = NUM_SECS*framesPerSec;

            public void handle(long now) {
                if (startTime < 0)
                    startTime = now;
                frameNum++;
                if (frameNum < NUM_FRAMES+1)
                {
                    drawFrame(canvas.getGraphicsContext2D(),
                              frameNum,
                              (now-startTime)/1e9,
                              width, height);
                }
                else if (frameNum == NUM_FRAMES+1)
                {
                    anim.stop();
                    System.out.println("Animation stopped");
                }
                else if (frameNum == NUM_FRAMES+2)
                {
                    System.out.println("Animation kept going after stop ???");
                }
            }
        };
        startTime = -1;
        anim.start();
    }

    public static void main(String[] args) {
        launch();
    }

} // end Test02SimpleAnimationStarter
0 Answers
Related