Game Loop runs slow in Mac OS X

I’m reading the book ‘Killer Game Programming in Java’ by Andrew Davison. The animation framwork example in Chapter 2, takes less than 20fps on my Macbook pro 15’ Retina with Java 1.6.0 installed. However, the code runs perfectly on my old P4 pc with WinXP… The example uses Java 3D timer, and active rendering with method paintScreen() instead of repaint()
It seems that the JPanel’s size has great impact on the fps, as a JPanel with size of (1280, 800) is much slower than one of (400, 300). I’ve google that but couldn’t get any specific answer, and I did try the following setting of Properties, but It didn’t work either.


System.setProperty("apple.awt.graphics.UseQuartz","true"); System.setProperty("apple.awt.graphics.EnableQ2DX","true");

Here is the code just for reference as most of them is not relevant to the problem. I think it’s something to do with the java rendering setting on Mac OS X. Thank you in advance!


import javax.swing.JFrame;
import javax.swing.JPanel;
import java.awt.BorderLayout;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Dimension;
import java.awt.Color;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.Toolkit;
import java.text.DecimalFormat;
import com.sun.j3d.utils.timer.J3DTimer;

public class Framework
{
    public static void main(String[] args)
    {
        new Framework().go();
    }

    public void go()
    {
        JFrame frame = new JFrame("Animation Framework");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        GamePanel mainPanel = new GamePanel(this, 16L * 1000000L);
        frame.getContentPane().add(mainPanel, BorderLayout.CENTER);
        frame.pack();
        frame.setResizable(false);
        frame.setVisible(true);
    }
}

public class GamePanel extends JPanel implements Runnable
{
    // size of panel
    private static final int PWIDTH = 1024;
    private static final int PHEIGHT = 768;

    // number of frames with delay of 0 ms before the animated thread yields to others
    private static final int NO_DELAYS_PER_YIELD = 16;

    // number of frames that can be skipped in any one animation loop
    // i.e the games's state is updated but not rendered
    private static final int MAX_FRAME_SKIPS = 5;

    // number of FPS stored to get an average
    private static final int NUM_FPS = 10;

    // unit change
    private static final double S2NS_D = 1000000000.0;
    private static final long S2NS_L = 1000000000L;
    private static final double MS2NS_D = 1000000.0;
    private static final long MS2NS_L = 1000000L;

    // record stats every 1 second(roughly)
    private static final long MAX_STATS_INTERVAL = S2NS_L;

    // used for gathering statistics
    // in nanoseconds
    private long statsInterval = 0L;
    private long prevStatsTime;   
    private long totalElapsedTime = 0L;
    private long gameStartTime;
    // in seconds
    private double timeSpentInGame = 0;

    private long frameCount = 0;
    private double[] fpsStore;
    private long statsCount = 0;
    private double averageFPS = 0.0;

    private long framesSkipped = 0L;
    private long totalFramesSkipped = 0L;
    private double[] upsStore;
    private double averageUPS = 0.0;

    private DecimalFormat df = new DecimalFormat("0.##");
    private DecimalFormat timedf = new DecimalFormat("0.####");

    // for the animation
    private Thread animator;

    private Framework top;

    // period between drawing in nanoseconds
    private long period = 16L * MS2NS_L;

    // stop the animation
    private volatile boolean running = false;
    // for the game termination
    private volatile boolean gameOver = false;
    // pause the game
    private volatile boolean isPaused = false;

    // global variables for off-screen rendering
    private Graphics dbg;
    private Image dbImage = null;

    public GamePanel(Framework top, long period)
    {
        this.top = top;
        this.period = period;

        setBackground(Color.red);
        setPreferredSize(new Dimension(PWIDTH, PHEIGHT));
        //setSize(PWIDTH, PHEIGHT);

        setFocusable(true);
        // JPanel now receives key events
        requestFocusInWindow();
        readyForTermination();

        // create game components
        // ..

        // listen for mouse presses
        addMouseListener( new MouseAdapter() {
            public void mousePressed(MouseEvent e)
            {
                testPress(e.getX(), e.getY());
            }
        });

        // initialise timing elements
        fpsStore = new double[NUM_FPS];
        upsStore = new double[NUM_FPS];
        for (int i = 0; i < NUM_FPS; i++)
        {
            fpsStore[i] = 0.0;
            upsStore[i] = 0.0;
        }
    }

