LWJGL 3 Getting Started Example - Frame Limiting

Started by bryanww, January 18, 2015, 02:03:23

Previous topic - Next topic

bryanww

The JLWJGL 3 Getting Started demo runs very smoothly but I noticed the main loop relies on vsync.  In LWJGL 3, is there any equivalent of Display.sync(60)?  I think it's unlikely but possible that the graphics card doesn't support vsync.  If that's the case or if vsync is disabled, then the main loop will be an infinite busy loop and burn through CPU and power.  I've tested this with glfwSwapInterval(0) just to be sure, and I found that it is indeed a busy loop.

I modified the demo and added a very cheap sync, and it protects the CPU when I set glfwSwapInterval(0).  I remember Display.sync(fps) being more elaborate (with a running average).  Can I get your opinion on the way I'm capping it below?  It tests correctly for me and I'm able to turn the framerate up and down, but I'm curious to know if my reasoning is correct or if I'm missing anything.

    private void loop() {
        // This line is critical for LWJGL's interoperation with GLFW's
        // OpenGL context, or any context that is managed externally.
        // LWJGL detects the context that is current in the current thread,
        // creates the ContextCapabilities instance and makes the OpenGL
        // bindings available for use.
        GLContext.createFromCurrent();
 
        // Set the clear color
        glClearColor(1.0f, 0.0f, 0.0f, 0.0f);
 
        // Run the rendering loop until the user has attempted to close
        // the window or has pressed the ESCAPE key.
        long lastNanos = System.nanoTime();
        while ( glfwWindowShouldClose(window) == GL_FALSE ) {
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // clear the framebuffer
            
            syncFrameRate(60, lastNanos);
            lastNanos = System.nanoTime();
            glfwSwapBuffers(window); // swap the color buffers
 
            // Poll for window events. The key callback above will only be
            // invoked during this call.
            glfwPollEvents();
        }
    }
    
    private void syncFrameRate(float fps, long lastNanos) {
        long targetNanos = lastNanos + (long) (1_000_000_000.0f / fps) - 1_000_000L;  // subtract 1 ms to skip the last sleep call
        try {
        	while (System.nanoTime() < targetNanos) {
        		Thread.sleep(1);
        	}
        }
        catch (InterruptedException ignore) {}
    }


Thanks!

DapperPixpy

I usually copy and paste this method in the various things I work on if it needs an FPS cap. It's a slightly edited version of what you find in the following link. http://forum.lwjgl.org/index.php?topic=4452.0
It's essentially a beefed up version of what you have, and should offer better accuracy. But what you have there should work, I think.

private long variableYieldTime, lastTime;
	
	/**
     * An accurate sync method that adapts automatically
     * to the system it runs on to provide reliable results.
     * 
     * @param fps The desired frame rate, in frames per second
     * @author kappa (On the LWJGL Forums)
     */
    private void sync(int fps) {
        if (fps <= 0) return;
         
        long sleepTime = 1000000000 / fps; // nanoseconds to sleep this frame
        // yieldTime + remainder micro & nano seconds if smaller than sleepTime
        long yieldTime = Math.min(sleepTime, variableYieldTime + sleepTime % (1000*1000));
        long overSleep = 0; // time the sync goes over by
         
        try {
            while (true) {
                long t = System.nanoTime() - lastTime;
                 
                if (t < sleepTime - yieldTime) {
                    Thread.sleep(1);
                }else if (t < sleepTime) {
                    // burn the last few CPU cycles to ensure accuracy
                    Thread.yield();
                }else {
                    overSleep = t - sleepTime;
                    break; // exit while loop
                }
            }
        } catch (InterruptedException e) {
        	e.printStackTrace();
        }finally{
        	lastTime = System.nanoTime() - Math.min(overSleep, sleepTime);
            
            // auto tune the time sync should yield
            if (overSleep > variableYieldTime) {
                // increase by 200 microseconds (1/5 a ms)
                variableYieldTime = Math.min(variableYieldTime + 200*1000, sleepTime);
            }
            else if (overSleep < variableYieldTime - 200*1000) {
                // decrease by 2 microseconds
                variableYieldTime = Math.max(variableYieldTime - 2*1000, 0);
            }
        }
    }