    // wait for the JPanel to be added to the JFrame/JApplet before starting
    public void addNotify()
    {
        // create the peer
        super.addNotify();
        // start the thread
        startGame();
    }

    // initialize and start the thread
    private void startGame()
    {
        if (animator == null || !running)
        {
            animator = new Thread(this);
            animator.start();
        }
    }

    // called by the user to stop execution
    public void stopGame()
    {
        running = false;
    }

    // called by the user to pause
    public void pauseGame()
    {
        isPaused = true;
    }

    // called by the user to resume
    public void resumeGame()
    {
        isPaused = false;
    }

    // is (x, y) important to this game?
    private void testPress(int x, int y)
    {
        if (!gameOver && !isPaused)
        {
        }
    }

    private void readyForTermination()
    {
        addKeyListener( new KeyAdapter() {
            // listen for esc, q, end, ctrl-c
            public void keyPressed(KeyEvent e)
            {
                int keyCode = e.getKeyCode();
                if (keyCode == KeyEvent.VK_ESCAPE || 
                    keyCode == KeyEvent.VK_Q || 
                    keyCode == KeyEvent.VK_END || 
                    keyCode == KeyEvent.VK_C && e.isControlDown())
                {
                    running = false;
                }

                if (keyCode == KeyEvent.VK_P)
                {
                    pauseGame();
                }

                if (keyCode == KeyEvent.VK_R)
                {
                    resumeGame();
                }
            }
        });
    }

    // repeatedly update, render, sleep so loop takes close to period ms.
    // sleep inaccuracies are handled.
    // overruns in update/renders will cause extra updates to be carried out.
    // so UPS ~= requested FPS.
    public void run()
    {
        long beforeTime, afterTime, timeDiff, sleepTime;
        long overSleepTime = 0L;
        int noDelays = 0;
        long excess = 0L;

        gameStartTime = J3DTimer.getValue();
        prevStatsTime = gameStartTime;
        beforeTime = gameStartTime;

        running = true;
        while (running)
        {
            // game state is updated
            gameUpdate();
            // render to a buffer
            gameRender();
            
            /*
            // request painting with the buffer
            repaint();
            */

            // draw buffer to screen
            paintScreen();

            afterTime = J3DTimer.getValue();
            timeDiff = afterTime - beforeTime;
            sleepTime = (period - timeDiff) - overSleepTime;

            if (sleepTime > 0)
            {
                try
                {
                    // sleep a bit in millisecond
                    Thread.sleep((long)((double)sleepTime / MS2NS_D + 0.5));
                }
                catch (InterruptedException ex)
                {}

                overSleepTime = (J3DTimer.getValue() - afterTime) - sleepTime;
            }
            else
            {
                // sleepTime <= 0; frame took longer than the period
                overSleepTime = 0L;
                // store excess time value
                excess -= sleepTime;

                if (++noDelays >= NO_DELAYS_PER_YIELD)
                {
                    // give another thread a chance to run
                    Thread.yield();
                    noDelays = 0;
                }
            }

            beforeTime = J3DTimer.getValue();

            // if frame animation is taking too long, update the game state without rendering it,
            // to get the updates/sec nearer to the required FPS
            int skips = 0;
            while (excess > period && skips < MAX_FRAME_SKIPS)
            {
                excess -= period;
                // udpate state without rendering
                gameUpdate();
                skips++;
            }

            framesSkipped += skips;
            storeStats();
        }

        printStats();
        // so enclosing JFrame/JApplet exits
        System.exit(0);
    }