Currently working on a 2D game. I plan to have it out in (//TODO: Make time estimation.)

bryanww

Thank you very much, that's super helpful!  I now recall the technique of using Thread.yield() at the end of the sync to improve the fps accuracy due to the inaccuracy of Thread.sleep(1).  But your version/the one from the previous post also has a very slick accuracy adjustment, and I like it very much because it's simple and efficient yet effective because I can see how the 200 microsecond adjustments will converge to an accurate time while being resistant to spikes.  I will definitely incorporate this into my experiments.  Thanks!

Quarry

I think that code is quite the overkill, here's an implementation from http://gafferongames.com/game-physics/fix-your-timestep/;

        double time = 0;
        double deltaTime = 1000.0 / FRAMES_PER_SECOND;

        long currentTime = System.currentTimeMillis();
        long accumulator = 0;

        while (glfwWindowShouldClose(window) == GL_FALSE) {
            long newTime = System.currentTimeMillis();
            long frameTime = newTime - currentTime;
            currentTime = newTime;

            accumulator += frameTime;

            while (accumulator >= deltaTime) {
                accumulator -= deltaTime;
                time += deltaTime;

                render(); // Your own drawing code
            }
        }

quew8

The code you posted will not work particularly well.

Imagine your last frame took three times as long as you want it too. What your code will do is to do three render()s. And it will call each render() with a time value as if it was rendered at the right time (which would be at equal intervals) but actually won't. The time it will render them at depends completely on how long that frame takes.

If there is some lag in the render() (ie it takes longer than you want to run the loop at) then this function will only make things worse.

If the render() takes shorter than you want it too then you will not get a consistent simulation speed (because the time change the render() gets is not proportional to the actual time change) and the number of times it is called will not be limited.

What you actually want to happen is that when render() takes less time than you want it too, then the thread will wait the difference. So the frame rate is limited and the simulation is smooth. Of course if render() takes longer than you want it too there ain't much you can do about it. But you can at least not make it worse.

So whether @DapperPixpy's code is overkill or not at least it does what it is supposed to.

abcdef

Have a read of this article http://www.koonsolo.com/news/dewitters-gameloop/, it goes through various different strategies and talks and the pro's and con's of each.

Quarry

Quote from: quew8 on January 24, 2015, 18:18:41
The code you posted will not work particularly well.

Imagine your last frame took three times as long as you want it too. What your code will do is to do three render()s. And it will call each render() with a time value as if it was rendered at the right time (which would be at equal intervals) but actually won't. The time it will render them at depends completely on how long that frame takes.

If there is some lag in the render() (ie it takes longer than you want to run the loop at) then this function will only make things worse.

If the render() takes shorter than you want it too then you will not get a consistent simulation speed (because the time change the render() gets is not proportional to the actual time change) and the number of times it is called will not be limited.

What you actually want to happen is that when render() takes less time than you want it too, then the thread will wait the difference. So the frame rate is limited and the simulation is smooth. Of course if render() takes longer than you want it too there ain't much you can do about it. But you can at least not make it worse.

So whether @DapperPixpy's code is overkill or not at least it does what it is supposed to.

Actually, none of those are problem. The thread halts as long as the accumulated time is not enough, so if render takes shorter than it's "supposed" to the time will always be constant. If it takes longer than expected it will not be constant time (neither will DapperPixpy's code) but the game will still run as expected, just accumulating more and more time

quew8

Well we'll agree to disagree. I'm not interested in an argument. But the one last thing I will say is that nowhere in that code does the thread "halt." It is a continuous loop which eats up CPU time without doing anything. Not necessarily wrong but generally considered bad practise, especially in a language like Java where it is so easy to do it right.

Quarry

What am I supposed to do when I have nothing for the CPU to do when it "eats up time" though? Why is it bad?

quew8

If you want your program to wait then just looping is still making the computer "do something" which means that it is taking up CPU cycles which could be better spent on a different process, such as the OS or another program you have running. So we tell the operating system that we want our thread to go to sleep for a bit. That halts the execution of the thread until the operating system thinks the time delay is over.

So the static Thread.sleep(long millis) function or the instance Object.sleep(long millis) method. As far as I know, they do exactly the same thing (unless the object is a thread other than the main thread). But the key thing to take away from this is that they are really not very accurate. Roughly speaking it'll give you a good approximation but the actual accuracy depends on the operating system and processor (I assume).