    private void gameUpdate()
    {
        if (!gameOver && !isPaused)
        {
        }
    }

    // draw the current frame to an image buffer
    private void gameRender()
    {
        // create the buffer
        if (dbImage == null)
        {
            dbImage = createImage(PWIDTH, PHEIGHT);
            if (dbImage == null)
            {
                System.out.println("dbImage is null");
                return;
            }
        }

        dbg = dbImage.getGraphics();

        // clear the background
        dbg.setColor(Color.black);
        dbg.fillRect(0, 0, PWIDTH / 2, PHEIGHT / 2);

        // draw game elements
        // ..

        if (gameOver)
        {
            gameOverMessage(dbg);
        }
    }

    // center the game-over message
    private void gameOverMessage(Graphics g)
    {
        //g.drawString(msg, x, y);
    }

    // actively render the buffer image to the screen
    private void paintScreen()
    {
        Graphics g;

        try
        {
            // the panel's graphics context must be freshly obtained 
            // each time it's needed, as it may be changed bye the JVM
            g = this.getGraphics();
            if (g != null && dbImage != null)
            {
                g.drawImage(dbImage, 0, 0, null);
            }
            // syn the display on some systems(i.e. Linux)
            Toolkit.getDefaultToolkit().sync();
            g.dispose();
        }
        catch (Exception e)
        {
            System.out.println("Graphics context error: " + e);
        }
    }

    private void storeStats()
    {
        ++frameCount;
        statsInterval += period;

        if (statsInterval >= MAX_STATS_INTERVAL)
        {
            long timeNow = J3DTimer.getValue();
            timeSpentInGame = (double)(timeNow - gameStartTime) / S2NS_D;

            // time since last stats collection
            long realElapsedTime = timeNow - prevStatsTime;
            totalElapsedTime += realElapsedTime;

            double timingErr = (double)(realElapsedTime - statsInterval) / (double)statsInterval * 
                100.0;

            totalFramesSkipped += framesSkipped;

            double actualFPS = 0.0;
            double actualUPS = 0.0;

            if (totalElapsedTime > 0)
            {
                actualFPS = (double)frameCount / totalElapsedTime * S2NS_D;
                actualUPS = (double)(frameCount + totalFramesSkipped) / totalElapsedTime * S2NS_D;
            }

            // store the latest FPS and UPS
            fpsStore[(int)statsCount % NUM_FPS] = actualFPS;
            upsStore[(int)statsCount % NUM_FPS] = actualUPS;
            ++statsCount;

            double totalFPS = 0.0;
            double totalUPS = 0.0;
            for (int i = 0; i < NUM_FPS; i++)
            {
                totalFPS += fpsStore[i];
                totalUPS += upsStore[i];
            }

            if (statsCount < NUM_FPS)
            {
                assert(statsCount > 0);
                averageFPS = totalFPS / statsCount;
                averageUPS = totalUPS / statsCount;
            }
            else
            {
                averageFPS = totalFPS / NUM_FPS;
                averageUPS = totalUPS / NUM_FPS;
            }

            System.out.println(timedf.format((double)statsInterval / S2NS_D) + "s " + 
                               timedf.format((double)realElapsedTime / S2NS_D) + "s " + 
                               df.format(timingErr) + "% " + frameCount + "c " + 
                               framesSkipped + "/" + totalFramesSkipped + " skips; " + 
                               df.format(actualFPS) + " " + df.format(averageFPS) + " afps; " + 
                               df.format(actualUPS) + " " + df.format(averageUPS) + " aups");

            framesSkipped = 0;
            prevStatsTime = timeNow;
            statsInterval = 0L;
        }
    }

    private void printStats()
    {
        System.out.println("Frame Count/Loss: " + frameCount + " / " + totalFramesSkipped);
	System.out.println("Average FPS: " + df.format(averageFPS));
	System.out.println("Average UPS: " + df.format(averageUPS));
        System.out.println("Time Spent: " + timeSpentInGame + " secs");
    }
}